retex-engine 1.3.0-alpha.2

The TeX typesetting engine for retex
Documentation
//! Directions: the direction parameters (`\pagedir`, `\bodydir`,
//! `\pardir`, `\textdir`, `\mathdir`, `\linedir` and their numeric
//! `…direction` forms), the directions of boxes, and the direction nodes
//! (`\begindir`/`\enddir`) that text-direction changes leave in
//! horizontal lists (LuaTeX manual §3.3).
//!
//! Behaviour established black-box against LuaTeX 1.22:
//!
//! * Direction parameters are saved and restored with groups like other
//!   parameters, and the assignment is local even with `\global`.
//!   `\tracingassigns` shows them as `\textdir TLT`.
//! * A box without `dir`/`bdir` gets the direction of the list it is made
//!   in: `\bodydir` in vertical mode, `\textdir` in horizontal mode and
//!   `\mathdir` in math mode. Inside the box, `\bodydir`, `\pardir` and
//!   `\textdir` are (re)assigned the box's direction, in that order. In a
//!   math shift group they are assigned `\mathdir`.
//! * A formula in text whose `\mathdir` differs from `\textdir` (both
//!   after the formula's group) gets `\begindir`/`\enddir` nodes just
//!   inside its math nodes.
//! * Simple and semi-simple groups start by (re)assigning two internal
//!   counters, `\nolocalwhatsits` and `\nolocaldirs`, to 0, and box
//!   groups `\nolocaldirs`; every `\textdir` adds one to `\nolocaldirs`.
//!   They show only in `\tracingassigns`/`\tracingrestores` output.
//! * `\textdir` in horizontal or math mode appends `\begindir <dir>`. If a
//!   text direction was already begun in the same group, an `\enddir` for
//!   it comes first (also for the same direction).
//! * In vertical mode the assignment only records the direction; a
//!   paragraph started while one is recorded in the current box begins
//!   with `\begindir <\textdir>` (after the local par node, before the
//!   indentation box), unless `\textdir` is `\pardir`.
//! * When a simple group (`{…}`) or a semi-simple group (`\begingroup…
//!   \endgroup`) ends outside vertical mode, the directions begun in it
//!   are ended, innermost first. Box and math groups end them silently.
//!   A box starts with no directions of its own.
//! * Line breaking: the last line gets `\enddir` nodes for the directions
//!   still open at the end of the paragraph, between the final
//!   `\penalty10000` and `\parfillskip`. Every other line ends with
//!   `\enddir` nodes for the directions open at its break (innermost
//!   first, before `\rightskip`), and the next line begins with
//!   `\begindir` nodes for them (outermost first).

use crate::Engine;
use crate::eqtb::{Loc, Val};
use crate::maincontrol::ModeKind;
use crate::nodes::{NodeData, NodeRef, id};
use crate::primitives_gen::assign_dir as ad;
use crate::save::GroupCode;

/// Dir node subtypes.
pub mod dir_sub {
    /// `\begindir` (LuaTeX's `normal`).
    pub const BEGIN: u16 = 0;
    /// `\enddir` (LuaTeX's `cancel`).
    pub const END: u16 = 1;
}

impl Engine {
    /// A new direction node, with the current attributes (observed:
    /// tagpdf finds its marked-content attribute on them).
    pub fn new_dir_node(&mut self, dir: i8, end: bool) -> NodeRef {
        let level = i32::from(self.save.cur_level);
        self.new_node(
            id::DIR,
            if end { dir_sub::END } else { dir_sub::BEGIN },
            NodeData::Dir { dir, level },
        )
    }

    /// After `\textdir` was set to `d` (from `prev`): records it and,
    /// outside vertical mode, appends the direction nodes.
    pub fn text_dir_assigned(&mut self, d: i32, prev: i32) {
        let level = self.save.cur_level;
        let dir = d.rem_euclid(4) as i8;
        let same_group = self.text_dirs.last().is_some_and(|&(l, _, _)| l == level);
        // The direction this one replaces (in the group: the one before
        // the group's first assignment).
        let mut before = prev.rem_euclid(4) as i8;
        if self.mode.abs_kind() == ModeKind::Vertical {
            if same_group && let Some((_, _, b)) = self.text_dirs.pop() {
                before = b;
            }
        } else {
            if same_group && let Some((_, old, b)) = self.text_dirs.pop() {
                before = b;
                let p = self.new_dir_node(old, true);
                self.tail_append(p);
            }
            let p = self.new_dir_node(dir, false);
            self.tail_append(p);
        }
        self.text_dirs.push((level, dir, before));
        let n = self.eqtb.local_count[1].val + 1;
        self.define(false, Loc::LocalCount(1), Val::Int(i64::from(n)));
    }

    /// Before a simple or semi-simple group ends: ends the directions
    /// begun in it (outside vertical mode).
    pub fn end_group_text_dirs(&mut self) {
        if self.mode.abs_kind() == ModeKind::Vertical {
            return;
        }
        let level = self.save.cur_level;
        let open: Vec<i8> = self
            .text_dirs
            .iter()
            .rev()
            .take_while(|&&(l, _, _)| l == level)
            .map(|&(_, d, _)| d)
            .collect();
        for d in open {
            let p = self.new_dir_node(d, true);
            self.tail_append(p);
        }
    }

    /// After a group ended: forgets the directions recorded in it.
    pub fn drop_text_dirs_above(&mut self, level: u16) {
        while self.text_dirs.last().is_some_and(|&(l, _, _)| l > level) {
            self.text_dirs.pop();
        }
    }

    /// The direction of a box being begun in the current mode, unless the
    /// box specification gives one.
    pub fn default_box_dir(&self) -> i32 {
        match self.mode.abs_kind() {
            ModeKind::Vertical => self.dir_param(ad::BODYDIR),
            ModeKind::Math => self.dir_param(ad::MATHDIR),
            _ => self.dir_param(ad::TEXTDIR),
        }
    }

    /// Just after the group of a box with direction `dir` began: sets the
    /// direction parameters inside it and starts its own text directions.
    pub fn begin_box_dirs(&mut self, group: GroupCode, dir: i32) {
        if !matches!(
            group,
            GroupCode::Hbox | GroupCode::AdjustedHbox | GroupCode::Vbox | GroupCode::Vtop
        ) {
            return;
        }
        for p in [ad::BODYDIR, ad::PARDIR, ad::TEXTDIR] {
            self.define(false, Loc::DirPar(p), Val::Int(i64::from(dir)));
        }
        self.reset_local_counts(false);
        let outer = std::mem::take(&mut self.text_dirs);
        self.outer_text_dirs.push(outer);
    }

    /// Just after a math shift group began: the direction parameters
    /// become `\mathdir` inside it.
    pub fn begin_math_dirs(&mut self) {
        let d = self.dir_param(ad::MATHDIR);
        let t = self.dir_param(ad::TEXTDIR);
        self.outer_math_dirs.push((self.save.cur_level, d, t));
        for p in [ad::BODYDIR, ad::PARDIR, ad::TEXTDIR] {
            self.define(false, Loc::DirPar(p), Val::Int(i64::from(d)));
        }
    }

    /// At the start of a simple or semi-simple group: LuaTeX resets its
    /// group-local counters `\nolocalwhatsits` and `\nolocaldirs` (only
    /// visible with `\tracingassigns`).
    pub fn reset_local_counts(&mut self, whatsits: bool) {
        let tracing = self
            .eqtb
            .int(crate::primitives_gen::assign_int::TRACINGASSIGNS)
            > 0;
        for i in [0, 1] {
            if (i == 1 || whatsits) && (tracing || self.eqtb.local_count[i as usize].val != 0) {
                self.define(false, Loc::LocalCount(i), Val::Int(0));
            }
        }
    }

    /// After the group of a box ended: back to the enclosing box's text
    /// directions.
    pub fn end_box_dirs(&mut self) {
        if let Some(outer) = self.outer_text_dirs.pop() {
            self.text_dirs = outer;
        }
    }

    /// At the start of a paragraph (observed): `\begindir` for the text
    /// direction outside the recorded ones, if it is not the paragraph's
    /// direction, then one for each recorded text direction. A direction
    /// set at the outermost level is not recorded as such: it is the one
    /// outside (`\pardir TRT \textdir TRT` there gives none, inside a
    /// group or a box `\begindir TLT` and `\begindir TRT`).
    pub fn par_text_dir(&mut self) {
        let inner: Vec<(i8, i8)> = self
            .text_dirs
            .iter()
            .filter(|&&(l, _, _)| l > 1)
            .map(|&(_, d, b)| (d, b))
            .collect();
        let outside = match inner.first() {
            Some(&(_, b)) => b,
            None => self.dir_param(ad::TEXTDIR).rem_euclid(4) as i8,
        };
        if i32::from(outside) != self.dir_param(ad::PARDIR) {
            let p = self.new_dir_node(outside, false);
            self.tail_append(p);
        }
        for (d, _) in inner {
            let p = self.new_dir_node(d, false);
            self.tail_append(p);
        }
    }

    /// Updates `open` (directions begun and not ended, outermost first)
    /// with the dir nodes from `p` up to, not including, `stop`.
    pub fn track_open_dirs(&self, mut p: NodeRef, stop: NodeRef, open: &mut Vec<i8>) {
        while !p.is_null() && p != stop {
            if let NodeData::Dir { dir, .. } = self.nodes.data(p) {
                if self.nodes.subtype(p) == dir_sub::END {
                    open.pop();
                } else {
                    open.push(*dir);
                }
            }
            p = self.nodes.next(p);
        }
    }

    /// A list of `\enddir` nodes for `open`, innermost first: (head, tail).
    pub fn end_dir_nodes(&mut self, open: &[i8]) -> Option<(NodeRef, NodeRef)> {
        self.dir_node_chain(open.iter().rev().copied(), true)
    }

    /// A list of `\begindir` nodes for `open`, outermost first.
    pub fn begin_dir_nodes(&mut self, open: &[i8]) -> Option<(NodeRef, NodeRef)> {
        self.dir_node_chain(open.iter().copied(), false)
    }

    fn dir_node_chain(
        &mut self,
        dirs: impl Iterator<Item = i8>,
        end: bool,
    ) -> Option<(NodeRef, NodeRef)> {
        let mut head: Option<(NodeRef, NodeRef)> = None;
        for d in dirs {
            let p = self.new_dir_node(d, end);
            head = match head {
                None => Some((p, p)),
                Some((h, t)) => {
                    self.nodes.link(t, p);
                    Some((h, p))
                }
            };
        }
        head
    }
}