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, 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}