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