1use super::sink::InlineSink;
15use crate::parser::blocks::raw_blocks::{extract_environment_name, is_inline_math_environment};
16use crate::parser::math::{MathParseOptions, parse_math_content};
17use crate::parser::utils::tree_copy::copy_green_node;
18use crate::syntax::SyntaxKind;
19
20fn emit_math_content(builder: &mut impl InlineSink, content: &str, opts: MathParseOptions) {
25 copy_green_node(builder, &parse_math_content(content, opts));
26}
27
28pub fn math_opts(config: &crate::options::ParserOptions) -> MathParseOptions {
31 MathParseOptions {
32 bookdown_equation_labels: config.extensions.bookdown_equation_references,
33 }
34}
35
36fn newline_ends_inline_math(after_nl: &str, allow_multiline: bool) -> bool {
46 if !allow_multiline {
47 return true;
48 }
49 let next = after_nl.trim_start_matches([' ', '\t']);
50 next.is_empty() || next.starts_with('\n') || next.starts_with('\r')
51}
52
53pub fn try_parse_inline_math(text: &str, allow_multiline: bool) -> Option<(usize, &str)> {
61 if !text.starts_with('$') || text.starts_with("$$") {
63 return None;
64 }
65
66 let rest = &text[1..];
67
68 if rest.is_empty() || rest.starts_with(char::is_whitespace) {
70 return None;
71 }
72
73 let mut pos = 0;
75 while pos < rest.len() {
76 let ch = rest[pos..].chars().next()?;
77
78 if ch == '$' {
79 if pos > 0 && rest.as_bytes()[pos - 1] == b'\\' {
81 pos += 1;
83 continue;
84 }
85
86 if pos == 0 || rest[..pos].ends_with(char::is_whitespace) {
88 pos += 1;
90 continue;
91 }
92
93 if let Some(next_ch) = rest[pos + 1..].chars().next()
95 && next_ch.is_ascii_digit()
96 {
97 pos += 1;
99 continue;
100 }
101
102 let math_content = &rest[..pos];
104 let total_len = 1 + pos + 1; return Some((total_len, math_content));
106 }
107
108 if ch == '\n' && newline_ends_inline_math(&rest[pos + 1..], allow_multiline) {
109 return None;
110 }
111
112 pos += ch.len_utf8();
113 }
114
115 None
117}
118
119pub fn try_parse_gfm_inline_math(text: &str, allow_multiline: bool) -> Option<(usize, &str)> {
122 if !text.starts_with("$`") {
123 return None;
124 }
125
126 let rest = &text[2..];
127 if rest.is_empty() {
128 return None;
129 }
130
131 let mut pos = 0;
132 while pos < rest.len() {
133 let ch = rest[pos..].chars().next()?;
134 if ch == '\n' && newline_ends_inline_math(&rest[pos + 1..], allow_multiline) {
135 return None;
136 }
137 if rest[pos..].starts_with("`$") {
138 if pos == 0 {
139 return None;
140 }
141 let math_content = &rest[..pos];
142 let total_len = 2 + pos + 2; return Some((total_len, math_content));
144 }
145 pos += ch.len_utf8();
146 }
147
148 None
149}
150
151pub fn try_parse_single_backslash_inline_math(
154 text: &str,
155 allow_multiline: bool,
156) -> Option<(usize, &str)> {
157 if !text.starts_with(r"\(") {
158 return None;
159 }
160
161 let rest = &text[2..]; let mut pos = 0;
165 while pos < rest.len() {
166 let ch = rest[pos..].chars().next()?;
167
168 if ch == '\\' && rest[pos..].starts_with(r"\)") {
169 let math_content = &rest[..pos];
171 let total_len = 2 + pos + 2; return Some((total_len, math_content));
173 }
174
175 if ch == '\n' && newline_ends_inline_math(&rest[pos + 1..], allow_multiline) {
176 return None;
177 }
178
179 pos += ch.len_utf8();
180 }
181
182 None
183}
184
185pub fn try_parse_double_backslash_inline_math(
188 text: &str,
189 allow_multiline: bool,
190) -> Option<(usize, &str)> {
191 if !text.starts_with(r"\\(") {
192 return None;
193 }
194
195 let rest = &text[3..]; let mut pos = 0;
199 while pos < rest.len() {
200 let ch = rest[pos..].chars().next()?;
201
202 if ch == '\\' && rest[pos..].starts_with(r"\\)") {
203 let math_content = &rest[..pos];
205 let total_len = 3 + pos + 3; return Some((total_len, math_content));
207 }
208
209 if ch == '\n' && newline_ends_inline_math(&rest[pos + 1..], allow_multiline) {
210 return None;
211 }
212
213 pos += ch.len_utf8();
214 }
215
216 None
217}
218
219pub fn try_parse_display_math(text: &str) -> Option<(usize, &str)> {
228 if !text.starts_with("$$") {
230 return None;
231 }
232
233 let opening_count = text.chars().take_while(|&c| c == '$').count();
235 if opening_count < 2 {
236 return None;
237 }
238
239 let rest = &text[opening_count..];
240
241 let mut pos = 0;
243 while pos < rest.len() {
244 let ch = rest[pos..].chars().next()?;
245
246 if ch == '$' {
247 if pos > 0 && rest.as_bytes()[pos - 1] == b'\\' {
249 pos += ch.len_utf8();
251 continue;
252 }
253
254 let closing_count = rest[pos..].chars().take_while(|&c| c == '$').count();
256
257 if closing_count >= opening_count {
259 let math_content = &rest[..pos];
260 let total_len = opening_count + pos + closing_count;
261 return Some((total_len, math_content));
262 }
263
264 pos += closing_count;
266 continue;
267 }
268
269 pos += ch.len_utf8();
270 }
271
272 None
274}
275
276pub fn try_parse_single_backslash_display_math(text: &str) -> Option<(usize, &str)> {
283 if !text.starts_with(r"\[") {
284 return None;
285 }
286
287 let rest = &text[2..]; let mut pos = 0;
291 while pos < rest.len() {
292 let ch = rest[pos..].chars().next()?;
293
294 if ch == '\\' && rest[pos..].starts_with(r"\]") {
295 let math_content = &rest[..pos];
297 let total_len = 2 + pos + 2; return Some((total_len, math_content));
299 }
300
301 pos += ch.len_utf8();
302 }
303
304 None
305}
306
307pub fn try_parse_double_backslash_display_math(text: &str) -> Option<(usize, &str)> {
314 if !text.starts_with(r"\\[") {
315 return None;
316 }
317
318 let rest = &text[3..]; let mut pos = 0;
322 while pos < rest.len() {
323 let ch = rest[pos..].chars().next()?;
324
325 if ch == '\\' && rest[pos..].starts_with(r"\\]") {
326 let math_content = &rest[..pos];
328 let total_len = 3 + pos + 3; return Some((total_len, math_content));
330 }
331
332 pos += ch.len_utf8();
333 }
334
335 None
336}
337
338pub fn try_parse_math_environment(text: &str) -> Option<(usize, &str, &str, &str)> {
341 let env_name = extract_environment_name(text)?;
342 if !is_inline_math_environment(env_name) {
343 return None;
344 }
345
346 let begin_marker_len = text.find('}')? + 1;
347 let begin_marker = &text[..begin_marker_len];
348 let end_marker = format!("\\end{{{}}}", env_name);
349
350 let after_begin = &text[begin_marker_len..];
351 let end_rel = after_begin.find(&end_marker)?;
352 let end_start = begin_marker_len + end_rel;
353 let end_marker_end = end_start + end_marker.len();
354
355 let mut end_line_end = end_marker_end;
356 while end_line_end < text.len() {
357 let ch = text[end_line_end..].chars().next()?;
358 if ch == '\n' || ch == '\r' {
359 break;
360 }
361 end_line_end += ch.len_utf8();
362 }
363
364 if end_line_end < text.len() {
365 if text[end_line_end..].starts_with("\r\n") {
366 end_line_end += 2;
367 } else {
368 end_line_end += 1;
369 }
370 }
371
372 let content = &text[begin_marker_len..end_start];
373 let end_marker_text = &text[end_start..end_line_end];
374 Some((end_line_end, begin_marker, content, end_marker_text))
375}
376
377pub fn emit_inline_math(builder: &mut impl InlineSink, content: &str, opts: MathParseOptions) {
379 builder.start_node(SyntaxKind::INLINE_MATH.into());
380
381 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "$");
383
384 emit_math_content(builder, content, opts);
386
387 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "$");
389
390 builder.finish_node();
391}
392
393pub fn emit_gfm_inline_math(builder: &mut impl InlineSink, content: &str, opts: MathParseOptions) {
395 builder.start_node(SyntaxKind::INLINE_MATH.into());
396 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "$`");
397 emit_math_content(builder, content, opts);
398 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "`$");
399 builder.finish_node();
400}
401
402pub fn emit_single_backslash_inline_math(
404 builder: &mut impl InlineSink,
405 content: &str,
406 opts: MathParseOptions,
407) {
408 builder.start_node(SyntaxKind::INLINE_MATH.into());
409
410 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\(");
411 emit_math_content(builder, content, opts);
412 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\)");
413
414 builder.finish_node();
415}
416
417pub fn emit_double_backslash_inline_math(
419 builder: &mut impl InlineSink,
420 content: &str,
421 opts: MathParseOptions,
422) {
423 builder.start_node(SyntaxKind::INLINE_MATH.into());
424
425 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\\(");
426 emit_math_content(builder, content, opts);
427 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\\)");
428
429 builder.finish_node();
430}
431
432pub fn emit_display_math(
434 builder: &mut impl InlineSink,
435 content: &str,
436 dollar_count: usize,
437 opts: MathParseOptions,
438) {
439 builder.start_node(SyntaxKind::DISPLAY_MATH.into());
440
441 let marker = "$".repeat(dollar_count);
443 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), &marker);
444
445 emit_math_content(builder, content, opts);
447
448 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), &marker);
450
451 builder.finish_node();
452}
453
454pub fn emit_display_math_environment(
456 builder: &mut impl InlineSink,
457 begin_marker: &str,
458 content: &str,
459 end_marker: &str,
460 opts: MathParseOptions,
461) {
462 builder.start_node(SyntaxKind::DISPLAY_MATH.into());
463 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), begin_marker);
464 emit_math_content(builder, content, opts);
465 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), end_marker);
466 builder.finish_node();
467}
468
469pub fn emit_single_backslash_display_math(
471 builder: &mut impl InlineSink,
472 content: &str,
473 opts: MathParseOptions,
474) {
475 builder.start_node(SyntaxKind::DISPLAY_MATH.into());
476
477 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\[");
478 emit_math_content(builder, content, opts);
479 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\]");
480
481 builder.finish_node();
482}
483
484pub fn emit_double_backslash_display_math(
486 builder: &mut impl InlineSink,
487 content: &str,
488 opts: MathParseOptions,
489) {
490 builder.start_node(SyntaxKind::DISPLAY_MATH.into());
491
492 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\\[");
493 emit_math_content(builder, content, opts);
494 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\\]");
495
496 builder.finish_node();
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn test_parse_simple_inline_math() {
505 let result = try_parse_inline_math("$x = y$", true);
506 assert_eq!(result, Some((7, "x = y")));
507 }
508
509 #[test]
510 fn test_parse_inline_math_with_spaces_inside() {
511 let result = try_parse_inline_math("$a + b$", true);
513 assert_eq!(result, Some((7, "a + b")));
514 }
515
516 #[test]
517 fn test_parse_inline_math_complex() {
518 let result = try_parse_inline_math(r"$\frac{1}{2}$", true);
519 assert_eq!(result, Some((13, r"\frac{1}{2}")));
520 }
521
522 #[test]
523 fn test_not_inline_math_display() {
524 let result = try_parse_inline_math("$$x = y$$", true);
526 assert_eq!(result, None);
527 }
528
529 #[test]
530 fn test_inline_math_no_close() {
531 let result = try_parse_inline_math("$no close", true);
532 assert_eq!(result, None);
533 }
534
535 #[test]
536 fn test_inline_math_spans_single_newline_pandoc() {
537 let result = try_parse_inline_math("$x =\ny$", true);
540 assert_eq!(result, Some((7, "x =\ny")));
541 }
542
543 #[test]
544 fn test_inline_math_single_line_only_commonmark() {
545 let result = try_parse_inline_math("$x =\ny$", false);
547 assert_eq!(result, None);
548 }
549
550 #[test]
551 fn test_inline_math_stops_at_blank_line() {
552 let result = try_parse_inline_math("$x =\n\ny$", true);
554 assert_eq!(result, None);
555 }
556
557 #[test]
558 fn test_not_inline_math() {
559 let result = try_parse_inline_math("no dollar", true);
560 assert_eq!(result, None);
561 }
562
563 #[test]
564 fn test_inline_math_with_trailing_text() {
565 let result = try_parse_inline_math("$x$ and more", true);
566 assert_eq!(result, Some((3, "x")));
567 }
568
569 #[test]
570 fn test_spec_opening_must_have_non_space_right() {
571 let result = try_parse_inline_math("$ x$", true);
573 assert_eq!(result, None, "Opening $ with space should not parse");
574 }
575
576 #[test]
577 fn test_spec_closing_must_have_non_space_left() {
578 let result = try_parse_inline_math("$x $", true);
580 assert_eq!(result, None, "Closing $ with space should not parse");
581 }
582
583 #[test]
584 fn test_spec_closing_not_followed_by_digit() {
585 let result = try_parse_inline_math("$x$5", true);
587 assert_eq!(result, None, "Closing $ followed by digit should not parse");
588 }
589
590 #[test]
591 fn test_spec_dollar_amounts() {
592 let result = try_parse_inline_math("$20,000", true);
594 assert_eq!(result, None, "Dollar amounts should not parse as math");
595 }
596
597 #[test]
598 fn test_valid_math_after_spec_checks() {
599 let result = try_parse_inline_math("$x$", true);
601 assert_eq!(result, Some((3, "x")), "Valid math should parse");
602 }
603
604 #[test]
605 fn test_math_followed_by_non_digit() {
606 let result = try_parse_inline_math("$x$a", true);
608 assert_eq!(
609 result,
610 Some((3, "x")),
611 "Math followed by non-digit should parse"
612 );
613 }
614
615 #[test]
617 fn test_parse_display_math_simple() {
618 let result = try_parse_display_math("$$x = y$$");
619 assert_eq!(result, Some((9, "x = y")));
620 }
621
622 #[test]
623 fn test_parse_display_math_multiline() {
624 let result = try_parse_display_math("$$\nx = y\n$$");
625 assert_eq!(result, Some((11, "\nx = y\n")));
626 }
627
628 #[test]
629 fn test_parse_display_math_triple_dollars() {
630 let result = try_parse_display_math("$$$x = y$$$");
631 assert_eq!(result, Some((11, "x = y")));
632 }
633
634 #[test]
635 fn test_parse_display_math_no_close() {
636 let result = try_parse_display_math("$$no close");
637 assert_eq!(result, None);
638 }
639
640 #[test]
641 fn test_not_display_math() {
642 let result = try_parse_display_math("$single dollar");
643 assert_eq!(result, None);
644 }
645
646 #[test]
647 fn test_display_math_with_trailing_text() {
648 let result = try_parse_display_math("$$x = y$$ and more");
649 assert_eq!(result, Some((9, "x = y")));
650 }
651
652 #[test]
654 fn test_single_backslash_inline_math() {
655 let result = try_parse_single_backslash_inline_math(r"\(x^2\)", true);
656 assert_eq!(result, Some((7, "x^2")));
657 }
658
659 #[test]
660 fn test_single_backslash_inline_math_complex() {
661 let result = try_parse_single_backslash_inline_math(r"\(\frac{a}{b}\)", true);
662 assert_eq!(result, Some((15, r"\frac{a}{b}")));
663 }
664
665 #[test]
666 fn test_single_backslash_inline_math_no_close() {
667 let result = try_parse_single_backslash_inline_math(r"\(no close", true);
668 assert_eq!(result, None);
669 }
670
671 #[test]
672 fn test_single_backslash_inline_math_spans_single_newline() {
673 let result = try_parse_single_backslash_inline_math("\\(x =\ny\\)", true);
677 assert_eq!(result, Some((9, "x =\ny")));
678 let blank = try_parse_single_backslash_inline_math("\\(x =\n\ny\\)", true);
679 assert_eq!(blank, None);
680 let cm = try_parse_single_backslash_inline_math("\\(x =\ny\\)", false);
681 assert_eq!(cm, None);
682 }
683
684 #[test]
685 fn test_single_backslash_display_math() {
686 let result = try_parse_single_backslash_display_math(r"\[E = mc^2\]");
687 assert_eq!(result, Some((12, "E = mc^2")));
688 }
689
690 #[test]
691 fn test_single_backslash_display_math_multiline() {
692 let result = try_parse_single_backslash_display_math("\\[\nx = y\n\\]");
693 assert_eq!(result, Some((11, "\nx = y\n")));
694 }
695
696 #[test]
697 fn test_single_backslash_display_math_no_close() {
698 let result = try_parse_single_backslash_display_math(r"\[no close");
699 assert_eq!(result, None);
700 }
701
702 #[test]
704 fn test_double_backslash_inline_math() {
705 let result = try_parse_double_backslash_inline_math(r"\\(x^2\\)", true);
706 assert_eq!(result, Some((9, "x^2")));
707 }
708
709 #[test]
710 fn test_double_backslash_inline_math_complex() {
711 let result = try_parse_double_backslash_inline_math(r"\\(\alpha + \beta\\)", true);
712 assert_eq!(result, Some((20, r"\alpha + \beta")));
713 }
714
715 #[test]
716 fn test_double_backslash_inline_math_no_close() {
717 let result = try_parse_double_backslash_inline_math(r"\\(no close", true);
718 assert_eq!(result, None);
719 }
720
721 #[test]
722 fn test_double_backslash_inline_math_spans_single_newline() {
723 let result = try_parse_double_backslash_inline_math("\\\\(x =\ny\\\\)", true);
727 assert_eq!(result, Some((11, "x =\ny")));
728 let blank = try_parse_double_backslash_inline_math("\\\\(x =\n\ny\\\\)", true);
729 assert_eq!(blank, None);
730 let cm = try_parse_double_backslash_inline_math("\\\\(x =\ny\\\\)", false);
731 assert_eq!(cm, None);
732 }
733
734 #[test]
735 fn test_double_backslash_display_math() {
736 let result = try_parse_double_backslash_display_math(r"\\[E = mc^2\\]");
737 assert_eq!(result, Some((14, "E = mc^2")));
738 }
739
740 #[test]
741 fn test_double_backslash_display_math_multiline() {
742 let result = try_parse_double_backslash_display_math("\\\\[\nx = y\n\\\\]");
743 assert_eq!(result, Some((13, "\nx = y\n")));
744 }
745
746 #[test]
747 fn test_double_backslash_display_math_no_close() {
748 let result = try_parse_double_backslash_display_math(r"\\[no close");
749 assert_eq!(result, None);
750 }
751
752 #[test]
754 fn test_display_math_escaped_dollar() {
755 let result = try_parse_display_math(r"$$a = \$100$$");
757 assert_eq!(result, Some((13, r"a = \$100")));
758 }
759
760 #[test]
761 fn test_display_math_with_content_on_fence_line() {
762 let result = try_parse_display_math("$$x = y\n$$");
764 assert_eq!(result, Some((10, "x = y\n")));
765 }
766}