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), unless `\textdir` is `\pardir`.
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` (from `prev`): records it and,
69 /// outside vertical mode, appends the direction nodes.
70 pub fn text_dir_assigned(&mut self, d: i32, prev: 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 // The direction this one replaces (in the group: the one before
75 // the group's first assignment).
76 let mut before = prev.rem_euclid(4) as i8;
77 if self.mode.abs_kind() == ModeKind::Vertical {
78 if same_group && let Some((_, _, b)) = self.text_dirs.pop() {
79 before = b;
80 }
81 } else {
82 if same_group && let Some((_, old, b)) = self.text_dirs.pop() {
83 before = b;
84 let p = self.new_dir_node(old, true);
85 self.tail_append(p);
86 }
87 let p = self.new_dir_node(dir, false);
88 self.tail_append(p);
89 }
90 self.text_dirs.push((level, dir, before));
91 let n = self.eqtb.local_count[1].val + 1;
92 self.define(false, Loc::LocalCount(1), Val::Int(i64::from(n)));
93 }
94
95 /// Before a simple or semi-simple group ends: ends the directions
96 /// begun in it (outside vertical mode).
97 pub fn end_group_text_dirs(&mut self) {
98 if self.mode.abs_kind() == ModeKind::Vertical {
99 return;
100 }
101 let level = self.save.cur_level;
102 let open: Vec<i8> = self
103 .text_dirs
104 .iter()
105 .rev()
106 .take_while(|&&(l, _, _)| l == level)
107 .map(|&(_, d, _)| d)
108 .collect();
109 for d in open {
110 let p = self.new_dir_node(d, true);
111 self.tail_append(p);
112 }
113 }
114
115 /// After a group ended: forgets the directions recorded in it.
116 pub fn drop_text_dirs_above(&mut self, level: u16) {
117 while self.text_dirs.last().is_some_and(|&(l, _, _)| l > level) {
118 self.text_dirs.pop();
119 }
120 }
121
122 /// The direction of a box being begun in the current mode, unless the
123 /// box specification gives one.
124 pub fn default_box_dir(&self) -> i32 {
125 match self.mode.abs_kind() {
126 ModeKind::Vertical => self.dir_param(ad::BODYDIR),
127 ModeKind::Math => self.dir_param(ad::MATHDIR),
128 _ => self.dir_param(ad::TEXTDIR),
129 }
130 }
131
132 /// Just after the group of a box with direction `dir` began: sets the
133 /// direction parameters inside it and starts its own text directions.
134 pub fn begin_box_dirs(&mut self, group: GroupCode, dir: i32) {
135 if !matches!(
136 group,
137 GroupCode::Hbox | GroupCode::AdjustedHbox | GroupCode::Vbox | GroupCode::Vtop
138 ) {
139 return;
140 }
141 for p in [ad::BODYDIR, ad::PARDIR, ad::TEXTDIR] {
142 self.define(false, Loc::DirPar(p), Val::Int(i64::from(dir)));
143 }
144 self.reset_local_counts(false);
145 let outer = std::mem::take(&mut self.text_dirs);
146 self.outer_text_dirs.push(outer);
147 }
148
149 /// Just after a math shift group began: the direction parameters
150 /// become `\mathdir` inside it.
151 pub fn begin_math_dirs(&mut self) {
152 let d = self.dir_param(ad::MATHDIR);
153 let t = self.dir_param(ad::TEXTDIR);
154 self.outer_math_dirs.push((self.save.cur_level, d, t));
155 for p in [ad::BODYDIR, ad::PARDIR, ad::TEXTDIR] {
156 self.define(false, Loc::DirPar(p), Val::Int(i64::from(d)));
157 }
158 }
159
160 /// At the start of a simple or semi-simple group: LuaTeX resets its
161 /// group-local counters `\nolocalwhatsits` and `\nolocaldirs` (only
162 /// visible with `\tracingassigns`).
163 pub fn reset_local_counts(&mut self, whatsits: bool) {
164 let tracing = self
165 .eqtb
166 .int(crate::primitives_gen::assign_int::TRACINGASSIGNS)
167 > 0;
168 for i in [0, 1] {
169 if (i == 1 || whatsits) && (tracing || self.eqtb.local_count[i as usize].val != 0) {
170 self.define(false, Loc::LocalCount(i), Val::Int(0));
171 }
172 }
173 }
174
175 /// After the group of a box ended: back to the enclosing box's text
176 /// directions.
177 pub fn end_box_dirs(&mut self) {
178 if let Some(outer) = self.outer_text_dirs.pop() {
179 self.text_dirs = outer;
180 }
181 }
182
183 /// At the start of a paragraph (observed): `\begindir` for the text
184 /// direction outside the recorded ones, if it is not the paragraph's
185 /// direction, then one for each recorded text direction. A direction
186 /// set at the outermost level is not recorded as such: it is the one
187 /// outside (`\pardir TRT \textdir TRT` there gives none, inside a
188 /// group or a box `\begindir TLT` and `\begindir TRT`).
189 pub fn par_text_dir(&mut self) {
190 let inner: Vec<(i8, i8)> = self
191 .text_dirs
192 .iter()
193 .filter(|&&(l, _, _)| l > 1)
194 .map(|&(_, d, b)| (d, b))
195 .collect();
196 let outside = match inner.first() {
197 Some(&(_, b)) => b,
198 None => self.dir_param(ad::TEXTDIR).rem_euclid(4) as i8,
199 };
200 if i32::from(outside) != self.dir_param(ad::PARDIR) {
201 let p = self.new_dir_node(outside, false);
202 self.tail_append(p);
203 }
204 for (d, _) in inner {
205 let p = self.new_dir_node(d, false);
206 self.tail_append(p);
207 }
208 }
209
210 /// Updates `open` (directions begun and not ended, outermost first)
211 /// with the dir nodes from `p` up to, not including, `stop`.
212 pub fn track_open_dirs(&self, mut p: NodeRef, stop: NodeRef, open: &mut Vec<i8>) {
213 while !p.is_null() && p != stop {
214 if let NodeData::Dir { dir, .. } = self.nodes.data(p) {
215 if self.nodes.subtype(p) == dir_sub::END {
216 open.pop();
217 } else {
218 open.push(*dir);
219 }
220 }
221 p = self.nodes.next(p);
222 }
223 }
224
225 /// A list of `\enddir` nodes for `open`, innermost first: (head, tail).
226 pub fn end_dir_nodes(&mut self, open: &[i8]) -> Option<(NodeRef, NodeRef)> {
227 self.dir_node_chain(open.iter().rev().copied(), true)
228 }
229
230 /// A list of `\begindir` nodes for `open`, outermost first.
231 pub fn begin_dir_nodes(&mut self, open: &[i8]) -> Option<(NodeRef, NodeRef)> {
232 self.dir_node_chain(open.iter().copied(), false)
233 }
234
235 fn dir_node_chain(
236 &mut self,
237 dirs: impl Iterator<Item = i8>,
238 end: bool,
239 ) -> Option<(NodeRef, NodeRef)> {
240 let mut head: Option<(NodeRef, NodeRef)> = None;
241 for d in dirs {
242 let p = self.new_dir_node(d, end);
243 head = match head {
244 None => Some((p, p)),
245 Some((h, t)) => {
246 self.nodes.link(t, p);
247 Some((h, p))
248 }
249 };
250 }
251 head
252 }
253}