retex-engine 1.3.0

The TeX typesetting engine for retex
Documentation
//! expl3's `\__tl_trim_spaces:nn` (behind `\tl_trim_spaces:n`,
//! `\tl_trim_spaces_apply:nN`, l3clist and l3keyval):
//!
//! ```tex
//! \cs_new:Npn \__tl_trim_spaces:nn #1#2
//!   {
//!     \__tl_trim_spaces_auxi:w
//!       \__tl_trim_mark: #2 \s__tl_nil
//!       \__tl_trim_mark: \__tl_trim_spaces_auxi:w
//!       \__tl_trim_mark: ~
//!       \__tl_trim_mark: \__tl_trim_spaces_auxii:w
//!     {#1}
//!   }
//! ```
//!
//! The auxiliaries remove the leading and the trailing explicit space
//! tokens (character code 32, category 10) of `#2` and finally leave
//! `#1{\__tl_trim_mark:<trimmed>}` (the mark keeps a braced group from
//! losing its braces as a delimited argument, and expands to nothing when
//! `#1` expands the group's first token). The native implementation leaves
//! the same tokens at once. It declines when `#2` contains the markers the
//! auxiliaries delimit their arguments with, and when the auxiliaries have
//! other definitions.
//!
//! Until expl3 2025-01 (TeX Live 2024) the macro took one argument, the
//! token list with `\__tl_trim_mark:` in front, and read the continuation
//! as the undelimited argument that follows (`\__tl_trim_spaces:nn
//! {\__tl_trim_mark: #1} {\__kernel_exp_not:w \exp_after:wN}`); it left
//! the same `<continuation>{\__tl_trim_mark:<trimmed>}`. The native
//! implementation reads that argument from the token lists that follow
//! (spaces skipped, one level of braces removed), and declines when it is
//! not there.

use std::cell::RefCell;

use super::Native;
use super::deps::Deps;
use crate::Engine;
use crate::token::{Token, TokenList};

/// `\__tl_trim_spaces:nn` of expl3 2025 and later.
pub(super) const TRIM_SPACES: Native = Native {
    name: "__tl_trim_spaces:nn",
    fingerprints: &[(16, 0x8450_0cf1_5dbc_0126), (16, 0xab79_0cc9_daf1_fe55)],
    executes: false,
    run: run_trim,
};

const NAMES: &[&str] = &[
    "__tl_trim_spaces_auxi:w",
    "__tl_trim_spaces_auxii:w",
    "__tl_trim_spaces_auxiii:w",
    "__tl_trim_spaces_auxiv:w",
    "__tl_trim_mark:",
    "s__tl_nil",
];

const KNOWN: &[u64] = &[0x101c_e402_7c72_cb09];

/// The auxiliaries of the one-argument version (expl3 2022 to 2024).
const NAMES_OLD: &[&str] = &[
    "__tl_trim_spaces_auxi:w",
    "__tl_trim_spaces_auxii:w",
    "__tl_trim_spaces_auxiii:w",
    "__tl_trim_spaces_auxiv:w",
    "__tl_trim_mark:",
    "s__tl_nil",
    "s__tl_stop",
];

const KNOWN_OLD: &[u64] = &[0xce28_fb3a_c46c_1751];

thread_local! {
    static DEPS: RefCell<Deps> = RefCell::new(Deps::new("trim_spaces", NAMES, KNOWN));
    static DEPS_OLD: RefCell<Deps> =
        RefCell::new(Deps::new("trim_spaces (one argument)", NAMES_OLD, KNOWN_OLD));
}

fn run_trim(e: &mut Engine, args: &[TokenList]) -> bool {
    if args.len() == 1 {
        return run_trim_old(e, &args[0]);
    }
    if !DEPS.with(|d| d.borrow_mut().check(e)) {
        return false;
    }
    let mut markers = Vec::with_capacity(NAMES.len());
    for n in NAMES {
        let Some(cs) = e.hash.lookup(n.as_bytes()) else {
            return false;
        };
        markers.push(Token::cs(cs));
    }
    let tl = &args[1];
    if tl.iter().any(|t| markers.contains(t)) {
        return false;
    }
    e.native_insert(result(&args[0], tl, markers[4]));
    true
}

/// `<continuation>{\__tl_trim_mark:<tl without its outer spaces>}`.
fn result(cont: &[Token], tl: &[Token], mark: Token) -> Vec<Token> {
    let start = tl
        .iter()
        .position(|&t| t != Token::SPACE)
        .unwrap_or(tl.len());
    let end = tl
        .iter()
        .rposition(|&t| t != Token::SPACE)
        .map_or(start, |i| i + 1);
    let mut out = Vec::with_capacity(cont.len() + end - start + 3);
    out.extend_from_slice(cont);
    out.push(Token::LEFT_BRACE);
    out.push(mark);
    out.extend_from_slice(&tl[start..end]);
    out.push(Token::RIGHT_BRACE);
    out
}

fn run_trim_old(e: &mut Engine, arg: &[Token]) -> bool {
    if !DEPS_OLD.with(|d| d.borrow_mut().check(e)) {
        return false;
    }
    let mut markers = Vec::with_capacity(NAMES_OLD.len());
    for n in NAMES_OLD {
        let Some(cs) = e.hash.lookup(n.as_bytes()) else {
            return false;
        };
        markers.push(Token::cs(cs));
    }
    let Some((&first, tl)) = arg.split_first() else {
        return false;
    };
    if first != markers[4] || tl.iter().any(|t| markers.contains(t)) {
        return false;
    }
    // The continuation: the undelimited argument that follows.
    let Some(mut f) = e.native_following_subst() else {
        return false;
    };
    let mut i = 0;
    while f.get(e, i) == Some(Token::SPACE) {
        i += 1;
    }
    let Some(t) = f.get(e, i) else {
        return false;
    };
    let cont: Vec<Token>;
    let used;
    if t.is_cs() {
        let m = e.eqtb.meaning(t.cs_id());
        if matches!(
            m.cmd,
            crate::cmd::Cmd::OuterCall | crate::cmd::Cmd::LongOuterCall
        ) {
            return false;
        }
        cont = vec![t];
        used = i + 1;
    } else if t.cmd_code() == crate::cmd::Cmd::LeftBrace as u32 {
        let mut depth = 0usize;
        let mut k = i;
        let mut body = Vec::new();
        loop {
            let Some(u) = f.get(e, k) else {
                return false;
            };
            k += 1;
            if !u.is_cs() && u.cmd_code() == crate::cmd::Cmd::LeftBrace as u32 {
                depth += 1;
                if depth == 1 {
                    continue;
                }
            } else if !u.is_cs() && u.cmd_code() == crate::cmd::Cmd::RightBrace as u32 {
                depth -= 1;
                if depth == 0 {
                    break;
                }
            } else if u.is_cs()
                && matches!(
                    e.eqtb.meaning(u.cs_id()).cmd,
                    crate::cmd::Cmd::OuterCall | crate::cmd::Cmd::LongOuterCall
                )
            {
                return false;
            }
            body.push(u);
        }
        cont = body;
        used = k;
    } else if t.cmd_code() == crate::cmd::Cmd::RightBrace as u32 {
        return false;
    } else {
        cont = vec![t];
        used = i + 1;
    }
    if f.count(e, used).is_none() {
        return false;
    }
    e.native_consume(&f, used);
    e.native_insert(result(&cont, tl, markers[4]));
    true
}