Skip to main content

ratex_parser/
macro_expander.rs

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