Skip to main content

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}