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).
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}