1use crate::parser::inlines::bookdown::try_parse_bookdown_equation_definition;
26use crate::syntax::SyntaxKind;
27use rowan::{GreenNode, GreenNodeBuilder};
28
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33pub struct MathParseOptions {
34 pub bookdown_equation_labels: bool,
38}
39
40pub fn parse_math_content(content: &str, opts: MathParseOptions) -> GreenNode {
44 let mut parser = MathParser {
45 input: content,
46 pos: 0,
47 builder: GreenNodeBuilder::new(),
48 opts,
49 };
50 parser.builder.start_node(SyntaxKind::MATH_CONTENT.into());
51 parser.parse_elements(Ctx::Top);
52 parser.builder.finish_node();
53 parser.builder.finish()
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58enum Ctx {
59 Top,
61 Group,
63 Env,
65 LeftRight,
67}
68
69struct MathParser<'a> {
70 input: &'a str,
71 pos: usize,
72 builder: GreenNodeBuilder<'static>,
73 opts: MathParseOptions,
74}
75
76impl MathParser<'_> {
77 fn rest(&self) -> &str {
78 &self.input[self.pos..]
79 }
80
81 fn peek_char(&self) -> Option<char> {
82 self.rest().chars().next()
83 }
84
85 fn bump_bytes(&mut self, len: usize, kind: SyntaxKind) {
87 let text = &self.input[self.pos..self.pos + len];
88 self.builder.token(kind.into(), text);
89 self.pos += len;
90 }
91
92 fn peek_control_word(&self) -> Option<&str> {
96 let after = self.rest().strip_prefix('\\')?;
97 let len: usize = after
98 .bytes()
99 .take_while(|b| b.is_ascii_alphabetic() || *b == b'@')
100 .count();
101 if len == 0 { None } else { Some(&after[..len]) }
102 }
103
104 fn parse_elements(&mut self, ctx: Ctx) {
105 while let Some(c) = self.peek_char() {
106 match c {
107 '}' if ctx == Ctx::Group => break,
108 '}' => self.bump_bytes(1, SyntaxKind::MATH_GROUP_CLOSE),
112 '\\' => {
113 if self.rest().starts_with("\\\\") {
114 self.bump_bytes(2, SyntaxKind::MATH_LINE_BREAK);
115 } else if let Some(word) = self.peek_control_word() {
116 match word {
117 "begin" => self.parse_environment(),
118 "end" if ctx == Ctx::Env => break,
119 "end" => {
120 self.parse_control_word();
124 }
125 "left" => self.parse_delimited(),
126 "right" if ctx == Ctx::LeftRight => break,
127 "right" => {
128 self.parse_control_word();
132 }
133 _ => self.parse_control_word(),
134 }
135 } else {
136 self.parse_control_symbol();
137 }
138 }
139 '{' => self.parse_group(),
140 '(' if self.opts.bookdown_equation_labels => match self.equation_label_len() {
143 Some(len) => self.bump_bytes(len, SyntaxKind::MATH_EQUATION_LABEL),
144 None => self.bump_bytes(1, SyntaxKind::MATH_OPEN),
145 },
146 '(' | '[' => self.bump_bytes(1, SyntaxKind::MATH_OPEN),
150 ')' | ']' => self.bump_bytes(1, SyntaxKind::MATH_CLOSE),
151 ',' | ';' => self.bump_bytes(1, SyntaxKind::MATH_PUNCT),
152 '&' => self.bump_bytes(1, SyntaxKind::MATH_ALIGN),
153 '^' | '_' => self.bump_bytes(1, SyntaxKind::MATH_SCRIPT),
154 c if is_operator(c) => self.bump_bytes(1, SyntaxKind::MATH_OPERATOR),
159 '%' => self.parse_comment(),
160 ' ' | '\t' => self.parse_spaces(),
161 '\n' => self.bump_bytes(1, SyntaxKind::MATH_NEWLINE),
162 '\r' => {
163 let len = if self.rest().starts_with("\r\n") {
164 2
165 } else {
166 1
167 };
168 self.bump_bytes(len, SyntaxKind::MATH_NEWLINE);
169 }
170 _ => self.parse_text(),
171 }
172 }
173 }
174
175 fn parse_environment(&mut self) {
180 self.builder.start_node(SyntaxKind::MATH_ENVIRONMENT.into());
181 self.parse_control_word(); self.parse_environment_name(); self.parse_elements(Ctx::Env);
184 if self.peek_control_word() == Some("end") {
185 self.parse_control_word(); self.parse_environment_name(); }
188 self.builder.finish_node();
189 }
190
191 fn parse_environment_name(&mut self) {
195 if self.peek_char() == Some('{') {
196 self.parse_group();
197 }
198 }
199
200 fn parse_group(&mut self) {
201 self.builder.start_node(SyntaxKind::MATH_GROUP.into());
202 self.bump_bytes(1, SyntaxKind::MATH_GROUP_OPEN); self.parse_elements(Ctx::Group);
204 if self.peek_char() == Some('}') {
205 self.bump_bytes(1, SyntaxKind::MATH_GROUP_CLOSE); }
207 self.builder.finish_node();
210 }
211
212 fn parse_delimited(&mut self) {
219 self.builder.start_node(SyntaxKind::MATH_DELIMITED.into());
220 self.parse_control_word(); self.consume_delimiter(); self.parse_elements(Ctx::LeftRight);
223 if self.peek_control_word() == Some("right") {
224 self.parse_control_word(); self.consume_delimiter(); }
227 self.builder.finish_node();
228 }
229
230 fn consume_delimiter(&mut self) {
238 match self.peek_char() {
239 Some('(' | '[') => self.bump_bytes(1, SyntaxKind::MATH_OPEN),
240 Some(')' | ']') => self.bump_bytes(1, SyntaxKind::MATH_CLOSE),
241 Some('.' | '|' | '/') => self.bump_bytes(1, SyntaxKind::MATH_TEXT),
242 Some('\\') => {
243 if self.peek_control_word().is_some() {
244 self.parse_control_word();
245 } else {
246 self.parse_control_symbol();
247 }
248 }
249 _ => {}
250 }
251 }
252
253 fn parse_control_word(&mut self) {
255 let word_len = self.peek_control_word().map(str::len).unwrap_or(0);
256 self.bump_bytes(1 + word_len, SyntaxKind::MATH_COMMAND);
257 }
258
259 fn parse_control_symbol(&mut self) {
262 let after = &self.input[self.pos + 1..];
263 let len = 1 + after.chars().next().map(char::len_utf8).unwrap_or(0);
264 self.bump_bytes(len, SyntaxKind::MATH_COMMAND);
265 }
266
267 fn parse_comment(&mut self) {
269 let len = self
270 .rest()
271 .find(['\n', '\r'])
272 .unwrap_or_else(|| self.rest().len());
273 self.bump_bytes(len, SyntaxKind::MATH_COMMENT);
274 }
275
276 fn parse_spaces(&mut self) {
277 let len = self
278 .rest()
279 .bytes()
280 .take_while(|&b| b == b' ' || b == b'\t')
281 .count();
282 self.bump_bytes(len, SyntaxKind::MATH_SPACE);
283 }
284
285 fn parse_text(&mut self) {
290 let len = self
291 .rest()
292 .find(|c: char| is_special(c))
293 .unwrap_or_else(|| self.rest().len());
294 debug_assert!(len > 0, "parse_text on a special char");
295 self.bump_bytes(len, SyntaxKind::MATH_TEXT);
296 }
297
298 fn equation_label_len(&self) -> Option<usize> {
302 try_parse_bookdown_equation_definition(self.rest()).map(|(len, _)| len)
303 }
304}
305
306fn is_special(c: char) -> bool {
308 is_operator(c)
309 || is_delimiter(c)
310 || matches!(
311 c,
312 '\\' | '{' | '}' | '&' | '^' | '_' | '%' | ' ' | '\t' | '\n' | '\r'
313 )
314}
315
316fn is_delimiter(c: char) -> bool {
321 matches!(c, '(' | ')' | '[' | ']' | ',' | ';')
322}
323
324fn is_operator(c: char) -> bool {
328 matches!(c, '+' | '-' | '*' | '=' | '<' | '>')
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::syntax::SyntaxNode;
335
336 fn node(content: &str) -> SyntaxNode {
337 SyntaxNode::new_root(parse_math_content(content, MathParseOptions::default()))
338 }
339
340 fn node_with(content: &str, opts: MathParseOptions) -> SyntaxNode {
341 SyntaxNode::new_root(parse_math_content(content, opts))
342 }
343
344 fn token_kinds(content: &str) -> Vec<SyntaxKind> {
345 node(content)
346 .descendants_with_tokens()
347 .filter_map(|el| el.into_token())
348 .map(|tok| tok.kind())
349 .collect()
350 }
351
352 fn assert_lossless(content: &str) {
354 assert_eq!(
355 node(content).text().to_string(),
356 content,
357 "roundtrip: {content:?}"
358 );
359 }
360
361 #[test]
362 fn root_is_math_content() {
363 assert_eq!(node("x").kind(), SyntaxKind::MATH_CONTENT);
364 }
365
366 #[test]
367 fn plain_text_is_one_atom_run() {
368 assert_eq!(token_kinds("abc"), vec![SyntaxKind::MATH_TEXT]);
370 assert_lossless("abc");
371 assert_eq!(
374 token_kinds("f(x)/2.5"),
375 vec![
376 SyntaxKind::MATH_TEXT, SyntaxKind::MATH_OPEN, SyntaxKind::MATH_TEXT, SyntaxKind::MATH_CLOSE, SyntaxKind::MATH_TEXT, ]
382 );
383 assert_lossless("f(x)/2.5");
384 }
385
386 #[test]
387 fn delimiters_and_punctuation_split_atom_runs() {
388 assert_eq!(
391 token_kinds("[a,b);"),
392 vec![
393 SyntaxKind::MATH_OPEN, SyntaxKind::MATH_TEXT, SyntaxKind::MATH_PUNCT, SyntaxKind::MATH_TEXT, SyntaxKind::MATH_CLOSE, SyntaxKind::MATH_PUNCT, ]
400 );
401 assert_lossless("[a,b);");
402 assert_eq!(token_kinds("a|b.c/d"), vec![SyntaxKind::MATH_TEXT]);
404 assert_lossless("a|b.c/d");
405 assert_eq!(token_kinds(r"\(\)\[\]"), vec![SyntaxKind::MATH_COMMAND; 4]);
407 assert_lossless(r"\(\)\[\]");
408 }
409
410 #[test]
411 fn operators_split_atom_runs() {
412 assert_eq!(
415 token_kinds("a+b=c"),
416 vec![
417 SyntaxKind::MATH_TEXT, SyntaxKind::MATH_OPERATOR, SyntaxKind::MATH_TEXT, SyntaxKind::MATH_OPERATOR, SyntaxKind::MATH_TEXT, ]
423 );
424 assert_lossless("a+b=c");
425 }
426
427 #[test]
428 fn each_operator_char_is_its_own_token() {
429 for op in ["+", "-", "*", "=", "<", ">"] {
430 assert_eq!(
431 token_kinds(op),
432 vec![SyntaxKind::MATH_OPERATOR],
433 "operator {op:?}"
434 );
435 assert_lossless(op);
436 }
437 assert_eq!(
439 token_kinds("a<=b"),
440 vec![
441 SyntaxKind::MATH_TEXT,
442 SyntaxKind::MATH_OPERATOR, SyntaxKind::MATH_OPERATOR, SyntaxKind::MATH_TEXT,
445 ]
446 );
447 assert_eq!(
449 token_kinds("-x"),
450 vec![SyntaxKind::MATH_OPERATOR, SyntaxKind::MATH_TEXT]
451 );
452 assert_lossless("-x");
453 assert_eq!(token_kinds(r"\<"), vec![SyntaxKind::MATH_COMMAND]);
455 assert_lossless(r"\<");
456 }
457
458 #[test]
459 fn operators_inside_groups_and_scripts_are_lossless() {
460 for content in [r"e^{-x}", r"10^{-3}", r"\frac{a+b}{c-d}", r"x_{i+1}"] {
461 assert_lossless(content);
462 }
463 }
464
465 #[test]
466 fn control_word_and_symbol() {
467 assert_eq!(
468 token_kinds(r"\alpha\,"),
469 vec![SyntaxKind::MATH_COMMAND, SyntaxKind::MATH_COMMAND]
470 );
471 assert_lossless(r"\alpha\,");
472 assert_eq!(token_kinds(r"\&\%\{\}"), vec![SyntaxKind::MATH_COMMAND; 4]);
474 assert_lossless(r"\&\%\{\}");
475 }
476
477 #[test]
478 fn brace_group_nests() {
479 let tree = node(r"x^{2}");
480 let group = tree
481 .descendants()
482 .find(|n| n.kind() == SyntaxKind::MATH_GROUP)
483 .expect("group");
484 let kinds: Vec<_> = group.children_with_tokens().map(|el| el.kind()).collect();
485 assert_eq!(
486 kinds,
487 vec![
488 SyntaxKind::MATH_GROUP_OPEN,
489 SyntaxKind::MATH_TEXT,
490 SyntaxKind::MATH_GROUP_CLOSE
491 ]
492 );
493 assert_lossless(r"x^{2}");
494 }
495
496 #[test]
497 fn line_break_alignment_and_scripts() {
498 assert_eq!(
499 token_kinds(r"x &= 1 \\"),
500 vec![
501 SyntaxKind::MATH_TEXT, SyntaxKind::MATH_SPACE, SyntaxKind::MATH_ALIGN, SyntaxKind::MATH_OPERATOR, SyntaxKind::MATH_SPACE, SyntaxKind::MATH_TEXT, SyntaxKind::MATH_SPACE, SyntaxKind::MATH_LINE_BREAK, ]
510 );
511 assert_lossless(r"x &= 1 \\");
512 assert_eq!(
513 token_kinds("x^2_i"),
514 vec![
515 SyntaxKind::MATH_TEXT,
516 SyntaxKind::MATH_SCRIPT,
517 SyntaxKind::MATH_TEXT,
518 SyntaxKind::MATH_SCRIPT,
519 SyntaxKind::MATH_TEXT,
520 ]
521 );
522 }
523
524 #[test]
525 fn environment_wraps_body() {
526 let content = "\\begin{aligned}\nx &= 1\n\\end{aligned}";
527 let tree = node(content);
528 let env = tree
529 .descendants()
530 .find(|n| n.kind() == SyntaxKind::MATH_ENVIRONMENT)
531 .expect("environment");
532 assert_eq!(env.text().to_string(), content);
533 let commands = env
534 .children_with_tokens()
535 .filter(|el| el.kind() == SyntaxKind::MATH_COMMAND)
536 .count();
537 assert_eq!(commands, 2);
538 assert_lossless(content);
539 }
540
541 #[test]
542 fn nested_environments() {
543 let content = r"\begin{a}\begin{b}x\end{b}\end{a}";
544 let envs = node(content)
545 .descendants()
546 .filter(|n| n.kind() == SyntaxKind::MATH_ENVIRONMENT)
547 .count();
548 assert_eq!(envs, 2);
549 assert_lossless(content);
550 }
551
552 #[test]
553 fn comment_runs_to_end_of_line() {
554 assert_eq!(
555 token_kinds("a % tail\nb"),
556 vec![
557 SyntaxKind::MATH_TEXT,
558 SyntaxKind::MATH_SPACE,
559 SyntaxKind::MATH_COMMENT,
560 SyntaxKind::MATH_NEWLINE,
561 SyntaxKind::MATH_TEXT,
562 ]
563 );
564 assert_lossless("a % tail\nb");
565 }
566
567 #[test]
568 fn crlf_and_unicode_are_lossless() {
569 assert_lossless("x &= 1\r\ny &= 2\r\n");
570 assert_lossless(r"\alpha + \beta \neq \gamma_{\text{αβγ}}");
571 }
572
573 #[test]
574 fn empty_content() {
575 assert_eq!(node("").text().to_string(), "");
576 assert!(token_kinds("").is_empty());
577 }
578
579 #[test]
580 fn trailing_backslash() {
581 assert_eq!(
582 token_kinds("a\\"),
583 vec![SyntaxKind::MATH_TEXT, SyntaxKind::MATH_COMMAND]
584 );
585 assert_lossless("a\\");
586 }
587
588 #[test]
591 fn malformed_math_is_still_lossless() {
592 for content in [
593 "{a",
594 "a}b",
595 r"\begin{aligned} x &= 1",
596 r"\begin{aligned}x\end{matrix}",
597 r"x \end{aligned}",
598 ] {
599 assert_lossless(content);
600 }
601 }
602
603 fn delimited_count(content: &str) -> usize {
606 node(content)
607 .descendants()
608 .filter(|n| n.kind() == SyntaxKind::MATH_DELIMITED)
609 .count()
610 }
611
612 #[test]
613 fn left_right_wraps_a_delimited_node() {
614 let content = r"\left( x + y \right)";
615 let tree = node(content);
616 let delim = tree
617 .descendants()
618 .find(|n| n.kind() == SyntaxKind::MATH_DELIMITED)
619 .expect("delimited node");
620 assert_eq!(delim.text().to_string(), content);
621 let commands: Vec<String> = delim
623 .children_with_tokens()
624 .filter_map(|el| el.into_token())
625 .filter(|t| t.kind() == SyntaxKind::MATH_COMMAND)
626 .map(|t| t.text().to_string())
627 .collect();
628 assert_eq!(commands, vec![r"\left", r"\right"]);
629 assert_lossless(content);
630 }
631
632 #[test]
633 fn left_right_delimiters_keep_their_token_kinds() {
634 assert_eq!(
636 token_kinds(r"\left(x\right)"),
637 vec![
638 SyntaxKind::MATH_COMMAND, SyntaxKind::MATH_OPEN, SyntaxKind::MATH_TEXT, SyntaxKind::MATH_COMMAND, SyntaxKind::MATH_CLOSE, ]
644 );
645 }
646
647 #[test]
648 fn null_delimiter_and_asymmetric_pairs_are_lossless() {
649 for content in [
651 r"\left. x \right|",
652 r"\left( x \right]",
653 r"\left\{ x \right\}",
654 ] {
655 assert_eq!(delimited_count(content), 1, "one node: {content:?}");
656 assert_lossless(content);
657 }
658 }
659
660 #[test]
661 fn nested_delimited_is_lossless() {
662 let content = r"\left[ \left( a \right) \right]";
663 assert_eq!(delimited_count(content), 2);
664 assert_lossless(content);
665 }
666
667 #[test]
668 fn unclosed_and_stray_delimiters_stay_lossless() {
669 assert_eq!(delimited_count(r"\left( x"), 1);
672 assert_lossless(r"\left( x");
673 assert_eq!(delimited_count(r"x \right)"), 0);
674 assert_lossless(r"x \right)");
675 }
676
677 #[test]
678 fn leftarrow_and_rightarrow_are_not_delimiters() {
679 let content = r"a \leftarrow b \rightarrow c";
681 assert_eq!(delimited_count(content), 0);
682 assert_lossless(content);
683 }
684
685 const BOOKDOWN: MathParseOptions = MathParseOptions {
688 bookdown_equation_labels: true,
689 };
690
691 fn label_kinds(content: &str, opts: MathParseOptions) -> Vec<SyntaxKind> {
692 node_with(content, opts)
693 .descendants_with_tokens()
694 .filter_map(|el| el.into_token())
695 .map(|tok| tok.kind())
696 .collect()
697 }
698
699 #[test]
700 fn equation_label_recognized_when_enabled() {
701 let kinds = label_kinds(r"a (\#eq:foo)", BOOKDOWN);
702 assert!(kinds.contains(&SyntaxKind::MATH_EQUATION_LABEL));
703 let label = node_with(r"a (\#eq:foo)", BOOKDOWN)
705 .descendants_with_tokens()
706 .filter_map(|el| el.into_token())
707 .find(|t| t.kind() == SyntaxKind::MATH_EQUATION_LABEL)
708 .expect("label token");
709 assert_eq!(label.text(), r"(\#eq:foo)");
710 }
711
712 #[test]
713 fn equation_label_ignored_when_disabled() {
714 let kinds = label_kinds(r"a (\#eq:foo)", MathParseOptions::default());
716 assert!(!kinds.contains(&SyntaxKind::MATH_EQUATION_LABEL));
717 }
718
719 #[test]
720 fn plain_parens_tokenize_the_same_with_or_without_bookdown() {
721 let expected = vec![
725 SyntaxKind::MATH_TEXT, SyntaxKind::MATH_OPEN, SyntaxKind::MATH_TEXT, SyntaxKind::MATH_CLOSE, ];
730 assert_eq!(token_kinds("f(x)"), expected);
731 assert_eq!(label_kinds("f(x)", BOOKDOWN), expected);
732 }
733
734 #[test]
735 fn label_parsing_is_lossless() {
736 let content = "\\begin{align}\n a (\\#eq:solveG)\n\\end{align}";
737 assert_eq!(node_with(content, BOOKDOWN).text().to_string(), content);
738 }
739}