Skip to main content

ratex_parser/
parser.rs

1use ratex_lexer::token::{SourceLocation, Token};
2use unicode_normalization::UnicodeNormalization;
3
4use crate::error::{ParseError, ParseResult};
5use crate::functions::{self, ArgType, FunctionContext, FUNCTIONS};
6use crate::macro_expander::{MacroExpander, IMPLICIT_COMMANDS};
7use crate::parse_node::{AtomFamily, Mode, ParseNode};
8use crate::stack_safety::{validate_parse_nodes, DepthBudget, MAX_INPUT_DEPTH};
9
10/// End-of-expression tokens.
11static END_OF_EXPRESSION: &[&str] = &["}", "\\endgroup", "\\end", "\\right", "&"];
12
13/// Maximum supported nesting for input-controlled recursive structures.
14///
15/// Keep this conservative: parsing is used from mobile and WebAssembly hosts whose
16/// worker stacks can be substantially smaller than a desktop main-thread stack.
17/// The LaTeX parser. Converts a token stream into a ParseNode AST.
18///
19/// Follows KaTeX's Parser.ts closely:
20/// - `parse()` → parse full expression
21/// - `parseExpression()` → parse a list of atoms
22/// - `parseAtom()` → parse one atom with optional super/subscripts
23/// - `parseGroup()` → parse a group (braced or single token)
24/// - `parseFunction()` → parse a function call with arguments
25/// - `parseSymbol()` → parse a single symbol
26pub struct Parser<'a> {
27    pub mode: Mode,
28    pub gullet: MacroExpander<'a>,
29    pub leftright_depth: i32,
30    depth_budget: DepthBudget,
31    next_token: Option<Token>,
32    pub equation_counter: usize,
33}
34
35impl<'a> Parser<'a> {
36    pub fn new(input: &'a str) -> Self {
37        let depth_budget = DepthBudget::new(MAX_INPUT_DEPTH);
38        Self {
39            mode: Mode::Math,
40            gullet: MacroExpander::new_with_budget(input, Mode::Math, depth_budget.clone()),
41            leftright_depth: 0,
42            depth_budget,
43            next_token: None,
44            equation_counter: 0,
45        }
46    }
47
48    // ── Token management ────────────────────────────────────────────────
49
50    /// Return the current lookahead token (fetching from gullet if needed).
51    pub fn fetch(&mut self) -> ParseResult<Token> {
52        if self.next_token.is_none() {
53            self.next_token = Some(self.gullet.expand_next_token()?);
54        }
55        Ok(self.next_token.clone().unwrap())
56    }
57
58    /// Discard the current lookahead token.
59    pub fn consume(&mut self) {
60        self.next_token = None;
61    }
62
63    /// Expect the next token to have the given text, consuming it.
64    pub fn expect(&mut self, text: &str, do_consume: bool) -> ParseResult<()> {
65        let tok = self.fetch()?;
66        if tok.text != text {
67            return Err(ParseError::new(
68                format!("Expected '{}', got '{}'", text, tok.text),
69                Some(&tok),
70            ));
71        }
72        if do_consume {
73            self.consume();
74        }
75        Ok(())
76    }
77
78    /// Consume spaces in math mode.
79    pub fn consume_spaces(&mut self) -> ParseResult<()> {
80        loop {
81            let tok = self.fetch()?;
82            if tok.text == " " {
83                self.consume();
84            } else {
85                break;
86            }
87        }
88        Ok(())
89    }
90
91    /// Switch between "math" and "text" modes.
92    pub fn switch_mode(&mut self, new_mode: Mode) {
93        self.mode = new_mode;
94        self.gullet.switch_mode(new_mode);
95    }
96
97    pub(crate) fn depth_budget(&self) -> &DepthBudget {
98        &self.depth_budget
99    }
100
101    // ── Main parse entry ────────────────────────────────────────────────
102
103    /// Parse the entire input and return the AST.
104    pub fn parse(&mut self) -> ParseResult<Vec<ParseNode>> {
105        self.gullet.begin_group();
106
107        // The root expression itself is not input nesting.  Only recursive
108        // expressions below it consume the structural-depth budget.
109        let result = self.parse_expression_impl(false, None);
110
111        match result {
112            Ok(parse) => {
113                let eof_result = self.expect("EOF", true);
114                self.gullet.end_group();
115                eof_result?;
116                validate_parse_nodes(&parse, MAX_INPUT_DEPTH)?;
117                Ok(parse)
118            }
119            Err(e) => {
120                self.gullet.end_groups();
121                Err(e)
122            }
123        }
124    }
125
126    // ── Expression parsing ──────────────────────────────────────────────
127
128    /// Parse an expression: a list of atoms.
129    pub fn parse_expression(
130        &mut self,
131        break_on_infix: bool,
132        break_on_token_text: Option<&str>,
133    ) -> ParseResult<Vec<ParseNode>> {
134        let _guard = self
135            .depth_budget
136            .enter()
137            .map_err(|_| ParseError::recursion_limit_exceeded())?;
138
139        self.parse_expression_impl(break_on_infix, break_on_token_text)
140    }
141
142    fn parse_expression_impl(
143        &mut self,
144        break_on_infix: bool,
145        break_on_token_text: Option<&str>,
146    ) -> ParseResult<Vec<ParseNode>> {
147        let mut body = Vec::new();
148
149        loop {
150            if self.mode == Mode::Math {
151                self.consume_spaces()?;
152            }
153
154            let lex = self.fetch()?;
155
156            if END_OF_EXPRESSION.contains(&lex.text.as_str()) {
157                break;
158            }
159            if let Some(break_text) = break_on_token_text {
160                if lex.text == break_text {
161                    break;
162                }
163            }
164            if break_on_infix {
165                if let Some(func) = FUNCTIONS.get(lex.text.as_str()) {
166                    if func.infix {
167                        break;
168                    }
169                }
170            }
171
172            let atom = self.parse_atom(break_on_token_text)?;
173
174            match atom {
175                None => break,
176                Some(node) if node.type_name() == "internal" => continue,
177                Some(node) => body.push(node),
178            }
179        }
180
181        if self.mode == Mode::Text {
182            self.form_ligatures(&mut body);
183        }
184
185        self.handle_infix_nodes(body)
186    }
187
188    /// Rewrite infix operators (e.g. \over → \frac).
189    fn handle_infix_nodes(&mut self, body: Vec<ParseNode>) -> ParseResult<Vec<ParseNode>> {
190        let mut over_index: Option<usize> = None;
191        let mut func_name: Option<String> = None;
192
193        for (i, node) in body.iter().enumerate() {
194            if let ParseNode::Infix { replace_with, .. } = node {
195                if over_index.is_some() {
196                    return Err(ParseError::msg("only one infix operator per group"));
197                }
198                over_index = Some(i);
199                func_name = Some(replace_with.clone());
200            }
201        }
202
203        if let (Some(idx), Some(fname)) = (over_index, func_name) {
204            let numer_body: Vec<ParseNode> = body[..idx].to_vec();
205            let denom_body: Vec<ParseNode> = body[idx + 1..].to_vec();
206
207            let numer = if numer_body.len() == 1 {
208                if let ParseNode::OrdGroup { .. } = &numer_body[0] {
209                    numer_body.into_iter().next().unwrap()
210                } else {
211                    ParseNode::OrdGroup {
212                        mode: self.mode,
213                        body: numer_body,
214                        semisimple: None,
215                        loc: None,
216                    }
217                }
218            } else {
219                ParseNode::OrdGroup {
220                    mode: self.mode,
221                    body: numer_body,
222                    semisimple: None,
223                    loc: None,
224                }
225            };
226
227            let denom = if denom_body.len() == 1 {
228                if let ParseNode::OrdGroup { .. } = &denom_body[0] {
229                    denom_body.into_iter().next().unwrap()
230                } else {
231                    ParseNode::OrdGroup {
232                        mode: self.mode,
233                        body: denom_body,
234                        semisimple: None,
235                        loc: None,
236                    }
237                }
238            } else {
239                ParseNode::OrdGroup {
240                    mode: self.mode,
241                    body: denom_body,
242                    semisimple: None,
243                    loc: None,
244                }
245            };
246
247            let node = if fname == "\\\\abovefrac" {
248                // \above passes the infix node (with bar size) as the middle argument
249                let infix_node = body[idx].clone();
250                self.call_function(&fname, vec![numer, infix_node, denom], vec![], None, None)?
251            } else {
252                self.call_function(&fname, vec![numer, denom], vec![], None, None)?
253            };
254            Ok(vec![node])
255        } else {
256            Ok(body)
257        }
258    }
259
260    /// Form ligatures in text mode (e.g. -- → –, --- → —).
261    fn form_ligatures(&self, group: &mut Vec<ParseNode>) {
262        let mut i = 0;
263        while i + 1 < group.len() {
264            let a_text = group[i].symbol_text().map(|s| s.to_string());
265            let b_text = group[i + 1].symbol_text().map(|s| s.to_string());
266
267            if let (Some(a), Some(b)) = (a_text, b_text) {
268                if group[i].type_name() == "textord" && group[i + 1].type_name() == "textord" {
269                    if a == "-" && b == "-" {
270                        if i + 2 < group.len() {
271                            if let Some(c) = group[i + 2].symbol_text() {
272                                if c == "-" && group[i + 2].type_name() == "textord" {
273                                    group[i] = ParseNode::TextOrd {
274                                        mode: Mode::Text,
275                                        text: "---".to_string(),
276                                        loc: None,
277                                    };
278                                    group.remove(i + 2);
279                                    group.remove(i + 1);
280                                    continue;
281                                }
282                            }
283                        }
284                        group[i] = ParseNode::TextOrd {
285                            mode: Mode::Text,
286                            text: "--".to_string(),
287                            loc: None,
288                        };
289                        group.remove(i + 1);
290                        continue;
291                    }
292                    if (a == "'" || a == "`") && b == a {
293                        group[i] = ParseNode::TextOrd {
294                            mode: Mode::Text,
295                            text: format!("{}{}", a, a),
296                            loc: None,
297                        };
298                        group.remove(i + 1);
299                        continue;
300                    }
301                }
302            }
303            i += 1;
304        }
305    }
306
307    // ── Atom parsing ────────────────────────────────────────────────────
308
309    /// Parse a single atom with optional super/subscripts.
310    pub fn parse_atom(
311        &mut self,
312        break_on_token_text: Option<&str>,
313    ) -> ParseResult<Option<ParseNode>> {
314        let mut base = self.parse_group("atom", break_on_token_text)?;
315
316        if let Some(ref b) = base {
317            if b.type_name() == "internal" {
318                return Ok(base);
319            }
320        }
321
322        if self.mode == Mode::Text {
323            return Ok(base);
324        }
325
326        let mut superscript: Option<ParseNode> = None;
327        let mut subscript: Option<ParseNode> = None;
328
329        loop {
330            self.consume_spaces()?;
331            let lex = self.fetch()?;
332
333            if lex.text == "\\limits" || lex.text == "\\nolimits" {
334                let is_limits = lex.text == "\\limits";
335                self.consume();
336                if let Some(base_node) = base.as_mut() {
337                    match base_node {
338                        ParseNode::Op {
339                            limits,
340                            always_handle_sup_sub,
341                            ..
342                        } => {
343                            *limits = is_limits;
344                            *always_handle_sup_sub = Some(is_limits);
345                        }
346                        ParseNode::OperatorName {
347                            limits,
348                            always_handle_sup_sub,
349                            ..
350                        } => {
351                            *limits = is_limits;
352                            *always_handle_sup_sub = is_limits;
353                        }
354                        _ => {}
355                    }
356                }
357            } else if lex.text == "^" {
358                if superscript.is_some() {
359                    return Err(ParseError::new("Double superscript", Some(&lex)));
360                }
361                superscript = Some(self.handle_sup_subscript("superscript")?);
362            } else if lex.text == "_" {
363                if subscript.is_some() {
364                    return Err(ParseError::new("Double subscript", Some(&lex)));
365                }
366                subscript = Some(self.handle_sup_subscript("subscript")?);
367            } else if lex.text == "'" {
368                if superscript.is_some() {
369                    return Err(ParseError::new("Double superscript", Some(&lex)));
370                }
371                let prime = ParseNode::TextOrd {
372                    mode: self.mode,
373                    text: "\\prime".to_string(),
374                    loc: None,
375                };
376                let mut primes = vec![prime.clone()];
377                self.consume();
378                while self.fetch()?.text == "'" {
379                    primes.push(prime.clone());
380                    self.consume();
381                }
382                if self.fetch()?.text == "^" {
383                    primes.push(self.handle_sup_subscript("superscript")?);
384                }
385                superscript = Some(ParseNode::OrdGroup {
386                    mode: self.mode,
387                    body: primes,
388                    semisimple: None,
389                    loc: None,
390                });
391            } else if let Some((mapped, is_sub)) = lex
392                .text
393                .chars()
394                .next()
395                .and_then(crate::unicode_sup_sub::unicode_sub_sup)
396            {
397                if is_sub && subscript.is_some() {
398                    return Err(ParseError::new("Double subscript", Some(&lex)));
399                }
400                if !is_sub && superscript.is_some() {
401                    return Err(ParseError::new("Double superscript", Some(&lex)));
402                }
403                // Collect consecutive Unicode sup/sub chars of the same kind
404                let mut subsup_tokens = vec![Token::new(mapped, 0, 0)];
405                self.consume();
406                loop {
407                    let tok = self.fetch()?;
408                    match tok
409                        .text
410                        .chars()
411                        .next()
412                        .and_then(crate::unicode_sup_sub::unicode_sub_sup)
413                    {
414                        Some((m, sub)) if sub == is_sub => {
415                            subsup_tokens.insert(0, Token::new(m, 0, 0));
416                            self.consume();
417                        }
418                        _ => break,
419                    }
420                }
421                let body = self.subparse(subsup_tokens)?;
422                let group = ParseNode::OrdGroup {
423                    mode: Mode::Math,
424                    body,
425                    semisimple: None,
426                    loc: None,
427                };
428                if is_sub {
429                    subscript = Some(group);
430                } else {
431                    superscript = Some(group);
432                }
433            } else {
434                break;
435            }
436        }
437
438        if superscript.is_some() || subscript.is_some() {
439            Ok(Some(ParseNode::SupSub {
440                mode: self.mode,
441                base: base.map(Box::new),
442                sup: superscript.map(Box::new),
443                sub: subscript.map(Box::new),
444                loc: None,
445            }))
446        } else {
447            Ok(base)
448        }
449    }
450
451    /// Handle a subscript or superscript.
452    fn handle_sup_subscript(&mut self, name: &str) -> ParseResult<ParseNode> {
453        let symbol_token = self.fetch()?;
454        self.consume();
455        self.consume_spaces()?;
456
457        let group = self.parse_group(name, None)?;
458        match group {
459            Some(g) if g.type_name() != "internal" => Ok(g),
460            Some(_) => {
461                // Skip internal nodes, try again
462                let g2 = self.parse_group(name, None)?;
463                g2.ok_or_else(|| {
464                    ParseError::new(
465                        format!("Expected group after '{}'", symbol_token.text),
466                        Some(&symbol_token),
467                    )
468                })
469            }
470            None => Err(ParseError::new(
471                format!("Expected group after '{}'", symbol_token.text),
472                Some(&symbol_token),
473            )),
474        }
475    }
476
477    // ── Group parsing ───────────────────────────────────────────────────
478
479    /// Parse a group: braced expression, function call, or single symbol.
480    pub fn parse_group(
481        &mut self,
482        name: &str,
483        break_on_token_text: Option<&str>,
484    ) -> ParseResult<Option<ParseNode>> {
485        self.parse_group_impl(name, break_on_token_text, false)
486    }
487
488    pub(crate) fn parse_structural_group(
489        &mut self,
490        name: &str,
491        break_on_token_text: Option<&str>,
492    ) -> ParseResult<Option<ParseNode>> {
493        let _guard = self
494            .depth_budget
495            .enter()
496            .map_err(|_| ParseError::recursion_limit_exceeded())?;
497        self.parse_group_impl(name, break_on_token_text, true)
498    }
499
500    fn parse_delimiter_group(&mut self, name: &str) -> ParseResult<Option<ParseNode>> {
501        let first_token = self.fetch()?;
502        let text = first_token.text.clone();
503
504        if text == "{" || text == "\\begingroup" {
505            return self.parse_structural_group(name, None);
506        }
507
508        let result = self.parse_symbol_inner()?;
509        if result.is_none() {
510            return Err(ParseError::new(
511                format!("Expected delimiter as {}", name),
512                Some(&first_token),
513            ));
514        }
515
516        Ok(result)
517    }
518
519    fn parse_group_impl(
520        &mut self,
521        name: &str,
522        break_on_token_text: Option<&str>,
523        depth_already_counted: bool,
524    ) -> ParseResult<Option<ParseNode>> {
525        let first_token = self.fetch()?;
526        let text = first_token.text.clone();
527
528        if text == "{" || text == "\\begingroup" {
529            self.consume();
530            let group_end = if text == "{" { "}" } else { "\\endgroup" };
531
532            if !depth_already_counted && self.depth_budget.remaining() == 0 {
533                return Err(ParseError::recursion_limit_exceeded());
534            }
535
536            self.gullet.begin_group();
537            let result: ParseResult<_> = (|| {
538                let expression = if depth_already_counted {
539                    self.parse_expression_impl(false, Some(group_end))?
540                } else {
541                    self.parse_expression(false, Some(group_end))?
542                };
543                let last_token = self.fetch()?;
544                self.expect(group_end, true)?;
545                Ok((expression, last_token))
546            })();
547            self.gullet.end_group();
548            let (expression, last_token) = result?;
549
550            let loc = Some(SourceLocation::range(&first_token.loc, &last_token.loc));
551            let semisimple = if text == "\\begingroup" {
552                Some(true)
553            } else {
554                None
555            };
556
557            Ok(Some(ParseNode::OrdGroup {
558                mode: self.mode,
559                body: expression,
560                semisimple,
561                loc,
562            }))
563        } else {
564            let result = match self.parse_function(break_on_token_text, Some(name))? {
565                some @ Some(_) => some,
566                None => self.parse_symbol_inner()?,
567            };
568
569            if result.is_none()
570                && text.starts_with('\\')
571                && !IMPLICIT_COMMANDS.contains(&text.as_str())
572            {
573                return Err(ParseError::new(
574                    format!("Undefined control sequence: {}", text),
575                    Some(&first_token),
576                ));
577            }
578
579            Ok(result)
580        }
581    }
582
583    // ── Function parsing ────────────────────────────────────────────────
584
585    /// Try to parse a function call. Returns None if not a function.
586    pub fn parse_function(
587        &mut self,
588        break_on_token_text: Option<&str>,
589        name: Option<&str>,
590    ) -> ParseResult<Option<ParseNode>> {
591        let token = self.fetch()?;
592        let func = token.text.clone();
593
594        let func_data = match FUNCTIONS.get(func.as_str()) {
595            Some(f) => f,
596            None => return Ok(None),
597        };
598
599        self.consume();
600
601        if let Some(n) = name {
602            if n != "atom" && !func_data.allowed_in_argument {
603                return Err(ParseError::new(
604                    format!("Got function '{}' with no arguments as {}", func, n),
605                    Some(&token),
606                ));
607            }
608        }
609
610        functions::check_mode_compatibility(func_data, self.mode, &func, Some(&token))?;
611
612        // `\hspace*{len}` — `*` is a separate token (not part of the control word); consume it here.
613        // Must use gullet peek/pop only: `parser.fetch()` without `consume()` advances the lexer and
614        // leaves `{` only in `next_token`, so `parse_size_group`'s `gullet.future()` would miss the brace.
615        if func == "\\hspace" {
616            self.gullet.consume_spaces();
617            if self.gullet.future().text == "*" {
618                self.gullet.pop_token();
619            }
620        }
621
622        let (args, opt_args) = self.parse_arguments(&func, func_data)?;
623
624        self.call_function(
625            &func,
626            args,
627            opt_args,
628            Some(token),
629            break_on_token_text.map(|s| s.to_string()).as_deref(),
630        )
631        .map(Some)
632    }
633
634    /// Call a function handler.
635    pub fn call_function(
636        &mut self,
637        name: &str,
638        args: Vec<ParseNode>,
639        opt_args: Vec<Option<ParseNode>>,
640        token: Option<Token>,
641        break_on_token_text: Option<&str>,
642    ) -> ParseResult<ParseNode> {
643        let func = FUNCTIONS
644            .get(name)
645            .ok_or_else(|| ParseError::msg(format!("No function handler for {}", name)))?;
646
647        let mut ctx = FunctionContext {
648            func_name: name.to_string(),
649            parser: self,
650            token: token.clone(),
651            break_on_token_text: break_on_token_text.map(|s| s.to_string()),
652        };
653
654        (func.handler)(&mut ctx, args, opt_args)
655    }
656
657    /// Parse the arguments for a function.
658    pub fn parse_arguments(
659        &mut self,
660        func: &str,
661        func_data: &functions::FunctionSpec,
662    ) -> ParseResult<(Vec<ParseNode>, Vec<Option<ParseNode>>)> {
663        let total_args = func_data.num_args + func_data.num_optional_args;
664        if total_args == 0 {
665            return Ok((Vec::new(), Vec::new()));
666        }
667
668        let mut args = Vec::new();
669        let mut opt_args = Vec::new();
670
671        for i in 0..total_args {
672            let arg_type = func_data
673                .arg_types
674                .as_ref()
675                .and_then(|types| types.get(i).copied());
676            let is_optional = i < func_data.num_optional_args;
677
678            let effective_type = if (func_data.primitive && arg_type.is_none())
679                || (func_data.node_type == "sqrt"
680                    && i == 1
681                    && opt_args
682                        .first()
683                        .is_some_and(|o: &Option<ParseNode>| o.is_none()))
684            {
685                Some(ArgType::Primitive)
686            } else {
687                arg_type
688            };
689            let structural = argument_consumes_depth(func, func_data, effective_type);
690
691            let arg = self.parse_group_of_type(
692                &format!("argument to '{}'", func),
693                effective_type,
694                is_optional,
695                structural,
696            )?;
697
698            if is_optional {
699                opt_args.push(arg);
700            } else if let Some(a) = arg {
701                args.push(a);
702            } else {
703                return Err(ParseError::msg(
704                    "Null argument, please report this as a bug",
705                ));
706            }
707        }
708
709        Ok((args, opt_args))
710    }
711
712    /// Parse a group with a specific type.
713    fn parse_group_of_type(
714        &mut self,
715        name: &str,
716        arg_type: Option<ArgType>,
717        optional: bool,
718        structural: bool,
719    ) -> ParseResult<Option<ParseNode>> {
720        match arg_type {
721            Some(ArgType::Color) => self.parse_color_group(optional),
722            Some(ArgType::Size) => self.parse_size_group(optional),
723            Some(ArgType::Primitive) => {
724                if optional {
725                    return Err(ParseError::msg("A primitive argument cannot be optional"));
726                }
727                let group = if structural {
728                    self.parse_structural_group(name, None)?
729                } else {
730                    self.parse_delimiter_group(name)?
731                };
732                match group {
733                    Some(g) => Ok(Some(g)),
734                    None => Err(ParseError::new(format!("Expected group as {}", name), None)),
735                }
736            }
737            Some(ArgType::Math) | Some(ArgType::Text) => {
738                let mode = match arg_type {
739                    Some(ArgType::Math) => Some(Mode::Math),
740                    Some(ArgType::Text) => Some(Mode::Text),
741                    _ => None,
742                };
743                self.parse_argument_group(optional, mode)
744            }
745            Some(ArgType::HBox) => {
746                let group = self.parse_argument_group(optional, Some(Mode::Text))?;
747                match group {
748                    Some(g) => Ok(Some(ParseNode::Styling {
749                        mode: g.mode(),
750                        style: crate::parse_node::StyleStr::Text,
751                        body: vec![g],
752                        loc: None,
753                    })),
754                    None => Ok(None),
755                }
756            }
757            Some(ArgType::Raw) => {
758                let token = self.parse_string_group("raw", optional)?;
759                match token {
760                    Some(t) => Ok(Some(ParseNode::Raw {
761                        mode: Mode::Text,
762                        string: t.text,
763                        loc: None,
764                    })),
765                    None => Ok(None),
766                }
767            }
768            Some(ArgType::Url) => self.parse_url_group(optional),
769            None | Some(ArgType::Original) => self.parse_argument_group(optional, None),
770        }
771    }
772
773    /// Parse a color group.
774    fn parse_color_group(&mut self, optional: bool) -> ParseResult<Option<ParseNode>> {
775        let res = self.parse_string_group("color", optional)?;
776        match res {
777            None => Ok(None),
778            Some(token) => {
779                let text = token.text.trim().to_string();
780                let re = regex_lite::Regex::new(
781                    r"^(#[a-fA-F0-9]{3,4}|#[a-fA-F0-9]{6}|#[a-fA-F0-9]{8}|[a-fA-F0-9]{6}|[a-zA-Z]+|\d+(\.\d+)?(,\d+(\.\d+)?)*)$",
782                )
783                .unwrap();
784
785                if !re.is_match(&text) {
786                    return Err(ParseError::new(
787                        format!("Invalid color: '{}'", text),
788                        Some(&token),
789                    ));
790                }
791                let mut color = text;
792                if regex_lite::Regex::new(r"^[0-9a-fA-F]{6}$")
793                    .unwrap()
794                    .is_match(&color)
795                {
796                    color = format!("#{}", color);
797                }
798
799                Ok(Some(ParseNode::ColorToken {
800                    mode: self.mode,
801                    color,
802                    loc: None,
803                }))
804            }
805        }
806    }
807
808    /// Parse a size group (e.g., "3pt", "1em").
809    pub fn parse_size_group(&mut self, optional: bool) -> ParseResult<Option<ParseNode>> {
810        let mut is_blank = false;
811
812        self.gullet.consume_spaces();
813        let res = if !optional && self.gullet.future().text != "{" {
814            Some(
815                self.parse_regex_group(
816                    &regex_lite::Regex::new(r"^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$")
817                        .unwrap(),
818                    "size",
819                )?,
820            )
821        } else {
822            self.parse_string_group("size", optional)?
823        };
824
825        let res = match res {
826            Some(r) => r,
827            None => return Ok(None),
828        };
829
830        let mut text = res.text.clone();
831        if !optional && text.is_empty() {
832            text = "0pt".to_string();
833            is_blank = true;
834        }
835
836        let size_re =
837            regex_lite::Regex::new(r"([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})").unwrap();
838        let m = size_re
839            .captures(&text)
840            .ok_or_else(|| ParseError::new(format!("Invalid size: '{}'", text), Some(&res)))?;
841
842        let sign = m.get(1).map_or("", |m| m.as_str());
843        let magnitude = m.get(2).map_or("", |m| m.as_str());
844        let unit = m.get(3).map_or("", |m| m.as_str());
845
846        let number: f64 = format!("{}{}", sign, magnitude).parse().unwrap_or(0.0);
847
848        if !is_valid_unit(unit) {
849            return Err(ParseError::new(
850                format!("Invalid unit: '{}'", unit),
851                Some(&res),
852            ));
853        }
854
855        Ok(Some(ParseNode::Size {
856            mode: self.mode,
857            value: crate::parse_node::Measurement {
858                number,
859                unit: unit.to_string(),
860            },
861            is_blank,
862            loc: None,
863        }))
864    }
865
866    /// Parse a URL group.
867    /// Temporarily disables `%` as comment character to allow `%20` etc. in URLs.
868    fn parse_url_group(&mut self, optional: bool) -> ParseResult<Option<ParseNode>> {
869        self.gullet.lexer.set_catcode('%', 13);
870        self.gullet.lexer.set_catcode('~', 12);
871        let res = self.parse_string_group("url", optional);
872        self.gullet.lexer.set_catcode('%', 14);
873        self.gullet.lexer.set_catcode('~', 13);
874        let res = res?;
875        match res {
876            None => Ok(None),
877            Some(token) => {
878                let url = token.text;
879                Ok(Some(ParseNode::Url {
880                    mode: self.mode,
881                    url,
882                    loc: None,
883                }))
884            }
885        }
886    }
887
888    /// Parse a string group (brace-enclosed string).
889    fn parse_string_group(
890        &mut self,
891        _mode_name: &str,
892        optional: bool,
893    ) -> ParseResult<Option<Token>> {
894        let arg_token = self.gullet.scan_argument(optional)?;
895        let arg_token = match arg_token {
896            Some(t) => t,
897            None => return Ok(None),
898        };
899
900        let mut s = String::new();
901        loop {
902            let next = self.fetch()?;
903            if next.text == "EOF" {
904                break;
905            }
906            s.push_str(&next.text);
907            self.consume();
908        }
909        self.consume(); // consume EOF
910
911        let mut result = arg_token;
912        result.text = s;
913        Ok(Some(result))
914    }
915
916    /// Parse a regex-delimited group.
917    fn parse_regex_group(
918        &mut self,
919        regex: &regex_lite::Regex,
920        mode_name: &str,
921    ) -> ParseResult<Token> {
922        let first_token = self.fetch()?;
923        let mut last_token = first_token.clone();
924        let mut s = String::new();
925
926        loop {
927            let next = self.fetch()?;
928            if next.text == "EOF" {
929                break;
930            }
931            let candidate = format!("{}{}", s, next.text);
932            if regex.is_match(&candidate) {
933                last_token = next;
934                s = candidate;
935                self.consume();
936            } else {
937                break;
938            }
939        }
940
941        if s.is_empty() {
942            return Err(ParseError::new(
943                format!("Invalid {}: '{}'", mode_name, first_token.text),
944                Some(&first_token),
945            ));
946        }
947
948        Ok(first_token.range(&last_token, s))
949    }
950
951    /// Parse an argument group (with optional mode switch).
952    pub fn parse_argument_group(
953        &mut self,
954        optional: bool,
955        mode: Option<Mode>,
956    ) -> ParseResult<Option<ParseNode>> {
957        let arg_token = self.gullet.scan_argument(optional)?;
958        let arg_token = match arg_token {
959            Some(t) => t,
960            None => return Ok(None),
961        };
962
963        let outer_mode = self.mode;
964        if let Some(m) = mode {
965            self.switch_mode(m);
966        }
967
968        self.gullet.begin_group();
969        let result: ParseResult<_> = (|| {
970            let expression = self.parse_expression(false, Some("EOF"))?;
971            self.expect("EOF", true)?;
972            Ok(expression)
973        })();
974        self.gullet.end_group();
975        let result_mode = self.mode;
976        let expression = result;
977
978        if mode.is_some() {
979            self.switch_mode(outer_mode);
980        }
981
982        let expression = expression?;
983
984        let result = ParseNode::OrdGroup {
985            mode: result_mode,
986            loc: Some(arg_token.loc.clone()),
987            body: expression,
988            semisimple: None,
989        };
990
991        Ok(Some(result))
992    }
993
994    // ── Symbol parsing ──────────────────────────────────────────────────
995
996    /// Parse a single symbol (internal version that returns Result).
997    fn parse_symbol_inner(&mut self) -> ParseResult<Option<ParseNode>> {
998        let nucleus = self.fetch()?;
999        let text = nucleus.text.clone();
1000
1001        if let Some(stripped) = text.strip_prefix("\\verb") {
1002            self.consume();
1003            let (star, rest) = if let Some(r) = stripped.strip_prefix('*') {
1004                (true, r)
1005            } else {
1006                (false, stripped)
1007            };
1008            let chars: Vec<char> = rest.chars().collect();
1009            if chars.len() < 2 {
1010                return Err(ParseError::new("\\verb assertion failed", Some(&nucleus)));
1011            }
1012            let body: String = chars[1..chars.len() - 1].iter().collect();
1013            return Ok(Some(ParseNode::Verb {
1014                mode: Mode::Text,
1015                body,
1016                star,
1017                loc: Some(nucleus.loc.clone()),
1018            }));
1019        }
1020
1021        let font_mode = match self.mode {
1022            Mode::Math => ratex_font::symbols::Mode::Math,
1023            Mode::Text => ratex_font::symbols::Mode::Text,
1024        };
1025
1026        // ^ and _ are handled by parse_atom for sup/sub, not as symbol nodes
1027        if text == "^" || text == "_" {
1028            return Ok(None);
1029        }
1030
1031        // Bare backslash (incomplete control sequence) → not a valid symbol
1032        if text == "\\" {
1033            return Ok(None);
1034        }
1035
1036        if let Some(sym_info) = ratex_font::symbols::get_symbol(&text, font_mode) {
1037            let loc = Some(SourceLocation::range(&nucleus.loc, &nucleus.loc));
1038            let group = sym_info.group;
1039
1040            let node = if group.is_atom() {
1041                let family = match group {
1042                    ratex_font::symbols::Group::Bin => AtomFamily::Bin,
1043                    ratex_font::symbols::Group::Close => AtomFamily::Close,
1044                    ratex_font::symbols::Group::Inner => AtomFamily::Inner,
1045                    ratex_font::symbols::Group::Open => AtomFamily::Open,
1046                    ratex_font::symbols::Group::Punct => AtomFamily::Punct,
1047                    ratex_font::symbols::Group::Rel => AtomFamily::Rel,
1048                    _ => unreachable!(),
1049                };
1050                ParseNode::Atom {
1051                    mode: self.mode,
1052                    family,
1053                    text: text.clone(),
1054                    loc,
1055                }
1056            } else {
1057                match group {
1058                    ratex_font::symbols::Group::MathOrd => ParseNode::MathOrd {
1059                        mode: self.mode,
1060                        text: text.clone(),
1061                        loc,
1062                    },
1063                    ratex_font::symbols::Group::TextOrd => ParseNode::TextOrd {
1064                        mode: self.mode,
1065                        text: text.clone(),
1066                        loc,
1067                    },
1068                    ratex_font::symbols::Group::OpToken => ParseNode::OpToken {
1069                        mode: self.mode,
1070                        text: text.clone(),
1071                        loc,
1072                    },
1073                    ratex_font::symbols::Group::AccentToken => ParseNode::AccentToken {
1074                        mode: self.mode,
1075                        text: text.clone(),
1076                        loc,
1077                    },
1078                    ratex_font::symbols::Group::Spacing => ParseNode::SpacingNode {
1079                        mode: self.mode,
1080                        text: text.clone(),
1081                        loc,
1082                    },
1083                    _ => ParseNode::MathOrd {
1084                        mode: self.mode,
1085                        text: text.clone(),
1086                        loc,
1087                    },
1088                }
1089            };
1090
1091            self.consume();
1092            return Ok(Some(node));
1093        }
1094
1095        // Unicode accented characters → decompose into accent nodes
1096        // Handles both precomposed (á U+00E1) and combining forms (a + U+0301)
1097        if let Some(node) = self.try_parse_unicode_accent(&text, &nucleus)? {
1098            self.consume();
1099            return Ok(Some(node));
1100        }
1101
1102        // Non-ASCII characters without accent decomposition → treat as textord
1103        // KaTeX always uses mode="text" for these, regardless of current mode
1104        let first_char = text.chars().next();
1105        if let Some(ch) = first_char {
1106            if ch as u32 >= 0x80 {
1107                let node = ParseNode::TextOrd {
1108                    mode: Mode::Text,
1109                    text: text.clone(),
1110                    loc: Some(SourceLocation::range(&nucleus.loc, &nucleus.loc)),
1111                };
1112                self.consume();
1113                return Ok(Some(node));
1114            }
1115        }
1116
1117        Ok(None)
1118    }
1119
1120    /// Try to decompose a Unicode accented character into accent nodes.
1121    /// Returns None if no decomposition is available.
1122    /// Only decomposes Latin-script characters, matching KaTeX behavior.
1123    fn try_parse_unicode_accent(
1124        &self,
1125        text: &str,
1126        nucleus: &Token,
1127    ) -> ParseResult<Option<ParseNode>> {
1128        let nfd: String = text.nfd().collect();
1129        let chars: Vec<char> = nfd.chars().collect();
1130
1131        if chars.len() < 2 {
1132            return Ok(None);
1133        }
1134
1135        // Build from the base up through each combining mark
1136        let mut split_idx = chars.len() - 1;
1137        while split_idx > 0 && is_supported_combining_accent(chars[split_idx]) {
1138            split_idx -= 1;
1139        }
1140
1141        // Verify ALL trailing chars are supported combining accents
1142        if split_idx == chars.len() - 1 {
1143            return Ok(None);
1144        }
1145
1146        // Only decompose Latin-script base characters.
1147        let base_char = chars[0];
1148        if !is_latin_base_char(base_char) {
1149            return Ok(None);
1150        }
1151
1152        let accent_count = chars.len() - split_idx - 1;
1153        self.depth_budget
1154            .ensure_additional(accent_count)
1155            .map_err(|_| ParseError::new("Recursion limit exceeded", Some(nucleus)))?;
1156
1157        let loc = Some(SourceLocation::range(&nucleus.loc, &nucleus.loc));
1158
1159        // Base: everything before the combining marks
1160        let mut base_str: String = chars[..split_idx + 1].iter().collect();
1161
1162        // Accented i→ı and j→ȷ (dotless variants), matching KaTeX behavior
1163        if base_str.len() == 1 {
1164            match base_str.as_str() {
1165                "i" => base_str = "\u{0131}".to_string(), // ı
1166                "j" => base_str = "\u{0237}".to_string(), // ȷ
1167                _ => {}
1168            }
1169        }
1170
1171        let font_mode = match self.mode {
1172            Mode::Math => ratex_font::symbols::Mode::Math,
1173            Mode::Text => ratex_font::symbols::Mode::Text,
1174        };
1175
1176        let mut node = if base_str.chars().count() == 1 {
1177            let ch = base_str.chars().next().unwrap();
1178            if let Some(sym) = ratex_font::symbols::get_symbol(&base_str, font_mode) {
1179                match sym.group {
1180                    ratex_font::symbols::Group::TextOrd => ParseNode::TextOrd {
1181                        mode: self.mode,
1182                        text: base_str.clone(),
1183                        loc: loc.clone(),
1184                    },
1185                    _ => ParseNode::MathOrd {
1186                        mode: self.mode,
1187                        text: base_str.clone(),
1188                        loc: loc.clone(),
1189                    },
1190                }
1191            } else if (ch as u32) >= 0x80 {
1192                // Non-ASCII base chars always text mode (KaTeX compat)
1193                ParseNode::TextOrd {
1194                    mode: Mode::Text,
1195                    text: base_str.clone(),
1196                    loc: loc.clone(),
1197                }
1198            } else {
1199                ParseNode::MathOrd {
1200                    mode: self.mode,
1201                    text: base_str.clone(),
1202                    loc: loc.clone(),
1203                }
1204            }
1205        } else {
1206            return self
1207                .try_parse_unicode_accent(&base_str, nucleus)
1208                .map(|opt| {
1209                    opt.or_else(|| {
1210                        Some(ParseNode::TextOrd {
1211                            mode: Mode::Text,
1212                            text: base_str.clone(),
1213                            loc: loc.clone(),
1214                        })
1215                    })
1216                });
1217        };
1218
1219        // Wrap in accent nodes from innermost to outermost
1220        for &combining in &chars[split_idx + 1..] {
1221            let label = combining_to_accent_label(combining, self.mode);
1222            node = ParseNode::Accent {
1223                mode: self.mode,
1224                label,
1225                is_stretchy: Some(false),
1226                is_shifty: Some(true),
1227                base: Box::new(node),
1228                loc: loc.clone(),
1229            };
1230        }
1231
1232        Ok(Some(node))
1233    }
1234
1235    /// Parse a sub-expression from the given tokens.
1236    pub fn subparse(&mut self, tokens: Vec<Token>) -> ParseResult<Vec<ParseNode>> {
1237        let old_token = self.next_token.take();
1238
1239        self.gullet.push_token(Token::new("}", 0, 0));
1240        self.gullet.push_tokens(tokens);
1241        let parse = self.parse_expression(false, None)?;
1242        self.expect("}", true)?;
1243
1244        self.next_token = old_token;
1245        Ok(parse)
1246    }
1247}
1248
1249fn is_latin_base_char(ch: char) -> bool {
1250    matches!(ch,
1251        'A'..='Z' | 'a'..='z'
1252        | '\u{0131}' // ı (dotless i)
1253        | '\u{0237}' // ȷ (dotless j)
1254        | '\u{00C6}' // Æ
1255        | '\u{00D0}' // Ð
1256        | '\u{00D8}' // Ø
1257        | '\u{00DE}' // Þ
1258        | '\u{00DF}' // ß
1259        | '\u{00E6}' // æ
1260        | '\u{00F0}' // ð
1261        | '\u{00F8}' // ø
1262        | '\u{00FE}' // þ
1263    )
1264}
1265
1266fn is_supported_combining_accent(ch: char) -> bool {
1267    matches!(
1268        ch,
1269        '\u{0300}'
1270            | '\u{0301}'
1271            | '\u{0302}'
1272            | '\u{0303}'
1273            | '\u{0304}'
1274            | '\u{0306}'
1275            | '\u{0307}'
1276            | '\u{0308}'
1277            | '\u{030A}'
1278            | '\u{030B}'
1279            | '\u{030C}'
1280            | '\u{0327}'
1281    )
1282}
1283
1284fn combining_to_accent_label(ch: char, mode: Mode) -> String {
1285    match mode {
1286        Mode::Math => match ch {
1287            '\u{0300}' => "\\grave".to_string(),
1288            '\u{0301}' => "\\acute".to_string(),
1289            '\u{0302}' => "\\hat".to_string(),
1290            '\u{0303}' => "\\tilde".to_string(),
1291            '\u{0304}' => "\\bar".to_string(),
1292            '\u{0306}' => "\\breve".to_string(),
1293            '\u{0307}' => "\\dot".to_string(),
1294            '\u{0308}' => "\\ddot".to_string(),
1295            '\u{030A}' => "\\mathring".to_string(),
1296            '\u{030B}' => "\\H".to_string(),
1297            '\u{030C}' => "\\check".to_string(),
1298            '\u{0327}' => "\\c".to_string(),
1299            _ => format!("\\char\"{:X}", ch as u32),
1300        },
1301        Mode::Text => match ch {
1302            '\u{0300}' => "\\`".to_string(),
1303            '\u{0301}' => "\\'".to_string(),
1304            '\u{0302}' => "\\^".to_string(),
1305            '\u{0303}' => "\\~".to_string(),
1306            '\u{0304}' => "\\=".to_string(),
1307            '\u{0306}' => "\\u".to_string(),
1308            '\u{0307}' => "\\.".to_string(),
1309            '\u{0308}' => "\\\"".to_string(),
1310            '\u{030A}' => "\\r".to_string(),
1311            '\u{030B}' => "\\H".to_string(),
1312            '\u{030C}' => "\\v".to_string(),
1313            '\u{0327}' => "\\c".to_string(),
1314            _ => format!("\\char\"{:X}", ch as u32),
1315        },
1316    }
1317}
1318
1319fn argument_consumes_depth(
1320    func_name: &str,
1321    func_data: &functions::FunctionSpec,
1322    arg_type: Option<ArgType>,
1323) -> bool {
1324    match arg_type {
1325        Some(ArgType::Color | ArgType::Size | ArgType::Raw | ArgType::Url) => false,
1326        Some(ArgType::Primitive) => {
1327            !matches!(func_name, "\\left" | "\\right" | "\\middle")
1328                && func_data.node_type != "delimsizing"
1329        }
1330        Some(ArgType::Math | ArgType::Text | ArgType::HBox | ArgType::Original) | None => true,
1331    }
1332}
1333
1334fn is_valid_unit(unit: &str) -> bool {
1335    matches!(
1336        unit,
1337        "pt" | "mm"
1338            | "cm"
1339            | "in"
1340            | "bp"
1341            | "pc"
1342            | "dd"
1343            | "cc"
1344            | "nd"
1345            | "nc"
1346            | "sp"
1347            | "px"
1348            | "ex"
1349            | "em"
1350            | "mu"
1351    )
1352}
1353
1354/// If the whole expression is wrapped in TeX inline/display math delimiters, parse the inside only.
1355/// The parser already runs in math mode; a leading `$` would otherwise hit the `$` / `\\(` "switch to math"
1356/// handler, which is disallowed in math mode (see `functions::math`).
1357fn strip_outer_math_delimiters(input: &str) -> &str {
1358    let s = input.trim();
1359    if s.len() >= 4 && s.starts_with("$$") && s.ends_with("$$") {
1360        return s[2..s.len() - 2].trim();
1361    }
1362    if s.len() >= 2 && s.starts_with('$') && s.ends_with('$') {
1363        return s[1..s.len() - 1].trim();
1364    }
1365    s
1366}
1367
1368/// Convenience function: parse a LaTeX string and return the AST.
1369pub fn parse(input: &str) -> ParseResult<Vec<ParseNode>> {
1370    Parser::new(strip_outer_math_delimiters(input)).parse()
1371}
1372
1373#[cfg(test)]
1374mod tests {
1375    use super::*;
1376
1377    #[test]
1378    fn test_parse_single_char() {
1379        let result = parse("x").unwrap();
1380        assert_eq!(result.len(), 1);
1381        assert_eq!(result[0].type_name(), "mathord");
1382    }
1383
1384    #[test]
1385    fn test_parse_strips_outer_dollar_inline_math() {
1386        let inner = r"C_p[\ce{H2O(l)}] = \pu{75.3 J // mol K}";
1387        let wrapped = format!("${inner}$");
1388        let a = parse(&wrapped).expect("wrapped");
1389        let b = parse(inner).expect("inner");
1390        assert_eq!(a.len(), b.len());
1391        for (x, y) in a.iter().zip(b.iter()) {
1392            assert_eq!(x.type_name(), y.type_name());
1393        }
1394    }
1395
1396    #[test]
1397    fn test_parse_addition() {
1398        let result = parse("a+b").unwrap();
1399        assert_eq!(result.len(), 3);
1400        assert_eq!(result[0].type_name(), "mathord"); // a
1401        assert_eq!(result[1].type_name(), "atom"); // +
1402        assert_eq!(result[2].type_name(), "mathord"); // b
1403    }
1404
1405    #[test]
1406    fn test_parse_superscript() {
1407        let result = parse("x^2").unwrap();
1408        assert_eq!(result.len(), 1);
1409        assert_eq!(result[0].type_name(), "supsub");
1410    }
1411
1412    #[test]
1413    fn test_parse_subscript() {
1414        let result = parse("a_i").unwrap();
1415        assert_eq!(result.len(), 1);
1416        assert_eq!(result[0].type_name(), "supsub");
1417    }
1418
1419    #[test]
1420    fn test_parse_supsub() {
1421        let result = parse("x^2_i").unwrap();
1422        assert_eq!(result.len(), 1);
1423        assert_eq!(result[0].type_name(), "supsub");
1424        if let ParseNode::SupSub { sup, sub, .. } = &result[0] {
1425            assert!(sup.is_some());
1426            assert!(sub.is_some());
1427        } else {
1428            panic!("Expected SupSub");
1429        }
1430    }
1431
1432    #[test]
1433    fn test_parse_group() {
1434        let result = parse("{a+b}").unwrap();
1435        assert_eq!(result.len(), 1);
1436        assert_eq!(result[0].type_name(), "ordgroup");
1437    }
1438
1439    #[test]
1440    fn test_parse_frac() {
1441        let result = parse("\\frac{a}{b}").unwrap();
1442        assert_eq!(result.len(), 1);
1443        assert_eq!(result[0].type_name(), "genfrac");
1444    }
1445
1446    #[test]
1447    fn test_parse_sqrt() {
1448        let result = parse("\\sqrt{x}").unwrap();
1449        assert_eq!(result.len(), 1);
1450        assert_eq!(result[0].type_name(), "sqrt");
1451    }
1452
1453    #[test]
1454    fn test_parse_sqrt_optional() {
1455        let result = parse("\\sqrt[3]{x}").unwrap();
1456        assert_eq!(result.len(), 1);
1457        if let ParseNode::Sqrt { index, .. } = &result[0] {
1458            assert!(index.is_some());
1459        } else {
1460            panic!("Expected Sqrt");
1461        }
1462    }
1463
1464    #[test]
1465    fn test_parse_nested() {
1466        let result = parse("\\frac{\\sqrt{a^2+b^2}}{c}").unwrap();
1467        assert_eq!(result.len(), 1);
1468        assert_eq!(result[0].type_name(), "genfrac");
1469    }
1470
1471    #[test]
1472    fn test_parse_empty() {
1473        let result = parse("").unwrap();
1474        assert_eq!(result.len(), 0);
1475    }
1476
1477    #[test]
1478    fn test_parse_double_superscript_error() {
1479        let result = parse("x^2^3");
1480        assert!(result.is_err());
1481    }
1482
1483    #[test]
1484    fn test_parse_unclosed_brace_error() {
1485        let result = parse("{x");
1486        assert!(result.is_err());
1487    }
1488
1489    #[test]
1490    fn test_parse_json_output() {
1491        let result = parse("x^2").unwrap();
1492        let json = serde_json::to_string_pretty(&result).unwrap();
1493        assert!(json.contains("supsub"));
1494    }
1495}