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