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