1use 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
17pub(crate) const DEFAULT_MAX_NESTING_DEPTH: usize = 32;
42
43pub fn parse(input: &str) -> Result<MathNode, ParseError> {
74 parse_with_colors(input).map(|(n, _)| n)
75}
76
77pub 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 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 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
1183fn 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}