Skip to main content

latex_rust/parser/
parse.rs

1//! LaTeX math string → [`MathNode`].
2//!
3//! Binding of `_` / `^` / primes is Pratt-style (tight postfix on the nucleus).
4//! The math list itself is a TeX-style row of atoms, not an arithmetic tree.
5
6use super::ast::{
7    AccentKind, AtomKind, ColSpec, DelimSize, Delimiter, EnvRow, EqNumber, IntegralKind, MathNode,
8    MatrixStyle, PhantomKind, SpaceKind, TextStyle,
9};
10use super::preproc::preprocess;
11use super::token::{tokenize, Token};
12use crate::color::{parse_color_spec, Color, ColorTable};
13use crate::dim::Dim;
14use crate::error::{Error, ParseError};
15use crate::symbols::{lookup, SymbolKind as CatalogKind};
16
17/// Deepest nesting [`parse`] and [`layout`](crate::layout()) accept.
18///
19/// Both recurse over the structure of the input, so their stack use grows with
20/// how deeply it nests. Past this depth they return an error rather than
21/// consume unbounded stack: input this deep is pathological, and a crate that
22/// refuses to invent a render should not abort the process either.
23///
24/// The count is of parser recursion levels rather than of LaTeX constructs, and
25/// a braced argument costs two of them (one for the argument and one for the
26/// group), so the limit admits `\frac{1}{…}` nested 15 deep. Real mathematics
27/// rarely nests beyond five levels.
28///
29/// Same name and value as the public constant in latex-rust 2.0.0. It is
30/// crate-private here so that 1.0.5 adds no public API.
31///
32/// Measured on Linux x86-64 with the deepest input the limit admits in each
33/// nesting shape, parsed, laid out, rendered and dropped. An optimised build
34/// needs at most about 512 KiB of stack (31 nested `\left(` or 15 nested
35/// `\binom`), so it is safe on a 1 MiB stack and on the 2 MiB default of a
36/// `std::thread` worker. An unoptimised build needs up to about 4 MiB (31
37/// nested `matrix` environments), which fits the 8 MiB main thread but not a
38/// 2 MiB worker. Input past the limit is refused by the parser within about
39/// 100 KiB of stack in an optimised build. A caller on a smaller stack should
40/// render on a thread it has given more stack.
41pub(crate) const DEFAULT_MAX_NESTING_DEPTH: usize = 32;
42
43/// Parse a LaTeX math string into a [`MathNode`] using a fresh color table.
44///
45/// Accepts raw math, `$...$`, `$$...$$`, `\(...\)`, or `\[...\]`. Binding of
46/// `_` / `^` / primes is Pratt-style (tight postfix on the nucleus).
47///
48/// # Arguments
49///
50/// * `input` — math source, with or without delimiter fences.
51///
52/// # Returns
53///
54/// The typed AST, or a [`ParseError`] naming the problem.
55///
56/// # Errors
57///
58/// * [`ParseError::Unknown`] — command is not in the catalog.
59/// * [`ParseError::Unsupported`] — known construct this crate will not invent.
60/// * [`ParseError::Malformed`] — syntactically invalid input, or input that
61///   nests more than 32 levels deep (`input nests deeper than 32 levels`).
62/// * [`ParseError::UnmatchedDelimiter`] — `\left` without `\right` (or vice versa).
63/// * [`ParseError::TrailingBackslash`] — input ended with a stray `\`.
64///
65/// # Examples
66///
67/// ```
68/// use latex_rust::parse;
69///
70/// let ast = parse(r"\frac{1}{2}").unwrap();
71/// assert_eq!(ast.gold(), r#"(frac (atom Ord "1") (atom Ord "2"))"#);
72/// ```
73pub fn parse(input: &str) -> Result<MathNode, ParseError> {
74    parse_with_colors(input).map(|(n, _)| n)
75}
76
77/// Parse a math string, returning the AST and the color table after `\definecolor`.
78///
79/// # Arguments
80///
81/// * `input` — math source, with or without delimiter fences.
82///
83/// # Returns
84///
85/// The AST and the color table including any `\definecolor` names from `input`.
86///
87/// # Errors
88///
89/// Same as [`parse`].
90///
91/// # Examples
92///
93/// ```
94/// use latex_rust::parse_with_colors;
95///
96/// let (ast, table) = parse_with_colors(r"\definecolor{ok}{named}{red}x").unwrap();
97/// assert!(table.get("ok").is_ok());
98/// let _ = ast;
99/// ```
100pub fn parse_with_colors(input: &str) -> Result<(MathNode, ColorTable), ParseError> {
101    let sanitized = preprocess(input);
102    let tokens = tokenize(&sanitized)?;
103    let tokens = strip_fences(&tokens)?;
104    let mut p = Parser {
105        tokens,
106        pos: 0,
107        depth: 0,
108        colors: ColorTable::new(),
109    };
110    let node = p.parse_list(Stop::eof())?;
111    p.skip_ws();
112    if p.pos < p.tokens.len() {
113        return Err(ParseError::Malformed(format!(
114            "unexpected leftover token {}",
115            p.tokens[p.pos]
116        )));
117    }
118    Ok((node, p.colors))
119}
120
121#[derive(Clone, Copy)]
122struct Stop {
123    end_group: bool,
124    amp: bool,
125    cr: bool,
126    right: bool,
127    end_env: bool,
128    rbracket: bool,
129}
130
131impl Stop {
132    fn eof() -> Self {
133        Self {
134            end_group: false,
135            amp: false,
136            cr: false,
137            right: false,
138            end_env: false,
139            rbracket: false,
140        }
141    }
142
143    fn group() -> Self {
144        Self {
145            end_group: true,
146            ..Self::eof()
147        }
148    }
149
150    fn cell() -> Self {
151        Self {
152            amp: true,
153            cr: true,
154            end_env: true,
155            ..Self::eof()
156        }
157    }
158
159    fn delim() -> Self {
160        Self {
161            right: true,
162            ..Self::eof()
163        }
164    }
165
166    fn index() -> Self {
167        Self {
168            rbracket: true,
169            ..Self::eof()
170        }
171    }
172
173    fn substack_line() -> Self {
174        Self {
175            end_group: true,
176            cr: true,
177            ..Self::eof()
178        }
179    }
180}
181
182struct Parser {
183    tokens: Vec<Token>,
184    pos: usize,
185    colors: ColorTable,
186    /// Current nesting depth, bounded by [`DEFAULT_MAX_NESTING_DEPTH`].
187    depth: usize,
188}
189
190impl Parser {
191    fn skip_ws(&mut self) {
192        while matches!(self.tokens.get(self.pos), Some(Token::Space)) {
193            self.pos += 1;
194        }
195    }
196
197    fn peek(&self) -> Option<&Token> {
198        self.tokens.get(self.pos)
199    }
200
201    fn peek_ws(&mut self) -> Option<&Token> {
202        self.skip_ws();
203        self.peek()
204    }
205
206    fn bump(&mut self) -> Option<Token> {
207        self.skip_ws();
208        self.bump_raw()
209    }
210
211    fn bump_raw(&mut self) -> Option<Token> {
212        let t = self.tokens.get(self.pos).cloned()?;
213        self.pos += 1;
214        Some(t)
215    }
216
217    fn is_stop(&self, tok: &Token, stop: Stop) -> bool {
218        match tok {
219            Token::EndGroup if stop.end_group => true,
220            Token::AlignmentTab if stop.amp => true,
221            Token::Command(s) if s == "\\" && stop.cr => true,
222            Token::Command(s) if s == "cr" && stop.cr => true,
223            Token::Command(s) if s == "right" && stop.right => true,
224            Token::Command(s) if s == "end" && stop.end_env => true,
225            Token::Char(']') if stop.rbracket => true,
226            _ => false,
227        }
228    }
229
230    /// Run `f` one level deeper, refusing to descend past [`DEFAULT_MAX_NESTING_DEPTH`].
231    fn nested<T>(
232        &mut self,
233        f: impl FnOnce(&mut Self) -> Result<T, ParseError>,
234    ) -> Result<T, ParseError> {
235        if self.depth >= DEFAULT_MAX_NESTING_DEPTH {
236            return Err(ParseError::Malformed(format!(
237                "input nests deeper than {DEFAULT_MAX_NESTING_DEPTH} levels"
238            )));
239        }
240        self.depth += 1;
241        let out = f(self);
242        self.depth -= 1;
243        out
244    }
245
246    fn parse_list(&mut self, stop: Stop) -> Result<MathNode, ParseError> {
247        self.nested(|p| p.parse_list_inner(stop))
248    }
249
250    fn parse_list_inner(&mut self, stop: Stop) -> Result<MathNode, ParseError> {
251        let mut items = Vec::new();
252        loop {
253            self.skip_ws();
254            let Some(tok) = self.peek().cloned() else {
255                break;
256            };
257            if self.is_stop(&tok, stop) {
258                break;
259            }
260            match &tok {
261                Token::MathShift | Token::DisplayShift => {
262                    return Err(ParseError::Malformed("unexpected math shift".into()));
263                }
264                Token::Command(n) if n == "color" => {
265                    self.bump();
266                    let c = self.parse_color_from_cmd()?;
267                    let rest = self.parse_list(stop)?;
268                    items.push(MathNode::Color(c, Box::new(rest)));
269                    break;
270                }
271                Token::Command(n) if n == "definecolor" => {
272                    self.bump();
273                    self.parse_definecolor()?;
274                    continue;
275                }
276                _ => {}
277            }
278            items.push(self.parse_atom()?);
279        }
280        Ok(wrap_row(items))
281    }
282
283    fn parse_atom(&mut self) -> Result<MathNode, ParseError> {
284        let mut nucleus = self.parse_nucleus()?;
285        let mut limits = None;
286        loop {
287            match self.peek_ws() {
288                Some(Token::Command(n)) if n == "limits" => {
289                    self.bump();
290                    limits = Some(true);
291                }
292                Some(Token::Command(n)) if n == "nolimits" => {
293                    self.bump();
294                    limits = Some(false);
295                }
296                _ => break,
297            }
298        }
299        if let Some(flag) = limits {
300            if let MathNode::Operator(name, _) = nucleus {
301                nucleus = MathNode::Operator(name, flag);
302            }
303        }
304        self.bind_scripts(nucleus)
305    }
306
307    fn parse_nucleus(&mut self) -> Result<MathNode, ParseError> {
308        self.skip_ws();
309        match self.peek().cloned() {
310            None => Err(ParseError::Malformed("unexpected end of input".into())),
311            Some(Token::Superscript | Token::Subscript | Token::Char('\'')) => {
312                Ok(MathNode::Row(Vec::new()))
313            }
314            Some(Token::BeginGroup) => self.parse_group(),
315            Some(Token::Char(c)) => {
316                self.bump();
317                Ok(MathNode::Atom(c, atom_kind(c)))
318            }
319            Some(Token::Command(name)) => {
320                self.bump();
321                self.parse_command(&name)
322            }
323            Some(other) => Err(ParseError::Malformed(format!("unexpected token {other}"))),
324        }
325    }
326
327    fn parse_group(&mut self) -> Result<MathNode, ParseError> {
328        match self.bump() {
329            Some(Token::BeginGroup) => {}
330            _ => return Err(ParseError::Malformed("expected '{'".into())),
331        }
332        let inner = self.parse_list(Stop::group())?;
333        match self.bump() {
334            Some(Token::EndGroup) => Ok(inner),
335            _ => Err(ParseError::Malformed("unmatched '{'".into())),
336        }
337    }
338
339    fn parse_arg(&mut self) -> Result<MathNode, ParseError> {
340        self.nested(Self::parse_arg_inner)
341    }
342
343    fn parse_arg_inner(&mut self) -> Result<MathNode, ParseError> {
344        self.skip_ws();
345        match self.peek() {
346            Some(Token::BeginGroup) => self.parse_group(),
347            Some(_) => self.parse_nucleus(),
348            None => Err(ParseError::Malformed("missing argument".into())),
349        }
350    }
351
352    fn parse_script(&mut self) -> Result<MathNode, ParseError> {
353        self.parse_arg()
354    }
355
356    fn bind_scripts(&mut self, nucleus: MathNode) -> Result<MathNode, ParseError> {
357        let mut sub: Option<MathNode> = None;
358        let mut sup: Option<MathNode> = None;
359        let mut sup_from_prime = false;
360        loop {
361            match self.peek_ws() {
362                Some(Token::Subscript) => {
363                    self.bump();
364                    if sub.is_some() {
365                        return Err(ParseError::Malformed("double subscript".into()));
366                    }
367                    sub = Some(self.parse_script()?);
368                }
369                Some(Token::Superscript) => {
370                    self.bump();
371                    let s = self.parse_script()?;
372                    if let Some(prev) = sup.take() {
373                        if !sup_from_prime {
374                            return Err(ParseError::Malformed("double superscript".into()));
375                        }
376                        sup = Some(wrap_row(vec![prev, s]));
377                        sup_from_prime = false;
378                    } else {
379                        sup = Some(s);
380                    }
381                }
382                Some(Token::Char('\'')) => {
383                    self.bump();
384                    let prime = MathNode::Atom('′', AtomKind::Ord);
385                    sup = Some(match sup.take() {
386                        None => prime,
387                        Some(prev) => wrap_row(vec![prev, prime]),
388                    });
389                    sup_from_prime = true;
390                }
391                _ => break,
392            }
393        }
394        Ok(apply_scripts(nucleus, sub, sup))
395    }
396
397    fn parse_command(&mut self, name: &str) -> Result<MathNode, ParseError> {
398        match name {
399            "frac" | "dfrac" | "tfrac" | "cfrac" => {
400                let n = self.parse_arg()?;
401                let d = self.parse_arg()?;
402                Ok(MathNode::Fraction(Box::new(n), Box::new(d)))
403            }
404            "binom" | "dbinom" | "tbinom" => {
405                let n = self.parse_arg()?;
406                let k = self.parse_arg()?;
407                Ok(MathNode::Delimited(
408                    Delimiter::Char('('),
409                    Box::new(MathNode::Fraction(Box::new(n), Box::new(k))),
410                    Delimiter::Char(')'),
411                ))
412            }
413            "genfrac" => self.parse_genfrac(),
414            "sqrt" => {
415                let index = if matches!(self.peek_ws(), Some(Token::Char('['))) {
416                    self.bump();
417                    let idx = self.parse_list(Stop::index())?;
418                    match self.bump() {
419                        Some(Token::Char(']')) => {}
420                        _ => {
421                            return Err(ParseError::Malformed(
422                                "expected ']' after \\sqrt index".into(),
423                            ))
424                        }
425                    }
426                    Some(Box::new(idx))
427                } else {
428                    None
429                };
430                let rad = self.parse_arg()?;
431                Ok(MathNode::Radical(index, Box::new(rad)))
432            }
433            "left" => self.parse_delimited(),
434            "right" => Err(ParseError::UnmatchedDelimiter),
435            "begin" => self.parse_begin(),
436            "end" => Err(ParseError::Malformed("unexpected \\end".into())),
437            "over" => Err(ParseError::Malformed("\\over outside a group".into())),
438            "choose" => Err(ParseError::Malformed("\\choose outside a group".into())),
439            "hat" => self.accent(AccentKind::Hat),
440            "check" => self.accent(AccentKind::Check),
441            "breve" => self.accent(AccentKind::Breve),
442            "acute" => self.accent(AccentKind::Acute),
443            "grave" => self.accent(AccentKind::Grave),
444            "tilde" => self.accent(AccentKind::Tilde),
445            "bar" => self.accent(AccentKind::Bar),
446            "vec" => self.accent(AccentKind::Vec),
447            "dot" => self.accent(AccentKind::Dot),
448            "ddot" => self.accent(AccentKind::Ddot),
449            "dddot" => self.accent(AccentKind::Dddot),
450            "ddddot" => self.accent(AccentKind::Ddddot),
451            "widehat" => self.accent(AccentKind::WideHat),
452            "widetilde" => self.accent(AccentKind::WideTilde),
453            "overline" => self.accent(AccentKind::Overline),
454            "underline" => self.accent(AccentKind::Underline),
455            "overbrace" => self.accent(AccentKind::Overbrace),
456            "underbrace" => self.accent(AccentKind::Underbrace),
457            "overleftarrow" => self.accent(AccentKind::Overleftarrow),
458            "overrightarrow" => self.accent(AccentKind::Overrightarrow),
459            "overleftrightarrow" => self.accent(AccentKind::Overleftrightarrow),
460            "underleftarrow" => self.accent(AccentKind::Underleftarrow),
461            "underrightarrow" => self.accent(AccentKind::Underrightarrow),
462            "underleftrightarrow" => self.accent(AccentKind::Underleftrightarrow),
463            "cancel" => self.accent(AccentKind::Cancel),
464            "bcancel" => self.accent(AccentKind::BCancel),
465            "xcancel" => self.accent(AccentKind::XCancel),
466            "boxed" | "fbox" => self.accent(AccentKind::Boxed),
467            "mathring" => self.accent(AccentKind::Ring),
468            "cancelto" => {
469                let value = self.parse_arg()?;
470                let expr = self.parse_arg()?;
471                if is_empty_node(&expr) {
472                    return Err(ParseError::Malformed("empty accent base".into()));
473                }
474                Ok(MathNode::CancelTo(Box::new(value), Box::new(expr)))
475            }
476            "not" => {
477                let body = self.parse_nucleus()?;
478                Ok(MathNode::Accent(Box::new(body), AccentKind::Not))
479            }
480            "overset" => {
481                let over = self.parse_arg()?;
482                let base = self.parse_arg()?;
483                Ok(MathNode::OverUnder(
484                    Box::new(base),
485                    Some(Box::new(over)),
486                    None,
487                ))
488            }
489            "underset" => {
490                let under = self.parse_arg()?;
491                let base = self.parse_arg()?;
492                Ok(MathNode::OverUnder(
493                    Box::new(base),
494                    None,
495                    Some(Box::new(under)),
496                ))
497            }
498            "stackrel" => {
499                let over = self.parse_arg()?;
500                let base = self.parse_arg()?;
501                Ok(MathNode::OverUnder(
502                    Box::new(base),
503                    Some(Box::new(over)),
504                    None,
505                ))
506            }
507            "mathrm" | "textrm" => self.font(TextStyle::Rm),
508            "mathbf" | "textbf" => self.font(TextStyle::Bf),
509            "mathit" | "textit" => self.font(TextStyle::It),
510            "mathsf" | "textsf" => self.font(TextStyle::Sf),
511            "mathtt" | "texttt" => self.font(TextStyle::Tt),
512            "mathbb" => self.font(TextStyle::Bb),
513            "mathcal" => self.font(TextStyle::Cal),
514            "mathfrak" => self.font(TextStyle::Frak),
515            "mathscr" => self.font(TextStyle::Scr),
516            "boldsymbol" => self.font(TextStyle::Boldsymbol),
517            "pmb" => self.font(TextStyle::Pmb),
518            "xrightarrow" => self.parse_xarrow("longrightarrow"),
519            "xleftarrow" => self.parse_xarrow("longleftarrow"),
520            "text" | "mbox" => self.parse_text(TextStyle::Text),
521            "operatorname" => {
522                let name = self.collect_group_text()?;
523                Ok(MathNode::Operator(name, false))
524            }
525            "," => Ok(MathNode::Space(SpaceKind::Thin)),
526            ":" | ">" => Ok(MathNode::Space(SpaceKind::Medium)),
527            ";" => Ok(MathNode::Space(SpaceKind::Thick)),
528            "!" => Ok(MathNode::Space(SpaceKind::NegThin)),
529            "quad" => Ok(MathNode::Space(SpaceKind::Quad)),
530            "qquad" => Ok(MathNode::Space(SpaceKind::Qquad)),
531            " " => Ok(MathNode::Space(SpaceKind::ControlSpace)),
532            "hspace" => {
533                let spec = self.collect_group_text()?;
534                let d = parse_tex_dim(&spec)?;
535                Ok(MathNode::Space(SpaceKind::Hspace(d)))
536            }
537            "phantom" => {
538                let b = self.parse_arg()?;
539                Ok(MathNode::Phantom(PhantomKind::Full, Box::new(b)))
540            }
541            "vphantom" => {
542                let b = self.parse_arg()?;
543                Ok(MathNode::Phantom(PhantomKind::Vertical, Box::new(b)))
544            }
545            "hphantom" => {
546                let b = self.parse_arg()?;
547                Ok(MathNode::Phantom(PhantomKind::Horizontal, Box::new(b)))
548            }
549            "strut" => Ok(MathNode::Strut(Dim::ratio(7, 10), Dim::ratio(3, 10))),
550            "rule" => {
551                let _w = parse_tex_dim(&self.collect_group_text()?)?;
552                let h = parse_tex_dim(&self.collect_group_text()?)?;
553                Ok(MathNode::Strut(h, Dim::zero()))
554            }
555            "textcolor" => {
556                let c = self.parse_color_from_cmd()?;
557                let body = self.parse_arg()?;
558                Ok(MathNode::TextColor(c, Box::new(body)))
559            }
560            "colorbox" => {
561                let c = self.parse_color_from_cmd()?;
562                let body = self.parse_arg()?;
563                Ok(MathNode::ColorBox(c, Box::new(body)))
564            }
565            "fcolorbox" => {
566                let border = self.parse_color_from_cmd()?;
567                let fill = self.parse_color_from_cmd()?;
568                let body = self.parse_arg()?;
569                Ok(MathNode::FColorBox(border, fill, Box::new(body)))
570            }
571            "sum" => Ok(MathNode::Sum(None, None)),
572            "prod" => Ok(MathNode::Product(None, None)),
573            "int" => Ok(MathNode::Integral(IntegralKind::Int, None, None)),
574            "iint" => Ok(MathNode::Integral(IntegralKind::Iint, None, None)),
575            "iiint" => Ok(MathNode::Integral(IntegralKind::Iiint, None, None)),
576            "oint" => Ok(MathNode::Integral(IntegralKind::Oint, None, None)),
577            "oiint" => Ok(MathNode::Integral(IntegralKind::Oiint, None, None)),
578            "lim" => Ok(MathNode::Limit(None)),
579            "sin" | "cos" | "tan" | "cot" | "sec" | "csc" | "arcsin" | "arccos" | "arctan"
580            | "sinh" | "cosh" | "tanh" | "coth" | "log" | "ln" | "lg" | "exp" | "limsup"
581            | "liminf" | "sup" | "inf" | "max" | "min" | "det" | "dim" | "ker" | "deg" | "gcd"
582            | "lcm" | "Pr" | "arg" => Ok(MathNode::Operator(name.to_string(), false)),
583            "coprod" | "bigcup" | "bigcap" | "bigsqcup" | "bigvee" | "bigwedge" | "bigoplus"
584            | "bigotimes" | "biguplus" => Ok(MathNode::Operator(name.to_string(), true)),
585            "big" | "Big" | "bigg" | "Bigg" | "bigl" | "bigr" | "Bigl" | "Bigr" | "biggl"
586            | "biggr" | "Biggl" | "Biggr" | "bigm" | "Bigm" | "biggm" | "Biggm" => {
587                self.parse_sized_delim(name)
588            }
589            "tag" => {
590                let star = matches!(self.peek_ws(), Some(Token::Char('*')));
591                if star {
592                    self.bump();
593                }
594                let body = self.parse_arg()?;
595                Ok(MathNode::Tag {
596                    star,
597                    body: Box::new(body),
598                })
599            }
600            "label" => {
601                let key = self.collect_group_text()?;
602                Ok(MathNode::Label(key))
603            }
604            "ref" => {
605                let key = self.collect_group_text()?;
606                Ok(MathNode::Ref(key))
607            }
608            "nonumber" | "notag" => Ok(MathNode::NoNumber),
609            "hline" => Ok(MathNode::Hline),
610            "intertext" => {
611                let s = self.collect_group_text()?;
612                Ok(MathNode::Intertext(Box::new(MathNode::Text(
613                    s,
614                    TextStyle::Text,
615                ))))
616            }
617            "substack" => self.parse_substack(),
618            "displaystyle" | "textstyle" | "scriptstyle" | "scriptscriptstyle" | "limits"
619            | "nolimits" => self.parse_nucleus(),
620            "{" | "}" => {
621                let c = name.chars().next().unwrap_or('{');
622                Ok(MathNode::Atom(
623                    c,
624                    if name == "{" {
625                        AtomKind::Open
626                    } else {
627                        AtomKind::Close
628                    },
629                ))
630            }
631            "|" => Ok(MathNode::Symbol("Vert".into())),
632            "backslash" => Ok(MathNode::Symbol("backslash".into())),
633            _ => {
634                if name.starts_with("math")
635                    && name.len() > 4
636                    && name.chars().all(|c| c.is_ascii_alphabetic())
637                {
638                    return Err(ParseError::Unsupported(format!("font style {name}")));
639                }
640                if name.starts_with("wide") {
641                    return Err(ParseError::Unsupported(format!("accent {name}")));
642                }
643                self.parse_symbol_or_unknown(name)
644            }
645        }
646    }
647
648    fn accent(&mut self, kind: AccentKind) -> Result<MathNode, ParseError> {
649        let body = self.parse_arg()?;
650        if is_empty_node(&body) {
651            return Err(ParseError::Malformed("empty accent base".into()));
652        }
653        Ok(MathNode::Accent(Box::new(body), kind))
654    }
655
656    fn parse_xarrow(&mut self, arrow: &str) -> Result<MathNode, ParseError> {
657        let under = if matches!(self.peek_ws(), Some(Token::Char('['))) {
658            self.bump();
659            let u = self.parse_list(Stop::index())?;
660            match self.bump() {
661                Some(Token::Char(']')) => Some(Box::new(u)),
662                _ => {
663                    return Err(ParseError::Malformed(
664                        "expected ']' after x-arrow optional argument".into(),
665                    ))
666                }
667            }
668        } else {
669            None
670        };
671        let over = self.parse_arg()?;
672        Ok(MathNode::OverUnder(
673            Box::new(MathNode::Symbol(arrow.to_string())),
674            Some(Box::new(over)),
675            under,
676        ))
677    }
678
679    fn font(&mut self, style: TextStyle) -> Result<MathNode, ParseError> {
680        let inner = self.parse_arg()?;
681        Ok(collapse_text(apply_text_style(inner, style)))
682    }
683
684    fn parse_text(&mut self, style: TextStyle) -> Result<MathNode, ParseError> {
685        let s = self.collect_group_text()?;
686        Ok(MathNode::Text(s, style))
687    }
688
689    fn parse_delimited(&mut self) -> Result<MathNode, ParseError> {
690        let open = self.parse_delimiter()?;
691        let body = self.parse_list(Stop::delim())?;
692        match self.bump() {
693            Some(Token::Command(n)) if n == "right" => {}
694            _ => return Err(ParseError::UnmatchedDelimiter),
695        }
696        let close = self.parse_delimiter()?;
697        Ok(MathNode::Delimited(open, Box::new(body), close))
698    }
699
700    fn parse_sized_delim(&mut self, name: &str) -> Result<MathNode, ParseError> {
701        let size = DelimSize::from_command(name)
702            .ok_or_else(|| ParseError::Malformed(format!("unknown delimiter size \\{name}")))?;
703        let d = self.parse_delimiter()?;
704        let class = DelimSize::class_from_command(name).unwrap_or_else(|| match &d {
705            Delimiter::Char(c) => atom_kind(*c),
706            Delimiter::Named(n) if n == "{" => AtomKind::Open,
707            Delimiter::Named(n) if n == "}" => AtomKind::Close,
708            _ => AtomKind::Open,
709        });
710        Ok(MathNode::SizedDelim(d, size, class))
711    }
712
713    fn parse_delimiter(&mut self) -> Result<Delimiter, ParseError> {
714        self.skip_ws();
715        match self.bump() {
716            Some(Token::Char('.')) => Ok(Delimiter::Empty),
717            Some(Token::Char(c)) if matches!(c, '(' | ')' | '[' | ']' | '|' | '/' | '<' | '>') => {
718                Ok(Delimiter::Char(c))
719            }
720            Some(Token::Command(n)) => match n.as_str() {
721                "." => Ok(Delimiter::Empty),
722                "{" | "}" | "|" => Ok(Delimiter::Named(n)),
723                "langle" | "rangle" | "lfloor" | "rfloor" | "lceil" | "rceil" | "lvert"
724                | "rvert" | "lVert" | "rVert" | "vert" | "Vert" | "uparrow" | "downarrow"
725                | "Uparrow" | "Downarrow" | "updownarrow" | "Updownarrow" | "backslash"
726                | "lgroup" | "rgroup" | "lmoustache" | "rmoustache" => Ok(Delimiter::Named(n)),
727                other => Err(ParseError::Malformed(format!(
728                    "unknown delimiter \\{other}"
729                ))),
730            },
731            Some(other) => Err(ParseError::Malformed(format!(
732                "expected delimiter, found {other}"
733            ))),
734            None => Err(ParseError::Malformed("expected delimiter".into())),
735        }
736    }
737
738    fn parse_begin(&mut self) -> Result<MathNode, ParseError> {
739        let name = self.collect_group_text()?;
740        let colspec = if name == "array" {
741            let preamble = self.collect_group_text()?;
742            parse_colspec(&preamble)?
743        } else {
744            Vec::new()
745        };
746        let style = match name.as_str() {
747            "matrix" => MatrixStyle::Matrix,
748            "pmatrix" => MatrixStyle::Pmatrix,
749            "bmatrix" => MatrixStyle::Bmatrix,
750            "vmatrix" => MatrixStyle::Vmatrix,
751            "Vmatrix" => MatrixStyle::VVmatrix,
752            "Bmatrix" => MatrixStyle::BBmatrix,
753            "cases" => MatrixStyle::Cases,
754            "array" => MatrixStyle::Array,
755            "aligned" => MatrixStyle::Aligned,
756            "align" => MatrixStyle::Align,
757            "gather" => MatrixStyle::Gather,
758            "multline" => MatrixStyle::Multline,
759            "equation" => MatrixStyle::Equation,
760            "split" => MatrixStyle::Split,
761            other => {
762                return Err(ParseError::Unsupported(format!("environment {other}")));
763            }
764        };
765        let rows = self.parse_rows()?;
766        self.expect_end(&name)?;
767        Ok(MathNode::Matrix(style, colspec, rows))
768    }
769
770    fn parse_substack(&mut self) -> Result<MathNode, ParseError> {
771        self.skip_ws();
772        match self.bump() {
773            Some(Token::BeginGroup) => {}
774            _ => {
775                return Err(ParseError::Malformed(
776                    "expected '{' after \\substack".into(),
777                ))
778            }
779        }
780        let mut lines = Vec::new();
781        loop {
782            self.skip_ws();
783            if matches!(self.peek(), Some(Token::EndGroup)) {
784                self.bump();
785                break;
786            }
787            let line = self.parse_list(Stop::substack_line())?;
788            lines.push(line);
789            self.skip_ws();
790            match self.peek() {
791                Some(Token::Command(n)) if n == "\\" || n == "cr" => {
792                    self.bump();
793                }
794                Some(Token::EndGroup) => {
795                    self.bump();
796                    break;
797                }
798                None => return Err(ParseError::Malformed("unmatched '{' in \\substack".into())),
799                Some(other) => {
800                    return Err(ParseError::Malformed(format!(
801                        "unexpected token {other} in \\substack"
802                    )))
803                }
804            }
805        }
806        if lines.is_empty() {
807            return Err(ParseError::Malformed("empty \\substack".into()));
808        }
809        Ok(MathNode::Substack(lines))
810    }
811
812    fn parse_rows(&mut self) -> Result<Vec<EnvRow>, ParseError> {
813        self.skip_ws();
814        if matches!(self.peek(), Some(Token::Command(n)) if n == "end") {
815            return Ok(Vec::new());
816        }
817        let mut rows = Vec::new();
818        loop {
819            self.skip_ws();
820            if matches!(self.peek(), Some(Token::Command(n)) if n == "end") {
821                return Ok(rows);
822            }
823            if matches!(self.peek(), Some(Token::Command(n)) if n == "hline") {
824                self.bump();
825                rows.push(EnvRow::Hline);
826                continue;
827            }
828            if matches!(self.peek(), Some(Token::Command(n)) if n == "intertext") {
829                self.bump();
830                let s = self.collect_group_text()?;
831                rows.push(EnvRow::Intertext(Box::new(MathNode::Text(
832                    s,
833                    TextStyle::Text,
834                ))));
835                continue;
836            }
837            let mut cells = Vec::new();
838            let mut number = EqNumber::Default;
839            let mut labels = Vec::new();
840            loop {
841                let cell = self.parse_list(Stop::cell())?;
842                let cell = peel_row_meta(cell, &mut number, &mut labels);
843                cells.push(cell);
844                self.skip_ws();
845                match self.peek() {
846                    Some(Token::AlignmentTab) => {
847                        self.bump();
848                    }
849                    Some(Token::Command(n)) if n == "\\" || n == "cr" => {
850                        self.bump();
851                        rows.push(finish_env_row(cells, number, labels));
852                        self.skip_ws();
853                        if matches!(self.peek(), Some(Token::Command(e)) if e == "end") {
854                            return Ok(rows);
855                        }
856                        break;
857                    }
858                    Some(Token::Command(n)) if n == "end" => {
859                        rows.push(finish_env_row(cells, number, labels));
860                        return Ok(rows);
861                    }
862                    None => {
863                        return Err(ParseError::Malformed(
864                            "unmatched \\begin (missing \\end)".into(),
865                        ));
866                    }
867                    Some(other) => {
868                        return Err(ParseError::Malformed(format!(
869                            "unexpected token {other} in environment body"
870                        )));
871                    }
872                }
873            }
874        }
875    }
876
877    fn expect_end(&mut self, name: &str) -> Result<(), ParseError> {
878        match self.bump() {
879            Some(Token::Command(n)) if n == "end" => {}
880            _ => return Err(ParseError::Malformed(format!("expected \\end{{{name}}}"))),
881        }
882        let got = self.collect_group_text()?;
883        if got != name {
884            return Err(ParseError::Malformed(format!(
885                "\\begin{{{name}}} closed by \\end{{{got}}}"
886            )));
887        }
888        Ok(())
889    }
890
891    fn parse_genfrac(&mut self) -> Result<MathNode, ParseError> {
892        let ldel = self.collect_group_text()?;
893        let rdel = self.collect_group_text()?;
894        let _thickness = self.collect_group_text()?;
895        let _style = self.collect_group_text()?;
896        let num = self.parse_arg()?;
897        let den = self.parse_arg()?;
898        let frac = MathNode::Fraction(Box::new(num), Box::new(den));
899        if ldel.is_empty() && rdel.is_empty() {
900            return Ok(frac);
901        }
902        Ok(MathNode::Delimited(
903            delim_from_text(&ldel)?,
904            Box::new(frac),
905            delim_from_text(&rdel)?,
906        ))
907    }
908
909    fn parse_color_from_cmd(&mut self) -> Result<Color, ParseError> {
910        self.skip_ws();
911        let model = if matches!(self.peek(), Some(Token::Char('['))) {
912            self.bump();
913            let m = self.collect_until_char(']')?;
914            match self.bump() {
915                Some(Token::Char(']')) => {}
916                _ => {
917                    return Err(ParseError::Malformed(
918                        "expected ']' after color model".into(),
919                    ))
920                }
921            }
922            m
923        } else {
924            "named".into()
925        };
926        let spec = self.collect_group_text()?;
927        parse_color_spec(&model, &spec, Some(&self.colors)).map_err(color_err)
928    }
929
930    fn parse_definecolor(&mut self) -> Result<(), ParseError> {
931        let name = self.collect_group_text()?;
932        let model = self.collect_group_text()?;
933        let spec = self.collect_group_text()?;
934        self.colors
935            .define(&name, &model, &spec)
936            .map_err(color_err)?;
937        Ok(())
938    }
939
940    fn collect_group_text(&mut self) -> Result<String, ParseError> {
941        self.skip_ws();
942        match self.bump_raw() {
943            Some(Token::Space) => {
944                self.pos -= 1;
945                self.skip_ws();
946                return self.collect_group_text();
947            }
948            Some(Token::BeginGroup) => {}
949            _ => return Err(ParseError::Malformed("expected '{'".into())),
950        }
951        let mut s = String::new();
952        let mut depth = 1;
953        while depth > 0 {
954            match self.bump_raw() {
955                None => return Err(ParseError::Malformed("unmatched '{'".into())),
956                Some(Token::BeginGroup) => {
957                    depth += 1;
958                    s.push('{');
959                }
960                Some(Token::EndGroup) => {
961                    depth -= 1;
962                    if depth > 0 {
963                        s.push('}');
964                    }
965                }
966                Some(Token::Space) => s.push(' '),
967                Some(Token::Char(c)) => s.push(c),
968                Some(Token::Command(n)) => {
969                    if n.len() == 1 {
970                        s.push(n.chars().next().unwrap_or('\\'));
971                    } else {
972                        s.push('\\');
973                        s.push_str(&n);
974                    }
975                }
976                Some(other) => {
977                    return Err(ParseError::Malformed(format!(
978                        "unexpected token {other} in group text"
979                    )))
980                }
981            }
982        }
983        Ok(s)
984    }
985
986    fn collect_until_char(&mut self, end: char) -> Result<String, ParseError> {
987        let mut s = String::new();
988        loop {
989            match self.peek() {
990                None => return Err(ParseError::Malformed(format!("expected '{end}'"))),
991                Some(Token::Char(c)) if *c == end => break,
992                Some(Token::Char(c)) => {
993                    s.push(*c);
994                    self.bump_raw();
995                }
996                Some(Token::Space) => {
997                    s.push(' ');
998                    self.bump_raw();
999                }
1000                Some(other) => {
1001                    return Err(ParseError::Malformed(format!(
1002                        "unexpected token {other} in optional argument"
1003                    )))
1004                }
1005            }
1006        }
1007        Ok(s)
1008    }
1009
1010    fn parse_symbol_or_unknown(&self, name: &str) -> Result<MathNode, ParseError> {
1011        let canon = alias(name);
1012        if let Some(e) = lookup(canon).or_else(|| lookup(name)) {
1013            match e.kind {
1014                CatalogKind::Container | CatalogKind::Modifier => {
1015                    return Err(ParseError::Unsupported(format!("\\{name}")));
1016                }
1017                CatalogKind::Symbol | CatalogKind::Operator => {
1018                    return Ok(MathNode::Symbol(canon.to_string()));
1019                }
1020            }
1021        }
1022        if is_extra_symbol(canon) {
1023            return Ok(MathNode::Symbol(canon.to_string()));
1024        }
1025        Err(ParseError::Unknown(format!("\\{name}")))
1026    }
1027}
1028
1029fn strip_fences(tokens: &[Token]) -> Result<Vec<Token>, ParseError> {
1030    let t = trim_spaces(tokens);
1031    if t.len() >= 2 {
1032        let inner = match (&t[0], &t[t.len() - 1]) {
1033            (Token::MathShift, Token::MathShift) => Some(&t[1..t.len() - 1]),
1034            (Token::DisplayShift, Token::DisplayShift) => Some(&t[1..t.len() - 1]),
1035            (Token::Command(a), Token::Command(b)) if a == "[" && b == "]" => {
1036                Some(&t[1..t.len() - 1])
1037            }
1038            (Token::Command(a), Token::Command(b)) if a == "(" && b == ")" => {
1039                Some(&t[1..t.len() - 1])
1040            }
1041            _ => None,
1042        };
1043        if let Some(inner) = inner {
1044            return Ok(trim_spaces(inner).to_vec());
1045        }
1046        if matches!(t[0], Token::MathShift | Token::DisplayShift)
1047            || matches!(&t[0], Token::Command(s) if s == "[" || s == "(")
1048        {
1049            return Err(ParseError::Malformed("unmatched math delimiter".into()));
1050        }
1051    }
1052    Ok(t.to_vec())
1053}
1054
1055fn trim_spaces(tokens: &[Token]) -> &[Token] {
1056    let mut a = 0;
1057    let mut b = tokens.len();
1058    while a < b && tokens[a] == Token::Space {
1059        a += 1;
1060    }
1061    while b > a && tokens[b - 1] == Token::Space {
1062        b -= 1;
1063    }
1064    &tokens[a..b]
1065}
1066
1067fn wrap_row(mut items: Vec<MathNode>) -> MathNode {
1068    if items.len() == 1 {
1069        items.remove(0)
1070    } else {
1071        MathNode::Row(items)
1072    }
1073}
1074
1075fn parse_colspec(s: &str) -> Result<Vec<ColSpec>, ParseError> {
1076    let mut out = Vec::new();
1077    for c in s.chars() {
1078        match c {
1079            'l' => out.push(ColSpec::Left),
1080            'c' => out.push(ColSpec::Center),
1081            'r' => out.push(ColSpec::Right),
1082            '|' => out.push(ColSpec::VRule),
1083            ' ' | '\t' => {}
1084            '@' | '!' | '>' | '<' | 'p' | 'm' | 'b' | '*' => {
1085                return Err(ParseError::Unsupported(format!("array preamble `{c}`")))
1086            }
1087            other => return Err(ParseError::Malformed(format!("array preamble `{other}`"))),
1088        }
1089    }
1090    if out.is_empty() {
1091        return Err(ParseError::Malformed("empty array preamble".into()));
1092    }
1093    Ok(out)
1094}
1095
1096fn peel_row_meta(node: MathNode, number: &mut EqNumber, labels: &mut Vec<String>) -> MathNode {
1097    match node {
1098        MathNode::NoNumber => {
1099            *number = EqNumber::Suppress;
1100            MathNode::Row(Vec::new())
1101        }
1102        MathNode::Tag { star, body } => {
1103            *number = EqNumber::Tag { star, body };
1104            MathNode::Row(Vec::new())
1105        }
1106        MathNode::Label(k) => {
1107            labels.push(k);
1108            MathNode::Row(Vec::new())
1109        }
1110        MathNode::Hline => MathNode::Hline,
1111        MathNode::Intertext(n) => MathNode::Intertext(n),
1112        MathNode::Row(items) => {
1113            let mut kept = Vec::new();
1114            for it in items {
1115                let p = peel_row_meta(it, number, labels);
1116                if !is_empty_node(&p) {
1117                    kept.push(p);
1118                }
1119            }
1120            wrap_row(kept)
1121        }
1122        other => other,
1123    }
1124}
1125
1126fn finish_env_row(cells: Vec<MathNode>, number: EqNumber, labels: Vec<String>) -> EnvRow {
1127    if cells.len() == 1 && matches!(cells[0], MathNode::Hline) {
1128        return EnvRow::Hline;
1129    }
1130    if cells.len() == 1 {
1131        if let MathNode::Intertext(n) = &cells[0] {
1132            return EnvRow::Intertext(n.clone());
1133        }
1134    }
1135    EnvRow::Cells {
1136        cells,
1137        number,
1138        labels,
1139    }
1140}
1141
1142fn is_empty_node(n: &MathNode) -> bool {
1143    match n {
1144        MathNode::Row(v) => v.is_empty() || v.iter().all(is_empty_node),
1145        MathNode::Space(_) | MathNode::NoNumber | MathNode::Label(_) => true,
1146        _ => false,
1147    }
1148}
1149
1150fn apply_scripts(nucleus: MathNode, sub: Option<MathNode>, sup: Option<MathNode>) -> MathNode {
1151    match nucleus {
1152        MathNode::Sum(None, None) => MathNode::Sum(sub.map(Box::new), sup.map(Box::new)),
1153        MathNode::Product(None, None) => MathNode::Product(sub.map(Box::new), sup.map(Box::new)),
1154        MathNode::Integral(k, None, None) => {
1155            MathNode::Integral(k, sub.map(Box::new), sup.map(Box::new))
1156        }
1157        MathNode::Limit(None) => {
1158            let lim = MathNode::Limit(sub.map(Box::new));
1159            match sup {
1160                Some(s) => MathNode::Superscript(Box::new(lim), Box::new(s)),
1161                None => lim,
1162            }
1163        }
1164        MathNode::Accent(b, k @ (AccentKind::Overbrace | AccentKind::Underbrace)) => {
1165            match (sub, sup) {
1166                (None, None) => MathNode::Accent(b, k),
1167                (s, e) => MathNode::OverUnder(
1168                    Box::new(MathNode::Accent(b, k)),
1169                    e.map(Box::new),
1170                    s.map(Box::new),
1171                ),
1172            }
1173        }
1174        other => match (sub, sup) {
1175            (None, None) => other,
1176            (Some(s), None) => MathNode::Subscript(Box::new(other), Box::new(s)),
1177            (None, Some(e)) => MathNode::Superscript(Box::new(other), Box::new(e)),
1178            (Some(s), Some(e)) => MathNode::SubSup(Box::new(other), Box::new(s), Box::new(e)),
1179        },
1180    }
1181}
1182
1183/// Apply a math font style to a whole subformula.
1184///
1185/// A LaTeX font command such as `\mathrm` switches the font for everything in
1186/// its argument, not only for the characters at the top of the argument's list,
1187/// so this descends into scripts, fractions, radicals, accents and delimited
1188/// bodies as well as rows. Delimiters themselves are `Delimiter` values rather
1189/// than nodes and are left alone, as they are in LaTeX.
1190fn apply_text_style(node: MathNode, style: TextStyle) -> MathNode {
1191    match node {
1192        MathNode::Atom(c, _) if crate::style_map::is_stylable(c) => {
1193            MathNode::Text(c.to_string(), style)
1194        }
1195        MathNode::Text(s, _) => MathNode::Text(s, style),
1196        MathNode::Symbol(name) => {
1197            if let Some(ch) = crate::symbols::glyph_char(&name) {
1198                if crate::style_map::is_stylable(ch) {
1199                    MathNode::Text(ch.to_string(), style)
1200                } else {
1201                    MathNode::Symbol(name)
1202                }
1203            } else {
1204                MathNode::Symbol(name)
1205            }
1206        }
1207        MathNode::Row(v) => collapse_text(MathNode::Row(
1208            v.into_iter().map(|n| apply_text_style(n, style)).collect(),
1209        )),
1210        MathNode::Substack(v) => {
1211            MathNode::Substack(v.into_iter().map(|n| apply_text_style(n, style)).collect())
1212        }
1213        MathNode::Superscript(b, sup) => MathNode::Superscript(
1214            Box::new(apply_text_style(*b, style)),
1215            Box::new(apply_text_style(*sup, style)),
1216        ),
1217        MathNode::Subscript(b, sub) => MathNode::Subscript(
1218            Box::new(apply_text_style(*b, style)),
1219            Box::new(apply_text_style(*sub, style)),
1220        ),
1221        MathNode::SubSup(b, sub, sup) => MathNode::SubSup(
1222            Box::new(apply_text_style(*b, style)),
1223            Box::new(apply_text_style(*sub, style)),
1224            Box::new(apply_text_style(*sup, style)),
1225        ),
1226        MathNode::Fraction(num, den) => MathNode::Fraction(
1227            Box::new(apply_text_style(*num, style)),
1228            Box::new(apply_text_style(*den, style)),
1229        ),
1230        MathNode::Radical(index, body) => MathNode::Radical(
1231            index.map(|i| Box::new(apply_text_style(*i, style))),
1232            Box::new(apply_text_style(*body, style)),
1233        ),
1234        MathNode::Accent(body, kind) => {
1235            MathNode::Accent(Box::new(apply_text_style(*body, style)), kind)
1236        }
1237        MathNode::Delimited(open, body, close) => {
1238            MathNode::Delimited(open, Box::new(apply_text_style(*body, style)), close)
1239        }
1240        other => other,
1241    }
1242}
1243
1244fn collapse_text(node: MathNode) -> MathNode {
1245    let MathNode::Row(v) = node else {
1246        return node;
1247    };
1248    let mut out: Vec<MathNode> = Vec::new();
1249    for n in v {
1250        match (out.last_mut(), &n) {
1251            (Some(MathNode::Text(a, sa)), MathNode::Text(b, sb)) if sa == sb => {
1252                a.push_str(b);
1253            }
1254            _ => out.push(n),
1255        }
1256    }
1257    wrap_row(out)
1258}
1259
1260fn atom_kind(c: char) -> AtomKind {
1261    match c {
1262        '+' | '-' | '*' | '±' | '∓' | '·' | '×' | '÷' => AtomKind::Bin,
1263        '=' | '<' | '>' | '≠' | '≤' | '≥' | '≈' | '≡' => AtomKind::Rel,
1264        '(' | '[' | '{' => AtomKind::Open,
1265        ')' | ']' | '}' => AtomKind::Close,
1266        ',' | ';' | '!' | '?' | ':' => AtomKind::Punct,
1267        _ => AtomKind::Ord,
1268    }
1269}
1270
1271fn delim_from_text(s: &str) -> Result<Delimiter, ParseError> {
1272    let s = s.trim();
1273    if s.is_empty() || s == "." {
1274        return Ok(Delimiter::Empty);
1275    }
1276    if s.chars().count() == 1 {
1277        let c = s.chars().next().unwrap();
1278        return Ok(Delimiter::Char(c));
1279    }
1280    let name = s.strip_prefix('\\').unwrap_or(s);
1281    Ok(Delimiter::Named(name.to_string()))
1282}
1283
1284fn parse_tex_dim(s: &str) -> Result<Dim, ParseError> {
1285    let s = s.trim();
1286    if s.is_empty() {
1287        return Err(ParseError::Malformed("empty dimension".into()));
1288    }
1289    let mut i = 0;
1290    let b = s.as_bytes();
1291    if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
1292        i += 1;
1293    }
1294    while i < b.len() && (b[i].is_ascii_digit() || b[i] == b'.') {
1295        i += 1;
1296    }
1297    if i == 0 || (i == 1 && (b[0] == b'+' || b[0] == b'-')) {
1298        return Err(ParseError::Malformed(format!("invalid dimension `{s}`")));
1299    }
1300    let num = Dim::parse(&s[..i]);
1301    let unit = s[i..].trim();
1302    match unit {
1303        "" | "em" => Ok(num),
1304        "mu" => Ok(Dim::from_mu(&num)),
1305        "pt" | "bp" => Ok(num / Dim::from_i64(10)),
1306        other => Err(ParseError::Unsupported(format!("dimension unit {other}"))),
1307    }
1308}
1309
1310fn color_err(e: Error) -> ParseError {
1311    match e {
1312        Error::Unsupported { what } => ParseError::Unsupported(what),
1313        Error::Parse(p) => p,
1314        Error::Malformed { what } => ParseError::Malformed(what),
1315        Error::InvalidOption { what } => ParseError::Malformed(what),
1316        other => ParseError::Malformed(other.to_string()),
1317    }
1318}
1319
1320fn alias(name: &str) -> &str {
1321    match name {
1322        "le" => "leq",
1323        "ge" => "geq",
1324        "ne" => "neq",
1325        "dots" => "ldots",
1326        "lnot" => "neg",
1327        "dag" => "dagger",
1328        "ddag" => "ddagger",
1329        "owns" => "ni",
1330        _ => name,
1331    }
1332}
1333
1334fn is_extra_symbol(name: &str) -> bool {
1335    matches!(
1336        name,
1337        "Gamma"
1338            | "Delta"
1339            | "Theta"
1340            | "Lambda"
1341            | "Xi"
1342            | "Pi"
1343            | "Sigma"
1344            | "Upsilon"
1345            | "Phi"
1346            | "Psi"
1347            | "Omega"
1348            | "varepsilon"
1349            | "vartheta"
1350            | "varpi"
1351            | "varrho"
1352            | "varsigma"
1353            | "varphi"
1354            | "ldots"
1355            | "cdots"
1356            | "vdots"
1357            | "ddots"
1358            | "colon"
1359            | "mid"
1360            | "lvert"
1361            | "rvert"
1362            | "lVert"
1363            | "rVert"
1364            | "vert"
1365            | "Vert"
1366            | "implies"
1367            | "iff"
1368            | "to"
1369            | "gets"
1370            | "neq"
1371            | "leq"
1372            | "geq"
1373    )
1374}
1375
1376#[cfg(test)]
1377mod tests {
1378    use super::*;
1379
1380    #[test]
1381    fn frac_gold() {
1382        let n = parse(r"\frac{1}{2}").unwrap();
1383        assert_eq!(n.gold(), r#"(frac (atom Ord "1") (atom Ord "2"))"#);
1384    }
1385}