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