retex-engine 0.9.0

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).
//! * 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.
    pub fn new_dir_node(&mut self, dir: i8, end: bool) -> NodeRef {
        let level = i32::from(self.save.cur_level);
        self.nodes.alloc(
            id::DIR,
            if end { dir_sub::END } else { dir_sub::BEGIN },
            NodeData::Dir { dir, level },
        )
    }

    /// After `\textdir` was set to `d`: records it and, outside vertical
    /// mode, appends the direction nodes.
    pub fn text_dir_assigned(&mut self, d: 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);
        if self.mode.abs_kind() == ModeKind::Vertical {
            if same_group {
                self.text_dirs.pop();
            }
        } else {
            if same_group && let Some((_, old)) = self.text_dirs.pop() {
                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));
        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);
        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: `\begindir` if a text direction is
    /// recorded in the current box.
    pub fn par_text_dir(&mut self) {
        if !self.text_dirs.is_empty() {
            let d = self.dir_param(ad::TEXTDIR).rem_euclid(4) as i8;
            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
    }
}