retex_engine/dirs.rs
1//! Directions: the direction parameters (`\pagedir`, `\bodydir`,
2//! `\pardir`, `\textdir`, `\mathdir`, `\linedir` and their numeric
3//! `…direction` forms), the directions of boxes, and the direction nodes
4//! (`\begindir`/`\enddir`) that text-direction changes leave in
5//! horizontal lists (LuaTeX manual §3.3).
6//!
7//! Behaviour established black-box against LuaTeX 1.22:
8//!
9//! * Direction parameters are saved and restored with groups like other
10//! parameters, and the assignment is local even with `\global`.
11//! `\tracingassigns` shows them as `\textdir TLT`.
12//! * A box without `dir`/`bdir` gets the direction of the list it is made
13//! in: `\bodydir` in vertical mode, `\textdir` in horizontal mode and
14//! `\mathdir` in math mode. Inside the box, `\bodydir`, `\pardir` and
15//! `\textdir` are (re)assigned the box's direction, in that order. In a
16//! math shift group they are assigned `\mathdir`.
17//! * A formula in text whose `\mathdir` differs from `\textdir` (both
18//! after the formula's group) gets `\begindir`/`\enddir` nodes just
19//! inside its math nodes.
20//! * Simple and semi-simple groups start by (re)assigning two internal
21//! counters, `\nolocalwhatsits` and `\nolocaldirs`, to 0, and box
22//! groups `\nolocaldirs`; every `\textdir` adds one to `\nolocaldirs`.
23//! They show only in `\tracingassigns`/`\tracingrestores` output.
24//! * `\textdir` in horizontal or math mode appends `\begindir <dir>`. If a
25//! text direction was already begun in the same group, an `\enddir` for
26//! it comes first (also for the same direction).
27//! * In vertical mode the assignment only records the direction; a
28//! paragraph started while one is recorded in the current box begins
29//! with `\begindir <\textdir>` (after the local par node, before the
30//! indentation box).
31//! * When a simple group (`{…}`) or a semi-simple group (`\begingroup…
32//! \endgroup`) ends outside vertical mode, the directions begun in it
33//! are ended, innermost first. Box and math groups end them silently.
34//! A box starts with no directions of its own.
35//! * Line breaking: the last line gets `\enddir` nodes for the directions
36//! still open at the end of the paragraph, between the final
37//! `\penalty10000` and `\parfillskip`. Every other line ends with
38//! `\enddir` nodes for the directions open at its break (innermost
39//! first, before `\rightskip`), and the next line begins with
40//! `\begindir` nodes for them (outermost first).
41
42use crate::Engine;
43use crate::eqtb::{Loc, Val};
44use crate::maincontrol::ModeKind;
45use crate::nodes::{NodeData, NodeRef, id};
46use crate::primitives_gen::assign_dir as ad;
47use crate::save::GroupCode;
48
49/// Dir node subtypes.
50pub mod dir_sub {
51 /// `\begindir` (LuaTeX's `normal`).
52 pub const BEGIN: u16 = 0;
53 /// `\enddir` (LuaTeX's `cancel`).
54 pub const END: u16 = 1;
55}
56
57impl Engine {
58 /// A new direction node.
59 pub fn new_dir_node(&mut self, dir: i8, end: bool) -> NodeRef {
60 let level = i32::from(self.save.cur_level);
61 self.nodes.alloc(
62 id::DIR,
63 if end { dir_sub::END } else { dir_sub::BEGIN },
64 NodeData::Dir { dir, level },
65 )
66 }
67
68 /// After `\textdir` was set to `d`: records it and, outside vertical
69 /// mode, appends the direction nodes.
70 pub fn text_dir_assigned(&mut self, d: i32) {
71 let level = self.save.cur_level;
72 let dir = d.rem_euclid(4) as i8;
73 let same_group = self.text_dirs.last().is_some_and(|&(l, _)| l == level);
74 if self.mode.abs_kind() == ModeKind::Vertical {
75 if same_group {
76 self.text_dirs.pop();
77 }
78 } else {
79 if same_group && let Some((_, old)) = self.text_dirs.pop() {
80 let p = self.new_dir_node(old, true);
81 self.tail_append(p);
82 }
83 let p = self.new_dir_node(dir, false);
84 self.tail_append(p);
85 }
86 self.text_dirs.push((level, dir));
87 let n = self.eqtb.local_count[1].val + 1;
88 self.define(false, Loc::LocalCount(1), Val::Int(i64::from(n)));
89 }
90
91 /// Before a simple or semi-simple group ends: ends the directions
92 /// begun in it (outside vertical mode).
93 pub fn end_group_text_dirs(&mut self) {
94 if self.mode.abs_kind() == ModeKind::Vertical {
95 return;
96 }
97 let level = self.save.cur_level;
98 let open: Vec<i8> = self
99 .text_dirs
100 .iter()
101 .rev()
102 .take_while(|&&(l, _)| l == level)
103 .map(|&(_, d)| d)
104 .collect();
105 for d in open {
106 let p = self.new_dir_node(d, true);
107 self.tail_append(p);
108 }
109 }
110
111 /// After a group ended: forgets the directions recorded in it.
112 pub fn drop_text_dirs_above(&mut self, level: u16) {
113 while self.text_dirs.last().is_some_and(|&(l, _)| l > level) {
114 self.text_dirs.pop();
115 }
116 }
117
118 /// The direction of a box being begun in the current mode, unless the
119 /// box specification gives one.
120 pub fn default_box_dir(&self) -> i32 {
121 match self.mode.abs_kind() {
122 ModeKind::Vertical => self.dir_param(ad::BODYDIR),
123 ModeKind::Math => self.dir_param(ad::MATHDIR),
124 _ => self.dir_param(ad::TEXTDIR),
125 }
126 }
127
128 /// Just after the group of a box with direction `dir` began: sets the
129 /// direction parameters inside it and starts its own text directions.
130 pub fn begin_box_dirs(&mut self, group: GroupCode, dir: i32) {
131 if !matches!(
132 group,
133 GroupCode::Hbox | GroupCode::AdjustedHbox | GroupCode::Vbox | GroupCode::Vtop
134 ) {
135 return;
136 }
137 for p in [ad::BODYDIR, ad::PARDIR, ad::TEXTDIR] {
138 self.define(false, Loc::DirPar(p), Val::Int(i64::from(dir)));
139 }
140 self.reset_local_counts(false);
141 let outer = std::mem::take(&mut self.text_dirs);
142 self.outer_text_dirs.push(outer);
143 }
144
145 /// Just after a math shift group began: the direction parameters
146 /// become `\mathdir` inside it.
147 pub fn begin_math_dirs(&mut self) {
148 let d = self.dir_param(ad::MATHDIR);
149 let t = self.dir_param(ad::TEXTDIR);
150 self.outer_math_dirs.push((self.save.cur_level, d, t));
151 for p in [ad::BODYDIR, ad::PARDIR, ad::TEXTDIR] {
152 self.define(false, Loc::DirPar(p), Val::Int(i64::from(d)));
153 }
154 }
155
156 /// At the start of a simple or semi-simple group: LuaTeX resets its
157 /// group-local counters `\nolocalwhatsits` and `\nolocaldirs` (only
158 /// visible with `\tracingassigns`).
159 pub fn reset_local_counts(&mut self, whatsits: bool) {
160 let tracing = self
161 .eqtb
162 .int(crate::primitives_gen::assign_int::TRACINGASSIGNS)
163 > 0;
164 for i in [0, 1] {
165 if (i == 1 || whatsits) && (tracing || self.eqtb.local_count[i as usize].val != 0) {
166 self.define(false, Loc::LocalCount(i), Val::Int(0));
167 }
168 }
169 }
170
171 /// After the group of a box ended: back to the enclosing box's text
172 /// directions.
173 pub fn end_box_dirs(&mut self) {
174 if let Some(outer) = self.outer_text_dirs.pop() {
175 self.text_dirs = outer;
176 }
177 }
178
179 /// At the start of a paragraph: `\begindir` if a text direction is
180 /// recorded in the current box.
181 pub fn par_text_dir(&mut self) {
182 if !self.text_dirs.is_empty() {
183 let d = self.dir_param(ad::TEXTDIR).rem_euclid(4) as i8;
184 let p = self.new_dir_node(d, false);
185 self.tail_append(p);
186 }
187 }
188
189 /// Updates `open` (directions begun and not ended, outermost first)
190 /// with the dir nodes from `p` up to, not including, `stop`.
191 pub fn track_open_dirs(&self, mut p: NodeRef, stop: NodeRef, open: &mut Vec<i8>) {
192 while !p.is_null() && p != stop {
193 if let NodeData::Dir { dir, .. } = self.nodes.data(p) {
194 if self.nodes.subtype(p) == dir_sub::END {
195 open.pop();
196 } else {
197 open.push(*dir);
198 }
199 }
200 p = self.nodes.next(p);
201 }
202 }
203
204 /// A list of `\enddir` nodes for `open`, innermost first: (head, tail).
205 pub fn end_dir_nodes(&mut self, open: &[i8]) -> Option<(NodeRef, NodeRef)> {
206 self.dir_node_chain(open.iter().rev().copied(), true)
207 }
208
209 /// A list of `\begindir` nodes for `open`, outermost first.
210 pub fn begin_dir_nodes(&mut self, open: &[i8]) -> Option<(NodeRef, NodeRef)> {
211 self.dir_node_chain(open.iter().copied(), false)
212 }
213
214 fn dir_node_chain(
215 &mut self,
216 dirs: impl Iterator<Item = i8>,
217 end: bool,
218 ) -> Option<(NodeRef, NodeRef)> {
219 let mut head: Option<(NodeRef, NodeRef)> = None;
220 for d in dirs {
221 let p = self.new_dir_node(d, end);
222 head = match head {
223 None => Some((p, p)),
224 Some((h, t)) => {
225 self.nodes.link(t, p);
226 Some((h, p))
227 }
228 };
229 }
230 head
231 }
232}