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