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 for p in [ad::BODYDIR, ad::PARDIR, ad::TEXTDIR] {
150 self.define(false, Loc::DirPar(p), Val::Int(i64::from(d)));
151 }
152 }
153
154 /// At the start of a simple or semi-simple group: LuaTeX resets its
155 /// group-local counters `\nolocalwhatsits` and `\nolocaldirs` (only
156 /// visible with `\tracingassigns`).
157 pub fn reset_local_counts(&mut self, whatsits: bool) {
158 let tracing = self
159 .eqtb
160 .int(crate::primitives_gen::assign_int::TRACINGASSIGNS)
161 > 0;
162 for i in [0, 1] {
163 if (i == 1 || whatsits) && (tracing || self.eqtb.local_count[i as usize].val != 0) {
164 self.define(false, Loc::LocalCount(i), Val::Int(0));
165 }
166 }
167 }
168
169 /// After the group of a box ended: back to the enclosing box's text
170 /// directions.
171 pub fn end_box_dirs(&mut self) {
172 if let Some(outer) = self.outer_text_dirs.pop() {
173 self.text_dirs = outer;
174 }
175 }
176
177 /// At the start of a paragraph: `\begindir` if a text direction is
178 /// recorded in the current box.
179 pub fn par_text_dir(&mut self) {
180 if !self.text_dirs.is_empty() {
181 let d = self.dir_param(ad::TEXTDIR).rem_euclid(4) as i8;
182 let p = self.new_dir_node(d, false);
183 self.tail_append(p);
184 }
185 }
186
187 /// Updates `open` (directions begun and not ended, outermost first)
188 /// with the dir nodes from `p` up to, not including, `stop`.
189 pub fn track_open_dirs(&self, mut p: NodeRef, stop: NodeRef, open: &mut Vec<i8>) {
190 while !p.is_null() && p != stop {
191 if let NodeData::Dir { dir, .. } = self.nodes.data(p) {
192 if self.nodes.subtype(p) == dir_sub::END {
193 open.pop();
194 } else {
195 open.push(*dir);
196 }
197 }
198 p = self.nodes.next(p);
199 }
200 }
201
202 /// A list of `\enddir` nodes for `open`, innermost first: (head, tail).
203 pub fn end_dir_nodes(&mut self, open: &[i8]) -> Option<(NodeRef, NodeRef)> {
204 self.dir_node_chain(open.iter().rev().copied(), true)
205 }
206
207 /// A list of `\begindir` nodes for `open`, outermost first.
208 pub fn begin_dir_nodes(&mut self, open: &[i8]) -> Option<(NodeRef, NodeRef)> {
209 self.dir_node_chain(open.iter().copied(), false)
210 }
211
212 fn dir_node_chain(
213 &mut self,
214 dirs: impl Iterator<Item = i8>,
215 end: bool,
216 ) -> Option<(NodeRef, NodeRef)> {
217 let mut head: Option<(NodeRef, NodeRef)> = None;
218 for d in dirs {
219 let p = self.new_dir_node(d, end);
220 head = match head {
221 None => Some((p, p)),
222 Some((h, t)) => {
223 self.nodes.link(t, p);
224 Some((h, p))
225 }
226 };
227 }
228 head
229 }
230}