Skip to main content

ratex_parser/
macro_expander.rs

1use crate::expansion_budget::{Cost, ExpansionBudget};
2use std::collections::HashMap;
3use std::sync::{Arc, LazyLock};
4mod template;
5use template::{token_cost, Template};
6
7use ratex_lexer::token::{SourceLocation, Token};
8use ratex_lexer::Lexer;
9
10use crate::error::{ParseError, ParseResult};
11use crate::functions::FUNCTIONS;
12use crate::parse_node::Mode;
13use crate::stack_safety::{DepthBudget, MAX_INPUT_DEPTH};
14
15/// Commands that act like macros but aren't defined as a macro, function, or symbol.
16/// Used in `is_defined`.
17pub static IMPLICIT_COMMANDS: &[&str] = &["^", "_", "\\limits", "\\nolimits"];
18
19/// Handler type for function-based macros (e.g. \TextOrMath, \@ifstar).
20/// Takes the MacroExpander mutably and returns tokens to push onto the stack.
21pub type FnMacroHandler = fn(&mut MacroExpander) -> ParseResult<Vec<Token>>;
22
23/// A macro definition: string template, token list, or function.
24#[derive(Clone)]
25pub enum MacroDefinition {
26    /// Simple string expansion (e.g., `\def\foo{bar}` → "bar")
27    Text(String),
28    /// Pre-tokenized expansion with argument count
29    Tokens { tokens: Vec<Token>, num_args: usize },
30    /// Function-based macro (consumes tokens directly, returns expansion)
31    Function(FnMacroHandler),
32}
33
34impl std::fmt::Debug for MacroDefinition {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Self::Text(s) => write!(f, "Text({:?})", s),
38            Self::Tokens { tokens, num_args } => {
39                write!(f, "Tokens {{ {} tokens, {} args }}", tokens.len(), num_args)
40            }
41            Self::Function(_) => write!(f, "Function(...)"),
42        }
43    }
44}
45
46/// Immutable namespace storage. Aliases and group undo records share the body.
47struct StoredMacro {
48    definition: MacroDefinition,
49    token_cost: Option<Cost>,
50}
51impl StoredMacro {
52    fn new(definition: MacroDefinition) -> Self {
53        let token_cost = match &definition {
54            MacroDefinition::Tokens { tokens, .. } => token_cost(tokens),
55            _ => Some(Cost::default()),
56        };
57        Self {
58            definition,
59            token_cost,
60        }
61    }
62}
63
64/// The MacroExpander (or "gullet") manages macro expansion.
65///
66/// It sits between the Lexer (mouth) and the Parser (stomach).
67/// Tokens are read from the lexer, pushed onto an internal stack,
68/// and macros are expanded until only non-expandable tokens remain.
69///
70/// Modeled after KaTeX's MacroExpander.ts.
71pub struct MacroExpander<'a> {
72    pub lexer: Lexer<'a>,
73    pub mode: Mode,
74    stack: Vec<Token>,
75    macros: MacroNamespace,
76    expansion_count: usize,
77    allocation_budget: ExpansionBudget,
78    max_expand: usize,
79    depth_budget: DepthBudget,
80    expand_tokens_depth: usize,
81}
82
83// Builtins never change in place. Each parse stores only its own overrides;
84// aliases and group undo records can safely retain these shared definitions.
85static BUILTINS: LazyLock<HashMap<String, Arc<StoredMacro>>> = LazyLock::new(|| {
86    let mut me = MacroExpander {
87        lexer: Lexer::new(""),
88        mode: Mode::Math,
89        stack: Vec::new(),
90        macros: MacroNamespace::empty(),
91        expansion_count: 0,
92        allocation_budget: ExpansionBudget::default(),
93        max_expand: 1000,
94        depth_budget: DepthBudget::new(MAX_INPUT_DEPTH),
95        expand_tokens_depth: 0,
96    };
97    me.load_builtins();
98    me.macros.current
99});
100
101/// Scoped macro namespace supporting group nesting.
102struct MacroNamespace {
103    builtins: Option<&'static HashMap<String, Arc<StoredMacro>>>,
104    current: HashMap<String, Arc<StoredMacro>>,
105    group_stack: Vec<HashMap<String, Option<Arc<StoredMacro>>>>,
106}
107
108impl MacroNamespace {
109    fn new() -> Self {
110        Self {
111            builtins: Some(&BUILTINS),
112            ..Self::empty()
113        }
114    }
115    fn empty() -> Self {
116        Self {
117            builtins: None,
118            current: HashMap::new(),
119            group_stack: Vec::new(),
120        }
121    }
122
123    fn get_shared(&self, name: &str) -> Option<&Arc<StoredMacro>> {
124        self.current
125            .get(name)
126            .or_else(|| self.builtins.and_then(|b| b.get(name)))
127    }
128    fn get(&self, name: &str) -> Option<&MacroDefinition> {
129        self.get_shared(name).map(|d| &d.definition)
130    }
131
132    fn set(&mut self, name: String, def: MacroDefinition) {
133        self.set_shared(name, Arc::new(StoredMacro::new(def)));
134    }
135    fn set_shared(&mut self, name: String, def: Arc<StoredMacro>) {
136        if let Some(undo) = self.group_stack.last_mut() {
137            undo.entry(name.clone())
138                .or_insert_with(|| self.current.get(&name).cloned());
139        }
140        self.current.insert(name, def);
141    }
142
143    fn set_global(&mut self, name: String, def: MacroDefinition) {
144        self.current.insert(name, Arc::new(StoredMacro::new(def)));
145    }
146
147    fn has(&self, name: &str) -> bool {
148        self.get_shared(name).is_some()
149    }
150
151    fn begin_group(&mut self) {
152        self.group_stack.push(HashMap::new());
153    }
154
155    fn end_group(&mut self) {
156        if let Some(undo) = self.group_stack.pop() {
157            for (name, old_val) in undo {
158                match old_val {
159                    Some(def) => {
160                        self.current.insert(name, def);
161                    }
162                    None => {
163                        self.current.remove(&name);
164                    }
165                }
166            }
167        }
168    }
169
170    fn end_groups(&mut self) {
171        while !self.group_stack.is_empty() {
172            self.end_group();
173        }
174    }
175}
176
177/// Tokenize a macro expansion string into stack order (same as [`MacroDefinition::Text`] bodies).
178fn lex_string_to_stack_tokens(text: &str, budget: &ExpansionBudget) -> ParseResult<Vec<Token>> {
179    let mut lexer = Lexer::new(text);
180    let mut tokens = Vec::new();
181    loop {
182        let span = lexer.next_span();
183        if span.is_eof {
184            break;
185        }
186        budget.charge(Some(Cost::token(span.text)), "generated text lexer")?;
187        tokens.push(span.into_token());
188    }
189    tokens.reverse();
190    Ok(tokens)
191}
192
193fn dotsc_space_after(next: &str) -> bool {
194    matches!(
195        next,
196        ")" | "]"
197            | "\\rbrack"
198            | "\\}"
199            | "\\rbrace"
200            | "\\rangle"
201            | "\\rceil"
202            | "\\rfloor"
203            | "\\rgroup"
204            | "\\rmoustache"
205            | "\\right"
206            | "\\bigr"
207            | "\\biggr"
208            | "\\Bigr"
209            | "\\Biggr"
210            | "$"
211            | ";"
212            | "."
213    )
214}
215
216fn dotso_space_after(next: &str) -> bool {
217    next == "," || dotsc_space_after(next)
218}
219
220fn contextual_dots_for(next: &str) -> &'static str {
221    if next == "," {
222        "\\dotsc"
223    } else if matches!(
224        next,
225        "\\not"
226            | "+"
227            | "="
228            | "<"
229            | ">"
230            | "-"
231            | "*"
232            | ":"
233            | "\\DOTSB"
234            | "\\coprod"
235            | "\\bigvee"
236            | "\\bigwedge"
237            | "\\biguplus"
238            | "\\bigcap"
239            | "\\bigcup"
240            | "\\prod"
241            | "\\sum"
242            | "\\bigotimes"
243            | "\\bigoplus"
244            | "\\bigodot"
245            | "\\bigsqcup"
246            | "\\And"
247            | "\\longrightarrow"
248            | "\\Longrightarrow"
249            | "\\longleftarrow"
250            | "\\Longleftarrow"
251            | "\\longleftrightarrow"
252            | "\\Longleftrightarrow"
253            | "\\mapsto"
254            | "\\longmapsto"
255            | "\\hookrightarrow"
256            | "\\doteq"
257            | "\\mathbin"
258            | "\\mathrel"
259            | "\\relbar"
260            | "\\Relbar"
261            | "\\xrightarrow"
262            | "\\xleftarrow"
263    ) || next.starts_with("\\not")
264        || ratex_font::get_symbol(next, ratex_font::Mode::Math).is_some_and(|symbol| {
265            matches!(
266                symbol.group,
267                ratex_font::symbols::Group::Bin | ratex_font::symbols::Group::Rel
268            )
269        })
270    {
271        "\\dotsb"
272    } else if matches!(
273        next,
274        "\\DOTSI" | "\\int" | "\\oint" | "\\iint" | "\\iiint" | "\\iiiint" | "\\idotsint"
275    ) {
276        "\\dotsi"
277    } else if next == "\\DOTSX" {
278        "\\dotsx"
279    } else {
280        "\\dotso"
281    }
282}
283
284impl<'a> MacroExpander<'a> {
285    pub fn new(input: &'a str, mode: Mode) -> Self {
286        Self::new_with_budget(input, mode, DepthBudget::new(MAX_INPUT_DEPTH))
287    }
288
289    pub(crate) fn new_with_budget(input: &'a str, mode: Mode, depth_budget: DepthBudget) -> Self {
290        Self {
291            lexer: Lexer::new(input),
292            mode,
293            stack: Vec::new(),
294            macros: MacroNamespace::new(),
295            expansion_count: 0,
296            allocation_budget: ExpansionBudget::default(),
297            max_expand: 1000,
298            depth_budget,
299            expand_tokens_depth: 0,
300        }
301    }
302
303    fn load_builtins(&mut self) {
304        let builtins: &[(&str, &str)] = &[
305            // ── Grouping ──
306            ("\\bgroup", "{"),
307            ("\\egroup", "}"),
308
309            // ── Symbols from latex.ltx ──
310            ("\\lq", "`"),
311            ("\\rq", "'"),
312            // \lbrack and \rbrack are in the symbol table directly
313            ("\\aa", "\\r a"),
314            ("\\AA", "\\r A"),
315
316            // ── Active characters ──
317            ("~", "\\nobreakspace"),
318
319            // ── Phantoms ──
320            ("\\hphantom", "\\smash{\\phantom{#1}}"),
321
322            // ── Negated symbols ──
323            ("\\not", "\\html@mathml{\\mathrel{\\mathrlap\\@not}\\nobreak}{\\char\"338}"),
324            ("\\neq", "\\html@mathml{\\mathrel{\\not=}}{\\mathrel{\\char`≠}}"),
325            ("\\ne", "\\neq"),
326            ("\u{2260}", "\\neq"),
327            ("\\notin", "\\html@mathml{\\mathrel{{\\in}\\mathllap{/\\mskip1mu}}}{\\mathrel{\\char`∉}}"),
328            ("\u{2209}", "\\notin"),
329            ("\\notni", "\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u{220C}}}"),
330            ("\u{220C}", "\\notni"),
331            // \le and \ge are in the symbol table directly, not macros
332
333            // ── amsmath iff/implies ──
334            ("\\iff", "\\DOTSB\\;\\Longleftrightarrow\\;"),
335            ("\\implies", "\\DOTSB\\;\\Longrightarrow\\;"),
336            ("\\impliedby", "\\DOTSB\\;\\Longleftarrow\\;"),
337
338            // ── Italic Greek capitals ──
339            ("\\varGamma", "\\mathit{\\Gamma}"),
340            ("\\varDelta", "\\mathit{\\Delta}"),
341            ("\\varTheta", "\\mathit{\\Theta}"),
342            ("\\varLambda", "\\mathit{\\Lambda}"),
343            ("\\varXi", "\\mathit{\\Xi}"),
344            ("\\varPi", "\\mathit{\\Pi}"),
345            ("\\varSigma", "\\mathit{\\Sigma}"),
346            ("\\varUpsilon", "\\mathit{\\Upsilon}"),
347            ("\\varPhi", "\\mathit{\\Phi}"),
348            ("\\varPsi", "\\mathit{\\Psi}"),
349            ("\\varOmega", "\\mathit{\\Omega}"),
350
351            // ── Spacing (mode-aware via \TextOrMath) ──
352            ("\\,", "\\TextOrMath{\\kern{.1667em}}{\\mskip{3mu}}"),
353            ("\\thinspace", "\\,"),
354            ("\\>", "\\mskip{4mu}"),
355            ("\\:", "\\TextOrMath{\\kern{.2222em}}{\\mskip{4mu}}"),
356            ("\\medspace", "\\:"),
357            ("\\;", "\\TextOrMath{\\kern{.2777em}}{\\mskip{5mu}}"),
358            ("\\thickspace", "\\;"),
359            ("\\!", "\\TextOrMath{\\kern{-.1667em}}{\\mskip{-3mu}}"),
360            ("\\negthinspace", "\\!"),
361            ("\\negmedspace", "\\TextOrMath{\\kern{-.2222em}}{\\mskip{-4mu}}"),
362            ("\\negthickspace", "\\TextOrMath{\\kern{-.2777em}}{\\mskip{-5mu}}"),
363            ("\\enspace", "\\kern.5em "),
364            ("\\enskip", "\\hskip.5em\\relax"),
365            ("\\quad", "\\hskip1em\\relax"),
366            ("\\qquad", "\\hskip2em\\relax"),
367
368            // ── Newline ──
369            ("\\newline", "\\\\\\relax"),
370
371            // ── hspace ──
372            ("\\@hspace", "\\hskip #1\\relax"),
373            ("\\@hspacer", "\\rule{0pt}{0pt}\\hskip #1\\relax"),
374
375            // ── llap / rlap / clap ──
376            ("\\llap", "\\mathllap{\\textrm{#1}}"),
377            ("\\rlap", "\\mathrlap{\\textrm{#1}}"),
378            ("\\clap", "\\mathclap{\\textrm{#1}}"),
379
380            // ── Logos ──
381            ("\\TeX", "\\textrm{\\html@mathml{T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX}{TeX}}"),
382            ("\\LaTeX", "\\textrm{\\html@mathml{L\\kern-.36em\\raisebox{0.21em}{\\scriptstyle A}\\kern-.15em\\TeX}{LaTeX}}"),
383            ("\\KaTeX", "\\textrm{\\html@mathml{K\\kern-.17em\\raisebox{0.21em}{\\scriptstyle A}\\kern-.15em\\TeX}{KaTeX}}"),
384
385            // ── imath / jmath ──
386            ("\\imath", "\\html@mathml{\\@imath}{\u{0131}}"),
387            ("\\jmath", "\\html@mathml{\\@jmath}{\u{0237}}"),
388
389            // ── minuso ──
390            ("\\minuso", "\\mathbin{\\html@mathml{{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}{\\char`\u{29B5}}}"),
391            ("\\clap", "\\mathclap{\\textrm{#1}}"),
392
393            // ── mathstrut / underbar ──
394            ("\\mathstrut", "\\vphantom{(}"),
395            ("\\underbar", "\\underline{\\text{#1}}"),
396
397            // ── Bbbk ──
398            ("\\Bbbk", "\\Bbb{k}"),
399
400            // ── substack ──
401            ("\\substack", "\\begin{subarray}{c}#1\\end{subarray}"),
402
403            // ── boxed ──
404            ("\\boxed", "\\fbox{$\\displaystyle{#1}$}"),
405
406            // ── colon ──
407            ("\\colon", "\\nobreak\\mskip2mu\\mathpunct{}\\mathchoice{\\mkern-3mu}{\\mkern-3mu}{}{}{:}\\mskip6mu\\relax"),
408
409            // ── dots ──
410            ("\\cdots", "\\@cdots"),
411            ("\\dotsb", "\\cdots"),
412            ("\\dotsm", "\\cdots"),
413            ("\\dotsi", "\\!\\cdots"),
414            ("\\dotsx", "\\ldots\\,"),
415            ("\\DOTSI", "\\relax"),
416            ("\\DOTSB", "\\relax"),
417            ("\\DOTSX", "\\relax"),
418
419            // ── negated relations / corners (→ symbol table \@xxx) ──
420            ("\\gvertneqq", "\\@gvertneqq"),
421            ("\\lvertneqq", "\\@lvertneqq"),
422            ("\\ngeqq", "\\@ngeqq"),
423            ("\\ngeqslant", "\\@ngeqslant"),
424            ("\\nleqq", "\\@nleqq"),
425            ("\\nleqslant", "\\@nleqslant"),
426            ("\\nshortmid", "\\@nshortmid"),
427            ("\\nshortparallel", "\\@nshortparallel"),
428            ("\\nsubseteqq", "\\@nsubseteqq"),
429            ("\\nsupseteqq", "\\@nsupseteqq"),
430            ("\\ulcorner", "\\@ulcorner"),
431            ("\\urcorner", "\\@urcorner"),
432            ("\\llcorner", "\\@llcorner"),
433            ("\\lrcorner", "\\@lrcorner"),
434            ("\\varsubsetneq", "\\@varsubsetneq"),
435            ("\\varsubsetneqq", "\\@varsubsetneqq"),
436            ("\\varsupsetneq", "\\@varsupsetneq"),
437            ("\\varsupsetneqq", "\\@varsupsetneqq"),
438
439            // ── delimiters / text (compose from existing) ──
440            // Match KaTeX `macros.ts` html@mathml first branch (STIX-style white tortoise brackets).
441            ("\\lBrace", "\\mathopen{\\{\\mkern-3.2mu[}"),
442            ("\\rBrace", "\\mathclose{]\\mkern-3.2mu\\}}"),
443            ("\\llbracket", "\\mathopen{[\\mkern-3.2mu[}"),
444            ("\\rrbracket", "\\mathclose{]\\mkern-3.2mu]}"),
445            ("\\copyright", "\\textcircled{c}"),
446            ("\\textregistered", "\\textcircled{\\scriptsize R}"),
447            ("\\textcopyright", "\\html@mathml{\\textcircled{c}}{\\char`©}"),
448
449            // ── tmspace (TeX primitive used by \, \: \; \!) ──
450            // KaTeX `\tmspace` takes 3 args: sign (+/-), mu-glue, em-kern.
451            ("\\tmspace", "\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"),
452
453            // ── Unicode shorthand aliases ──
454            // Mirrors the `defineMacro("\u{...}", "...")` block in
455            // KaTeX `src/macros.ts`, so users can paste raw glyphs and
456            // get the same expansion as the named macro.
457            // Stacked relations (\u2258..\u225F).
458            ("\u{2258}", "\\mathrel{\u{E258}}"),
459            ("\u{2259}", "\\stackrel{\\tiny\\wedge}{=}"),
460            ("\u{225A}", "\\stackrel{\\tiny\\vee}{=}"),
461            ("\u{225B}", "\\stackrel{\\scriptsize\\star}{=}"),
462            ("\u{225D}", "\\stackrel{\\tiny\\mathrm{def}}{=}"),
463            ("\u{225E}", "\\mathrel{\u{E25E}}"),
464            ("\u{225F}", "\\stackrel{\\tiny?}{=}"),
465            // Misc relations / corners / punctuation.
466            ("\u{27C2}", "\\perp"),
467            ("\u{203C}", "\\mathclose{!\\mkern-0.8mu!}"),
468            ("\u{231C}", "\\ulcorner"),
469            ("\u{231D}", "\\urcorner"),
470            ("\u{231E}", "\\llcorner"),
471            ("\u{231F}", "\\lrcorner"),
472            ("\u{00A9}", "\\copyright"),
473            ("\u{00AE}", "\\textregistered"),
474            // Mathtools colon variants (∷ ∹ ≔ ≕ ⩴).
475            ("\u{2237}", "\\dblcolon"),
476            ("\u{2239}", "\\eqcolon"),
477            ("\u{2254}", "\\coloneqq"),
478            ("\u{2255}", "\\eqqcolon"),
479            ("\u{2A74}", "\\Coloneqq"),
480            // stmaryrd brackets.
481            ("\u{27E6}", "\\llbracket"),
482            ("\u{27E7}", "\\rrbracket"),
483            ("\u{2983}", "\\lBrace"),
484            ("\u{2984}", "\\rBrace"),
485            // Plimsoll.
486            ("\u{29B5}", "\\minuso"),
487
488            // ── vdots ──
489            ("\\vdots", "{\\varvdots\\rule{0pt}{15pt}}"),
490            ("\u{22ee}", "\\vdots"),
491
492            // ── bmod / pod / pmod / mod ──
493            ("\\bmod", "\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}\\mathbin{\\rm mod}\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}"),
494            ("\\pod", "\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)"),
495            ("\\pmod", "\\pod{{\\rm mod}\\mkern6mu#1}"),
496            ("\\mod", "\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}{\\rm mod}\\,\\,#1"),
497
498            // ── limsup / liminf / etc ──
499            ("\\limsup", "\\DOTSB\\operatorname*{lim\\,sup}"),
500            ("\\liminf", "\\DOTSB\\operatorname*{lim\\,inf}"),
501            ("\\injlim", "\\DOTSB\\operatorname*{inj\\,lim}"),
502            ("\\projlim", "\\DOTSB\\operatorname*{proj\\,lim}"),
503            ("\\varlimsup", "\\DOTSB\\operatorname*{\\overline{\\mathrm{lim}}}"),
504            ("\\varliminf", "\\DOTSB\\operatorname*{\\underline{\\mathrm{lim}}}"),
505            ("\\varinjlim", "\\DOTSB\\operatorname*{\\underrightarrow{\\mathrm{lim}}}"),
506            ("\\varprojlim", "\\DOTSB\\operatorname*{\\underleftarrow{\\mathrm{lim}}}"),
507
508            // ── statmath ──
509            ("\\argmin", "\\DOTSB\\operatorname*{arg\\,min}"),
510            ("\\argmax", "\\DOTSB\\operatorname*{arg\\,max}"),
511            ("\\plim", "\\DOTSB\\mathop{\\operatorname{plim}}\\limits"),
512
513            // ── mathtools colon variants ──
514            ("\\ordinarycolon", ":"),
515            ("\\vcentcolon", "\\mathrel{\\mathop\\ordinarycolon}"),
516            ("\\dblcolon", "\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}{\\mathop{\\char\"2237}}"),
517            ("\\coloneqq", "\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char\"2254}}"),
518            ("\\Coloneqq", "\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char\"2237\\char\"3d}}"),
519            ("\\coloneq", "\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char\"3a\\char\"2212}}"),
520            ("\\Coloneq", "\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char\"2237\\char\"2212}}"),
521            ("\\eqqcolon", "\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char\"2255}}"),
522            ("\\Eqqcolon", "\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char\"3d\\char\"2237}}"),
523            ("\\eqcolon", "\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char\"2239}}"),
524            ("\\Eqcolon", "\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char\"2212\\char\"2237}}"),
525            ("\\colonapprox", "\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char\"3a\\char\"2248}}"),
526            ("\\Colonapprox", "\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char\"2237\\char\"2248}}"),
527            ("\\colonsim", "\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char\"3a\\char\"223c}}"),
528            ("\\Colonsim", "\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char\"2237\\char\"223c}}"),
529
530            // ── colonequals alternate names ──
531            ("\\ratio", "\\vcentcolon"),
532            ("\\coloncolon", "\\dblcolon"),
533            ("\\colonequals", "\\coloneqq"),
534            ("\\coloncolonequals", "\\Coloneqq"),
535            ("\\equalscolon", "\\eqqcolon"),
536            ("\\equalscoloncolon", "\\Eqqcolon"),
537            ("\\colonminus", "\\coloneq"),
538            ("\\coloncolonminus", "\\Coloneq"),
539            ("\\minuscolon", "\\eqcolon"),
540            ("\\minuscoloncolon", "\\Eqcolon"),
541            ("\\coloncolonapprox", "\\Colonapprox"),
542            ("\\coloncolonsim", "\\Colonsim"),
543            ("\\simcolon", "\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),
544            ("\\simcoloncolon", "\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}"),
545            ("\\approxcolon", "\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),
546            ("\\approxcoloncolon", "\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}"),
547
548            // ── braket (string-based) ──
549            ("\\bra", "\\mathinner{\\langle{#1}|}"),
550            ("\\ket", "\\mathinner{|{#1}\\rangle}"),
551            ("\\braket", "\\mathinner{\\langle{#1}\\rangle}"),
552            ("\\Braket", "\\bra@ket{\\left\\langle}{\\,\\middle\\vert\\,}{\\,\\middle\\vert\\,}{\\right\\rangle}"),
553            ("\\Bra", "\\left\\langle#1\\right|"),
554            ("\\Ket", "\\left|#1\\right\\rangle"),
555
556            // ── texvc (MediaWiki) ──
557            ("\\darr", "\\downarrow"),
558            ("\\dArr", "\\Downarrow"),
559            ("\\Darr", "\\Downarrow"),
560            ("\\lang", "\\langle"),
561            ("\\rang", "\\rangle"),
562            ("\\uarr", "\\uparrow"),
563            ("\\uArr", "\\Uparrow"),
564            ("\\Uarr", "\\Uparrow"),
565            ("\\N", "\\mathbb{N}"),
566            ("\\R", "\\mathbb{R}"),
567            ("\\Z", "\\mathbb{Z}"),
568            ("\\alef", "\\aleph"),
569            ("\\alefsym", "\\aleph"),
570            ("\\Alpha", "\\mathrm{A}"),
571            ("\\Beta", "\\mathrm{B}"),
572            ("\\bull", "\\bullet"),
573            ("\\Chi", "\\mathrm{X}"),
574            ("\\clubs", "\\clubsuit"),
575            ("\\cnums", "\\mathbb{C}"),
576            ("\\Complex", "\\mathbb{C}"),
577            ("\\Dagger", "\\ddagger"),
578            ("\\diamonds", "\\diamondsuit"),
579            ("\\empty", "\\emptyset"),
580            ("\\Epsilon", "\\mathrm{E}"),
581            ("\\Eta", "\\mathrm{H}"),
582            ("\\exist", "\\exists"),
583            ("\\harr", "\\leftrightarrow"),
584            ("\\hArr", "\\Leftrightarrow"),
585            ("\\Harr", "\\Leftrightarrow"),
586            ("\\hearts", "\\heartsuit"),
587            ("\\image", "\\Im"),
588            ("\\infin", "\\infty"),
589            ("\\Iota", "\\mathrm{I}"),
590            ("\\isin", "\\in"),
591            ("\\Kappa", "\\mathrm{K}"),
592            ("\\larr", "\\leftarrow"),
593            ("\\lArr", "\\Leftarrow"),
594            ("\\Larr", "\\Leftarrow"),
595            ("\\lrarr", "\\leftrightarrow"),
596            ("\\lrArr", "\\Leftrightarrow"),
597            ("\\Lrarr", "\\Leftrightarrow"),
598            ("\\Mu", "\\mathrm{M}"),
599            ("\\natnums", "\\mathbb{N}"),
600            ("\\Nu", "\\mathrm{N}"),
601            ("\\Omicron", "\\mathrm{O}"),
602            ("\\plusmn", "\\pm"),
603            ("\\rarr", "\\rightarrow"),
604            ("\\rArr", "\\Rightarrow"),
605            ("\\Rarr", "\\Rightarrow"),
606            ("\\real", "\\Re"),
607            ("\\reals", "\\mathbb{R}"),
608            ("\\Reals", "\\mathbb{R}"),
609            ("\\Rho", "\\mathrm{P}"),
610            ("\\sdot", "\\cdot"),
611            ("\\sect", "\\S"),
612            ("\\spades", "\\spadesuit"),
613            ("\\sub", "\\subset"),
614            ("\\sube", "\\subseteq"),
615            ("\\supe", "\\supseteq"),
616            ("\\Tau", "\\mathrm{T}"),
617            ("\\thetasym", "\\vartheta"),
618            ("\\weierp", "\\wp"),
619            ("\\Zeta", "\\mathrm{Z}"),
620
621            // ── Khan Academy color aliases ──
622            ("\\blue", "\\textcolor{##6495ed}{#1}"),
623            ("\\orange", "\\textcolor{##ffa500}{#1}"),
624            ("\\pink", "\\textcolor{##ff00af}{#1}"),
625            ("\\red", "\\textcolor{##df0030}{#1}"),
626            ("\\green", "\\textcolor{##28ae7b}{#1}"),
627            ("\\gray", "\\textcolor{gray}{#1}"),
628            ("\\purple", "\\textcolor{##9d38bd}{#1}"),
629
630            // ── Unicode script letters ──
631            ("\u{212C}", "\\mathscr{B}"),
632            ("\u{2130}", "\\mathscr{E}"),
633            ("\u{2131}", "\\mathscr{F}"),
634            ("\u{210B}", "\\mathscr{H}"),
635            ("\u{2110}", "\\mathscr{I}"),
636            ("\u{2112}", "\\mathscr{L}"),
637            ("\u{2133}", "\\mathscr{M}"),
638            ("\u{211B}", "\\mathscr{R}"),
639            ("\u{212D}", "\\mathfrak{C}"),
640            ("\u{210C}", "\\mathfrak{H}"),
641            ("\u{2128}", "\\mathfrak{Z}"),
642
643            // ── notni ──
644            ("\\notni", "\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u{220C}}}"),
645
646            // ── actuarialangle ──
647            ("\\angln", "{\\angl n}"),
648
649            // ── set/Set (braket notation, simplified) ──
650            ("\\set", "\\bra@set{\\{\\,}{\\mid}{}{\\,\\}}"),
651            ("\\Set", "\\bra@set{\\left\\{\\:}{\\;\\middle\\vert\\;}{\\;\\middle\\Vert\\;}{\\:\\right\\}}"),
652
653            // ── KaTeX mhchem (\\tripledash for \\bond ~ forms) ──
654            (
655                "\\tripledash",
656                "{\\vphantom{-}\\raisebox{2.56mu}{$\\mkern2mu\\tiny\\text{-}\\mkern1mu\\text{-}\\mkern1mu\\text{-}\\mkern2mu$}}",
657            ),
658        ];
659
660        for &(name, expansion) in builtins {
661            self.macros.set(
662                name.to_string(),
663                MacroDefinition::Text(expansion.to_string()),
664            );
665        }
666
667        self.load_function_macros();
668    }
669
670    fn load_function_macros(&mut self) {
671        // \noexpand: mark the next token as non-expandable (only if expandable)
672        self.macros.set(
673            "\\noexpand".to_string(),
674            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
675                let mut tok = me.pop_token();
676                if me.is_expandable(&tok.text) {
677                    tok.noexpand = true;
678                    tok.treat_as_relax = true;
679                }
680                Ok(vec![tok])
681            }),
682        );
683
684        // \@firstoftwo{A}{B} → A
685        // NOTE: consume_args returns tokens in stack order (reversed).
686        // We return them as-is since expand_once does stack.extend(tokens).
687        self.macros.set(
688            "\\@firstoftwo".to_string(),
689            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
690                let args = me.consume_args(2)?;
691                Ok(args.into_iter().next().unwrap())
692            }),
693        );
694
695        // \@secondoftwo{A}{B} → B
696        self.macros.set(
697            "\\@secondoftwo".to_string(),
698            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
699                let args = me.consume_args(2)?;
700                Ok(args.into_iter().nth(1).unwrap())
701            }),
702        );
703
704        // \@ifnextchar{C}{T}{F}: peek; if next non-space == C then T else F
705        self.macros.set(
706            "\\@ifnextchar".to_string(),
707            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
708                let args = me.consume_args(3)?;
709                me.consume_spaces();
710                let next = me.future().text.clone();
711                let char_arg = &args[0];
712                // char_arg is reversed; the "first" char in original order is the last element
713                let char_text = char_arg.first().map_or("", |t| t.text.as_str());
714                if next == char_text {
715                    Ok(args.into_iter().nth(1).unwrap())
716                } else {
717                    Ok(args.into_iter().nth(2).unwrap())
718                }
719            }),
720        );
721
722        // \@ifstar{with-star}{without-star}: if next is * → consume * and use first arg
723        self.macros.set(
724            "\\@ifstar".to_string(),
725            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
726                let args = me.consume_args(2)?;
727                let next = me.future().text.clone();
728                if next == "*" {
729                    me.pop_token();
730                    Ok(args.into_iter().nth(0).unwrap())
731                } else {
732                    Ok(args.into_iter().nth(1).unwrap())
733                }
734            }),
735        );
736
737        // \TextOrMath{text-branch}{math-branch}: choose based on mode
738        self.macros.set(
739            "\\TextOrMath".to_string(),
740            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
741                let args = me.consume_args(2)?;
742                if me.mode == Mode::Text {
743                    Ok(args.into_iter().nth(0).unwrap())
744                } else {
745                    Ok(args.into_iter().nth(1).unwrap())
746                }
747            }),
748        );
749
750        // AMSMath's contextual \dots selection. It expands the following token
751        // once before classifying it, matching KaTeX's expandAfterFuture().
752        self.macros.set(
753            "\\dots".to_string(),
754            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
755                let _guard = me
756                    .depth_budget
757                    .enter()
758                    .map_err(|_| ParseError::recursion_limit_exceeded())?;
759                me.expand_once(false)?;
760                let next = me.future().text.clone();
761                lex_string_to_stack_tokens(contextual_dots_for(&next), &me.allocation_budget)
762            }),
763        );
764
765        // \dotso and \cdots add a thin space before punctuation/right
766        // delimiters. \dotsc intentionally excludes a following comma.
767        self.macros.set(
768            "\\dotso".to_string(),
769            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
770                let next = me.future().text.clone();
771                let text = if dotso_space_after(&next) {
772                    "\\ldots\\,"
773                } else {
774                    "\\ldots"
775                };
776                lex_string_to_stack_tokens(text, &me.allocation_budget)
777            }),
778        );
779
780        self.macros.set(
781            "\\dotsc".to_string(),
782            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
783                let next = me.future().text.clone();
784                let text = if dotsc_space_after(&next) {
785                    "\\ldots\\,"
786                } else {
787                    "\\ldots"
788                };
789                lex_string_to_stack_tokens(text, &me.allocation_budget)
790            }),
791        );
792
793        self.macros.set(
794            "\\cdots".to_string(),
795            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
796                let next = me.future().text.clone();
797                let text = if dotso_space_after(&next) {
798                    "\\@cdots\\,"
799                } else {
800                    "\\@cdots"
801                };
802                lex_string_to_stack_tokens(text, &me.allocation_budget)
803            }),
804        );
805
806        // \html@mathml is registered as a function in htmlmathml.rs
807
808        // \newcommand{\name}[nargs]{body}
809        self.macros.set(
810            "\\newcommand".to_string(),
811            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
812                handle_newcommand(me, false, true)
813            }),
814        );
815
816        // \renewcommand{\name}[nargs]{body}
817        self.macros.set(
818            "\\renewcommand".to_string(),
819            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
820                handle_newcommand(me, true, false)
821            }),
822        );
823
824        // \providecommand{\name}[nargs]{body}
825        self.macros.set(
826            "\\providecommand".to_string(),
827            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
828                handle_newcommand(me, true, true)
829            }),
830        );
831
832        // \char: parse decimal/octal/hex/backtick number → \@char{N}
833        self.macros.set(
834            "\\char".to_string(),
835            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
836                let mut tok = me.pop_token();
837                let mut number: i64;
838                let base: Option<u32>;
839
840                if tok.text == "'" {
841                    base = Some(8);
842                    tok = me.pop_token();
843                } else if tok.text == "\"" {
844                    base = Some(16);
845                    tok = me.pop_token();
846                } else if tok.text == "`" {
847                    tok = me.pop_token();
848                    if tok.text.starts_with('\\') {
849                        number = tok.text.chars().nth(1).map_or(0, |c| c as i64);
850                    } else {
851                        number = tok.text.chars().next().map_or(0, |c| c as i64);
852                    }
853                    let text = me
854                        .allocation_budget
855                        .format(format_args!("\\@char{{{number}}}"))?;
856                    let mut tokens = lex_string_to_stack_tokens(&text, &me.allocation_budget)?;
857                    for t in &mut tokens {
858                        t.loc = tok.loc.clone();
859                    }
860                    return Ok(tokens);
861                } else {
862                    base = Some(10);
863                }
864
865                if let Some(b) = base {
866                    number = i64::from_str_radix(&tok.text, b).unwrap_or(0);
867                    loop {
868                        let next = me.future().text.clone();
869                        if let Ok(d) = i64::from_str_radix(&next, b) {
870                            me.pop_token();
871                            number = number
872                                .checked_mul(i64::from(b))
873                                .and_then(|n| n.checked_add(d))
874                                .ok_or_else(|| ParseError::msg("\\char number out of range"))?;
875                        } else {
876                            break;
877                        }
878                    }
879                } else {
880                    number = 0;
881                }
882
883                let text = me
884                    .allocation_budget
885                    .format(format_args!("\\@char{{{number}}}"))?;
886                let mut tokens = lex_string_to_stack_tokens(&text, &me.allocation_budget)?;
887                for t in &mut tokens {
888                    t.loc = tok.loc.clone();
889                }
890                Ok(tokens)
891            }),
892        );
893
894        // \operatorname: \@ifstar\operatornamewithlimits\operatorname@
895        self.macros.set(
896            "\\operatorname".to_string(),
897            MacroDefinition::Text("\\@ifstar\\operatornamewithlimits\\operatorname@".to_string()),
898        );
899
900        // \message{...}: consume argument and discard (no-op)
901        self.macros.set(
902            "\\message".to_string(),
903            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
904                let _args = me.consume_args(1)?;
905                Ok(vec![])
906            }),
907        );
908
909        // \errmessage{...}: consume argument and discard (no-op)
910        self.macros.set(
911            "\\errmessage".to_string(),
912            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
913                let _args = me.consume_args(1)?;
914                Ok(vec![])
915            }),
916        );
917
918        // KaTeX HTML extensions: no-op (only render content, no HTML attributes).
919        // Not standard LaTeX; for compatibility we parse and expand to second argument only.
920        // \htmlStyle is registered as a real function so the renderer can honor basic CSS.
921        for name in &["\\htmlClass", "\\htmlData", "\\htmlId"] {
922            let name = (*name).to_string();
923            self.macros.set(
924                name.clone(),
925                MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
926                    let args = me.consume_args(2)?;
927                    let content = args
928                        .into_iter()
929                        .nth(1)
930                        .unwrap()
931                        .into_iter()
932                        .rev()
933                        .collect::<Vec<_>>();
934                    Ok(content)
935                }),
936            );
937        }
938
939        self.macros.set(
940            "\\bra@ket".to_string(),
941            MacroDefinition::Function(|me| me.expand_braket(false)),
942        );
943        self.macros.set(
944            "\\bra@set".to_string(),
945            MacroDefinition::Function(|me| me.expand_braket(true)),
946        );
947
948        // \\ce / \\pu: KaTeX mhchem 3.3.0 (Rust port in `crate::mhchem`)
949        self.macros.set(
950            "\\ce".to_string(),
951            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
952                let args = me.consume_args(1)?;
953                let s = crate::mhchem::arg_tokens_with_resources(&args[0], &me.allocation_budget)
954                    .map_err(|e| ParseError::msg(e.to_string()))?;
955                let tex = crate::mhchem::chem_parse_str_with_resources(
956                    &s,
957                    "ce",
958                    me.depth_budget.clone(),
959                    me.allocation_budget.clone(),
960                )
961                .map_err(|e| ParseError::msg(format!("\\ce: {e}")))?;
962                lex_string_to_stack_tokens(&tex, &me.allocation_budget)
963            }),
964        );
965        self.macros.set(
966            "\\pu".to_string(),
967            MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
968                let args = me.consume_args(1)?;
969                let s = crate::mhchem::arg_tokens_with_resources(&args[0], &me.allocation_budget)
970                    .map_err(|e| ParseError::msg(e.to_string()))?;
971                let tex = crate::mhchem::chem_parse_str_with_resources(
972                    &s,
973                    "pu",
974                    me.depth_budget.clone(),
975                    me.allocation_budget.clone(),
976                )
977                .map_err(|e| ParseError::msg(format!("\\pu: {e}")))?;
978                lex_string_to_stack_tokens(&tex, &me.allocation_budget)
979            }),
980        );
981    }
982
983    pub fn set_macro(&mut self, name: String, def: MacroDefinition) {
984        self.macros.set(name, def);
985    }
986
987    pub fn set_macro_global(&mut self, name: String, def: MacroDefinition) {
988        self.macros.set_global(name, def);
989    }
990
991    pub fn set_text_macro(&mut self, name: &str, text: &str) {
992        self.macros
993            .set(name.to_string(), MacroDefinition::Text(text.to_string()));
994    }
995
996    pub fn get_macro(&self, name: &str) -> Option<&MacroDefinition> {
997        self.macros.get(name)
998    }
999
1000    /// Expand a list of tokens fully (for \edef/\xdef).
1001    pub fn expand_tokens(&mut self, tokens: Vec<Token>) -> ParseResult<Vec<Token>> {
1002        let nested = self.expand_tokens_depth > 0;
1003        let _guard = if nested {
1004            Some(
1005                self.depth_budget
1006                    .enter()
1007                    .map_err(|_| ParseError::recursion_limit_exceeded())?,
1008            )
1009        } else {
1010            None
1011        };
1012
1013        self.expand_tokens_depth += 1;
1014        let saved_stack = std::mem::take(&mut self.stack);
1015        self.stack = tokens;
1016
1017        let result: ParseResult<Vec<Token>> = (|| {
1018            let mut result = Vec::new();
1019            loop {
1020                if self.stack.is_empty() {
1021                    break;
1022                }
1023                let expanded = self.expand_once(false)?;
1024                if !expanded {
1025                    if let Some(tok) = self.stack.pop() {
1026                        if tok.is_eof() {
1027                            break;
1028                        }
1029                        result.push(tok);
1030                    }
1031                }
1032            }
1033            result.reverse();
1034            Ok(result)
1035        })();
1036
1037        self.stack = saved_stack;
1038        self.expand_tokens_depth -= 1;
1039        result
1040    }
1041
1042    pub fn switch_mode(&mut self, new_mode: Mode) {
1043        self.mode = new_mode;
1044    }
1045
1046    pub fn begin_group(&mut self) {
1047        self.macros.begin_group();
1048    }
1049
1050    pub fn end_group(&mut self) {
1051        self.macros.end_group();
1052    }
1053
1054    pub fn end_groups(&mut self) {
1055        self.macros.end_groups();
1056    }
1057
1058    /// Returns the topmost token on the stack, without expanding it.
1059    pub fn future(&mut self) -> &Token {
1060        if self.stack.is_empty() {
1061            let tok = self.lexer.lex();
1062            self.stack.push(tok);
1063        }
1064        self.stack.last().unwrap()
1065    }
1066
1067    /// Remove and return the next unexpanded token.
1068    pub fn pop_token(&mut self) -> Token {
1069        self.future();
1070        self.stack.pop().unwrap()
1071    }
1072
1073    /// Modify the top token's text on the stack (for \global prefix handling).
1074    pub fn set_top_text(&mut self, text: String) {
1075        self.future();
1076        if let Some(tok) = self.stack.last_mut() {
1077            tok.text = text;
1078        }
1079    }
1080
1081    /// Push a token onto the stack.
1082    pub fn push_token(&mut self, token: Token) {
1083        self.stack.push(token);
1084    }
1085
1086    /// Push multiple tokens onto the stack.
1087    pub fn push_tokens(&mut self, tokens: Vec<Token>) {
1088        self.stack.extend(tokens);
1089    }
1090
1091    /// Consume all following space tokens, without expansion.
1092    pub fn consume_spaces(&mut self) {
1093        loop {
1094            let is_space = self.future().text == " ";
1095            if is_space {
1096                self.stack.pop();
1097            } else {
1098                break;
1099            }
1100        }
1101    }
1102
1103    /// Expand the next token once, charging only newly constructed output.
1104    fn expand_once(&mut self, expandable_only: bool) -> ParseResult<bool> {
1105        let top_token = self.pop_token();
1106        let name = &top_token.text;
1107        if top_token.noexpand {
1108            self.push_token(top_token);
1109            return Ok(false);
1110        }
1111        let def = self.macros.get_shared(name).cloned();
1112        let Some(def) = def else {
1113            if expandable_only && name.starts_with('\\') && !self.is_defined(name) {
1114                return Err(ParseError::new(
1115                    format!("Undefined control sequence: {}", name),
1116                    Some(&top_token),
1117                ));
1118            }
1119            self.push_token(top_token);
1120            return Ok(false);
1121        };
1122        // Function macros have historically bypassed active-character checks.
1123        if !matches!(&def.definition, MacroDefinition::Function(_)) && name.len() == 1 {
1124            let code = self.lexer_catcode(name.chars().next().unwrap());
1125            if code != 0 && code != 13 {
1126                self.push_token(top_token);
1127                return Ok(false);
1128            }
1129        }
1130        self.count_expansion(1)?;
1131        let tokens = match &def.definition {
1132            MacroDefinition::Function(handler) => handler(self)?,
1133            MacroDefinition::Text(text) => {
1134                let template = Template::Text(text);
1135                let args = self.consume_args(template.num_args())?;
1136                template.expand(args, &self.allocation_budget)?
1137            }
1138            MacroDefinition::Tokens { tokens, num_args } => {
1139                if *num_args == 0 {
1140                    self.allocation_budget
1141                        .charge(def.token_cost, "token macro body")?;
1142                    tokens.clone()
1143                } else {
1144                    let args = self.consume_args(*num_args)?;
1145                    Template::Tokens(tokens).expand(args, &self.allocation_budget)?
1146                }
1147            }
1148        };
1149        self.stack.extend(tokens);
1150        Ok(true)
1151    }
1152
1153    fn expand_braket(&mut self, first_only: bool) -> ParseResult<Vec<Token>> {
1154        let mut args = self.consume_args(4)?.into_iter();
1155        let left = args.next().unwrap();
1156        let middle = args.next().unwrap();
1157        let double = args.next().unwrap();
1158        let right = args.next().unwrap();
1159        let content = self.consume_arg(None)?.tokens;
1160        let mut iter = content.into_iter().rev().peekable();
1161        let mut out = Vec::new();
1162        let mut depth = 0i32;
1163        let mut replaced = false;
1164        while let Some(t) = iter.next() {
1165            if t.text == "{" {
1166                depth += 1;
1167            } else if t.text == "}" {
1168                depth -= 1;
1169            } else if depth == 0 && t.text == "|" && !(first_only && replaced) {
1170                let replacement =
1171                    if !double.is_empty() && iter.peek().is_some_and(|t| t.text == "|") {
1172                        iter.next();
1173                        &double
1174                    } else {
1175                        &middle
1176                    };
1177                self.allocation_budget
1178                    .charge(token_cost(replacement), "braket replacement")?;
1179                out.extend(replacement.iter().rev().cloned());
1180                replaced = true;
1181                continue;
1182            }
1183            out.push(t);
1184        }
1185        let mut to_expand = right;
1186        to_expand.extend(out.into_iter().rev());
1187        to_expand.extend(left);
1188        self.begin_group();
1189        let result = self.expand_tokens(to_expand);
1190        self.end_group();
1191        result
1192    }
1193
1194    /// Alias the immutable definition without copying a macro body.
1195    pub(crate) fn alias_macro(&mut self, name: &str, source: &str, global: bool) -> bool {
1196        let Some(def) = self.macros.get_shared(source).cloned() else {
1197            return false;
1198        };
1199        if global {
1200            self.macros.current.insert(name.to_string(), def);
1201        } else {
1202            self.macros.set_shared(name.to_string(), def);
1203        }
1204        true
1205    }
1206
1207    fn lexer_catcode(&self, ch: char) -> u8 {
1208        self.lexer.get_catcode(ch)
1209    }
1210
1211    fn count_expansion(&mut self, amount: usize) -> ParseResult<()> {
1212        self.expansion_count = self
1213            .expansion_count
1214            .checked_add(amount)
1215            .ok_or_else(|| ParseError::msg("Too many expansions"))?;
1216        self.allocation_budget.calls(self.expansion_count);
1217        if self.expansion_count > self.max_expand {
1218            Err(ParseError::msg(
1219                "Too many expansions: infinite loop or need to increase maxExpand setting",
1220            ))
1221        } else {
1222            Ok(())
1223        }
1224    }
1225
1226    /// Recursively expand the next token until a non-expandable token is found.
1227    pub fn expand_next_token(&mut self) -> ParseResult<Token> {
1228        loop {
1229            let expanded = self.expand_once(false)?;
1230            if !expanded {
1231                let mut token = self.stack.pop().unwrap();
1232                if token.treat_as_relax {
1233                    token.text = self.allocation_budget.text("\\relax")?;
1234                }
1235                return Ok(token);
1236            }
1237        }
1238    }
1239
1240    /// Consume a single argument from the token stream.
1241    pub fn consume_arg(&mut self, delims: Option<&[&str]>) -> ParseResult<ConsumedArg> {
1242        let is_delimited = delims.is_some_and(|d| !d.is_empty());
1243        if !is_delimited {
1244            self.consume_spaces();
1245        }
1246
1247        let start = self.future().clone();
1248        let mut tokens = Vec::new();
1249        let mut depth: i32 = 0;
1250        let end_tok;
1251
1252        loop {
1253            let tok = self.pop_token();
1254
1255            if tok.text == "{" {
1256                depth += 1;
1257            } else if tok.text == "}" {
1258                depth -= 1;
1259                if depth == -1 {
1260                    return Err(ParseError::new("Extra }", Some(&tok)));
1261                }
1262            } else if tok.is_eof() {
1263                return Err(ParseError::new(
1264                    "Unexpected end of input in a macro argument",
1265                    Some(&tok),
1266                ));
1267            }
1268
1269            if depth == 0 && !is_delimited {
1270                end_tok = tok.clone();
1271                tokens.push(tok);
1272                break;
1273            }
1274
1275            if let Some(delims) = delims {
1276                if is_delimited && depth == 0 {
1277                    if let Some(last) = delims.last() {
1278                        if tok.text == *last {
1279                            end_tok = tok;
1280                            break;
1281                        }
1282                    }
1283                }
1284            }
1285            tokens.push(tok);
1286        }
1287
1288        if start.text == "{" && tokens.last().is_some_and(|t| t.text == "}") {
1289            tokens.pop();
1290            tokens.remove(0);
1291        }
1292
1293        tokens.reverse();
1294
1295        Ok(ConsumedArg {
1296            tokens,
1297            start,
1298            end: end_tok,
1299        })
1300    }
1301
1302    /// Consume N arguments.
1303    fn consume_args(&mut self, num_args: usize) -> ParseResult<Vec<Vec<Token>>> {
1304        let mut args = Vec::new();
1305        for _ in 0..num_args {
1306            let arg = self.consume_arg(None)?;
1307            args.push(arg.tokens);
1308        }
1309        Ok(args)
1310    }
1311
1312    /// Scan a function argument (optional or mandatory).
1313    /// Pushes an EOF token to mark the end, then pushes the argument tokens.
1314    pub fn scan_argument(&mut self, is_optional: bool) -> ParseResult<Option<Token>> {
1315        if is_optional {
1316            self.consume_spaces();
1317            if self.future().text != "[" {
1318                return Ok(None);
1319            }
1320            let start = self.pop_token();
1321            let arg = self.consume_arg(Some(&["]"]))?;
1322            let end = &arg.end;
1323            let end_loc = end.loc.clone();
1324
1325            self.push_token(Token::new("EOF", end_loc.start, end_loc.end));
1326            self.push_tokens(arg.tokens);
1327
1328            let result = Token {
1329                text: String::new(),
1330                loc: SourceLocation::range(&start.loc, &end_loc),
1331                noexpand: false,
1332                treat_as_relax: false,
1333            };
1334            Ok(Some(result))
1335        } else {
1336            let arg = self.consume_arg(None)?;
1337            let end_loc = arg.end.loc.clone();
1338
1339            self.push_token(Token::new("EOF", end_loc.start, end_loc.end));
1340            self.push_tokens(arg.tokens);
1341
1342            let result = Token {
1343                text: String::new(),
1344                loc: SourceLocation::range(&arg.start.loc, &end_loc),
1345                noexpand: false,
1346                treat_as_relax: false,
1347            };
1348            Ok(Some(result))
1349        }
1350    }
1351
1352    /// Check if a command name is currently defined.
1353    pub fn is_defined(&self, name: &str) -> bool {
1354        self.macros.has(name)
1355            || FUNCTIONS.contains_key(name)
1356            || is_known_symbol(name)
1357            || IMPLICIT_COMMANDS.contains(&name)
1358    }
1359
1360    /// Check if a command is expandable.
1361    pub fn is_expandable(&self, name: &str) -> bool {
1362        if let Some(_def) = self.macros.get(name) {
1363            return true;
1364        }
1365        if let Some(func) = FUNCTIONS.get(name) {
1366            return !func.primitive;
1367        }
1368        false
1369    }
1370}
1371
1372pub struct ConsumedArg {
1373    pub tokens: Vec<Token>,
1374    pub start: Token,
1375    pub end: Token,
1376}
1377
1378fn handle_newcommand(
1379    me: &mut MacroExpander,
1380    exists_ok: bool,
1381    nonexists_ok: bool,
1382) -> ParseResult<Vec<Token>> {
1383    let name_arg = me.consume_arg(None)?;
1384    // name_arg.tokens is reversed (stack order); last element = first token in original
1385    let name = name_arg
1386        .tokens
1387        .last()
1388        .map_or_else(String::new, |t| t.text.clone());
1389
1390    let exists = me.is_defined(&name);
1391    if exists && !exists_ok {
1392        return Err(ParseError::msg(format!(
1393            "\\newcommand{{{}}} attempting to redefine {}; use \\renewcommand",
1394            name, name
1395        )));
1396    }
1397    if !exists && !nonexists_ok {
1398        return Err(ParseError::msg(format!(
1399            "\\renewcommand{{{}}} when command {} does not yet exist; use \\newcommand",
1400            name, name
1401        )));
1402    }
1403
1404    me.consume_spaces();
1405    let mut num_args = 0usize;
1406    if me.future().text == "[" {
1407        me.pop_token();
1408        let narg_tok = me.pop_token();
1409        num_args = narg_tok.text.parse().unwrap_or(0);
1410        let close = me.pop_token();
1411        if close.text != "]" {
1412            return Err(ParseError::msg("Expected ] in \\newcommand"));
1413        }
1414    }
1415
1416    let body_arg = me.consume_arg(None)?;
1417    let tokens = body_arg.tokens;
1418
1419    me.set_macro(name, MacroDefinition::Tokens { tokens, num_args });
1420    Ok(vec![])
1421}
1422
1423fn is_known_symbol(name: &str) -> bool {
1424    use ratex_font::symbols;
1425    symbols::get_symbol(name, symbols::Mode::Math).is_some()
1426        || symbols::get_symbol(name, symbols::Mode::Text).is_some()
1427}
1428
1429#[cfg(test)]
1430mod resource_tests;