panache_parser/parser/inlines/
math.rs1use crate::parser::blocks::raw_blocks::{extract_environment_name, is_inline_math_environment};
12use crate::syntax::SyntaxKind;
13use rowan::GreenNodeBuilder;
14
15pub fn try_parse_inline_math(text: &str) -> Option<(usize, &str)> {
23 if !text.starts_with('$') || text.starts_with("$$") {
25 return None;
26 }
27
28 let rest = &text[1..];
29
30 if rest.is_empty() || rest.starts_with(char::is_whitespace) {
32 return None;
33 }
34
35 let mut pos = 0;
37 while pos < rest.len() {
38 let ch = rest[pos..].chars().next()?;
39
40 if ch == '$' {
41 if pos > 0 && rest.as_bytes()[pos - 1] == b'\\' {
43 pos += 1;
45 continue;
46 }
47
48 if pos == 0 || rest[..pos].ends_with(char::is_whitespace) {
50 pos += 1;
52 continue;
53 }
54
55 if let Some(next_ch) = rest[pos + 1..].chars().next()
57 && next_ch.is_ascii_digit()
58 {
59 pos += 1;
61 continue;
62 }
63
64 let math_content = &rest[..pos];
66 let total_len = 1 + pos + 1; return Some((total_len, math_content));
68 }
69
70 if ch == '\n' {
72 return None;
73 }
74
75 pos += ch.len_utf8();
76 }
77
78 None
80}
81
82pub fn try_parse_gfm_inline_math(text: &str) -> Option<(usize, &str)> {
85 if !text.starts_with("$`") {
86 return None;
87 }
88
89 let rest = &text[2..];
90 if rest.is_empty() {
91 return None;
92 }
93
94 let mut pos = 0;
95 while pos < rest.len() {
96 let ch = rest[pos..].chars().next()?;
97 if ch == '\n' {
98 return None;
99 }
100 if rest[pos..].starts_with("`$") {
101 if pos == 0 {
102 return None;
103 }
104 let math_content = &rest[..pos];
105 let total_len = 2 + pos + 2; return Some((total_len, math_content));
107 }
108 pos += ch.len_utf8();
109 }
110
111 None
112}
113
114pub fn try_parse_single_backslash_inline_math(text: &str) -> Option<(usize, &str)> {
117 if !text.starts_with(r"\(") {
118 return None;
119 }
120
121 let rest = &text[2..]; let mut pos = 0;
125 while pos < rest.len() {
126 let ch = rest[pos..].chars().next()?;
127
128 if ch == '\\' && rest[pos..].starts_with(r"\)") {
129 let math_content = &rest[..pos];
131 let total_len = 2 + pos + 2; return Some((total_len, math_content));
133 }
134
135 if ch == '\n' {
137 return None;
138 }
139
140 pos += ch.len_utf8();
141 }
142
143 None
144}
145
146pub fn try_parse_double_backslash_inline_math(text: &str) -> Option<(usize, &str)> {
149 if !text.starts_with(r"\\(") {
150 return None;
151 }
152
153 let rest = &text[3..]; let mut pos = 0;
157 while pos < rest.len() {
158 let ch = rest[pos..].chars().next()?;
159
160 if ch == '\\' && rest[pos..].starts_with(r"\\)") {
161 let math_content = &rest[..pos];
163 let total_len = 3 + pos + 3; return Some((total_len, math_content));
165 }
166
167 if ch == '\n' {
169 return None;
170 }
171
172 pos += ch.len_utf8();
173 }
174
175 None
176}
177
178pub fn try_parse_display_math(text: &str) -> Option<(usize, &str)> {
187 if !text.starts_with("$$") {
189 return None;
190 }
191
192 let opening_count = text.chars().take_while(|&c| c == '$').count();
194 if opening_count < 2 {
195 return None;
196 }
197
198 let rest = &text[opening_count..];
199
200 let mut pos = 0;
202 while pos < rest.len() {
203 let ch = rest[pos..].chars().next()?;
204
205 if ch == '$' {
206 if pos > 0 && rest.as_bytes()[pos - 1] == b'\\' {
208 pos += ch.len_utf8();
210 continue;
211 }
212
213 let closing_count = rest[pos..].chars().take_while(|&c| c == '$').count();
215
216 if closing_count >= opening_count {
218 let math_content = &rest[..pos];
219 let total_len = opening_count + pos + closing_count;
220 return Some((total_len, math_content));
221 }
222
223 pos += closing_count;
225 continue;
226 }
227
228 pos += ch.len_utf8();
229 }
230
231 None
233}
234
235pub fn try_parse_single_backslash_display_math(text: &str) -> Option<(usize, &str)> {
242 if !text.starts_with(r"\[") {
243 return None;
244 }
245
246 let rest = &text[2..]; let mut pos = 0;
250 while pos < rest.len() {
251 let ch = rest[pos..].chars().next()?;
252
253 if ch == '\\' && rest[pos..].starts_with(r"\]") {
254 let math_content = &rest[..pos];
256 let total_len = 2 + pos + 2; return Some((total_len, math_content));
258 }
259
260 pos += ch.len_utf8();
261 }
262
263 None
264}
265
266pub fn try_parse_double_backslash_display_math(text: &str) -> Option<(usize, &str)> {
273 if !text.starts_with(r"\\[") {
274 return None;
275 }
276
277 let rest = &text[3..]; let mut pos = 0;
281 while pos < rest.len() {
282 let ch = rest[pos..].chars().next()?;
283
284 if ch == '\\' && rest[pos..].starts_with(r"\\]") {
285 let math_content = &rest[..pos];
287 let total_len = 3 + pos + 3; return Some((total_len, math_content));
289 }
290
291 pos += ch.len_utf8();
292 }
293
294 None
295}
296
297pub fn try_parse_math_environment(text: &str) -> Option<(usize, &str, &str, &str)> {
300 let env_name = extract_environment_name(text)?;
301 if !is_inline_math_environment(&env_name) {
302 return None;
303 }
304
305 let begin_marker_len = text.find('}')? + 1;
306 let begin_marker = &text[..begin_marker_len];
307 let end_marker = format!("\\end{{{}}}", env_name);
308
309 let after_begin = &text[begin_marker_len..];
310 let end_rel = after_begin.find(&end_marker)?;
311 let end_start = begin_marker_len + end_rel;
312 let end_marker_end = end_start + end_marker.len();
313
314 let mut end_line_end = end_marker_end;
315 while end_line_end < text.len() {
316 let ch = text[end_line_end..].chars().next()?;
317 if ch == '\n' || ch == '\r' {
318 break;
319 }
320 end_line_end += ch.len_utf8();
321 }
322
323 if end_line_end < text.len() {
324 if text[end_line_end..].starts_with("\r\n") {
325 end_line_end += 2;
326 } else {
327 end_line_end += 1;
328 }
329 }
330
331 let content = &text[begin_marker_len..end_start];
332 let end_marker_text = &text[end_start..end_line_end];
333 Some((end_line_end, begin_marker, content, end_marker_text))
334}
335
336pub fn emit_inline_math(builder: &mut GreenNodeBuilder, content: &str) {
338 builder.start_node(SyntaxKind::INLINE_MATH.into());
339
340 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "$");
342
343 builder.token(SyntaxKind::TEXT.into(), content);
345
346 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "$");
348
349 builder.finish_node();
350}
351
352pub fn emit_gfm_inline_math(builder: &mut GreenNodeBuilder, content: &str) {
354 builder.start_node(SyntaxKind::INLINE_MATH.into());
355 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "$`");
356 builder.token(SyntaxKind::TEXT.into(), content);
357 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), "`$");
358 builder.finish_node();
359}
360
361pub fn emit_single_backslash_inline_math(builder: &mut GreenNodeBuilder, content: &str) {
363 builder.start_node(SyntaxKind::INLINE_MATH.into());
364
365 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\(");
366 builder.token(SyntaxKind::TEXT.into(), content);
367 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\)");
368
369 builder.finish_node();
370}
371
372pub fn emit_double_backslash_inline_math(builder: &mut GreenNodeBuilder, content: &str) {
374 builder.start_node(SyntaxKind::INLINE_MATH.into());
375
376 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\\(");
377 builder.token(SyntaxKind::TEXT.into(), content);
378 builder.token(SyntaxKind::INLINE_MATH_MARKER.into(), r"\\)");
379
380 builder.finish_node();
381}
382
383pub fn emit_display_math(builder: &mut GreenNodeBuilder, content: &str, dollar_count: usize) {
385 builder.start_node(SyntaxKind::DISPLAY_MATH.into());
386
387 let marker = "$".repeat(dollar_count);
389 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), &marker);
390
391 builder.token(SyntaxKind::TEXT.into(), content);
393
394 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), &marker);
396
397 builder.finish_node();
398}
399
400pub fn emit_display_math_environment(
402 builder: &mut GreenNodeBuilder,
403 begin_marker: &str,
404 content: &str,
405 end_marker: &str,
406) {
407 builder.start_node(SyntaxKind::DISPLAY_MATH.into());
408 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), begin_marker);
409 builder.token(SyntaxKind::TEXT.into(), content);
410 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), end_marker);
411 builder.finish_node();
412}
413
414pub fn emit_single_backslash_display_math(builder: &mut GreenNodeBuilder, content: &str) {
416 builder.start_node(SyntaxKind::DISPLAY_MATH.into());
417
418 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\[");
419 builder.token(SyntaxKind::TEXT.into(), content);
420 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\]");
421
422 builder.finish_node();
423}
424
425pub fn emit_double_backslash_display_math(builder: &mut GreenNodeBuilder, content: &str) {
427 builder.start_node(SyntaxKind::DISPLAY_MATH.into());
428
429 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\\[");
430 builder.token(SyntaxKind::TEXT.into(), content);
431 builder.token(SyntaxKind::DISPLAY_MATH_MARKER.into(), r"\\]");
432
433 builder.finish_node();
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn test_parse_simple_inline_math() {
442 let result = try_parse_inline_math("$x = y$");
443 assert_eq!(result, Some((7, "x = y")));
444 }
445
446 #[test]
447 fn test_parse_inline_math_with_spaces_inside() {
448 let result = try_parse_inline_math("$a + b$");
450 assert_eq!(result, Some((7, "a + b")));
451 }
452
453 #[test]
454 fn test_parse_inline_math_complex() {
455 let result = try_parse_inline_math(r"$\frac{1}{2}$");
456 assert_eq!(result, Some((13, r"\frac{1}{2}")));
457 }
458
459 #[test]
460 fn test_not_inline_math_display() {
461 let result = try_parse_inline_math("$$x = y$$");
463 assert_eq!(result, None);
464 }
465
466 #[test]
467 fn test_inline_math_no_close() {
468 let result = try_parse_inline_math("$no close");
469 assert_eq!(result, None);
470 }
471
472 #[test]
473 fn test_inline_math_no_multiline() {
474 let result = try_parse_inline_math("$x =\ny$");
475 assert_eq!(result, None);
476 }
477
478 #[test]
479 fn test_not_inline_math() {
480 let result = try_parse_inline_math("no dollar");
481 assert_eq!(result, None);
482 }
483
484 #[test]
485 fn test_inline_math_with_trailing_text() {
486 let result = try_parse_inline_math("$x$ and more");
487 assert_eq!(result, Some((3, "x")));
488 }
489
490 #[test]
491 fn test_inline_math_escaped_dollar() {
492 let result = try_parse_inline_math(r"$a \$ b$");
495 assert!(result.is_some());
498 }
499
500 #[test]
501 fn test_spec_opening_must_have_non_space_right() {
502 let result = try_parse_inline_math("$ x$");
504 assert_eq!(result, None, "Opening $ with space should not parse");
505 }
506
507 #[test]
508 fn test_spec_closing_must_have_non_space_left() {
509 let result = try_parse_inline_math("$x $");
511 assert_eq!(result, None, "Closing $ with space should not parse");
512 }
513
514 #[test]
515 fn test_spec_closing_not_followed_by_digit() {
516 let result = try_parse_inline_math("$x$5");
518 assert_eq!(result, None, "Closing $ followed by digit should not parse");
519 }
520
521 #[test]
522 fn test_spec_dollar_amounts() {
523 let result = try_parse_inline_math("$20,000");
525 assert_eq!(result, None, "Dollar amounts should not parse as math");
526 }
527
528 #[test]
529 fn test_valid_math_after_spec_checks() {
530 let result = try_parse_inline_math("$x$");
532 assert_eq!(result, Some((3, "x")), "Valid math should parse");
533 }
534
535 #[test]
536 fn test_math_followed_by_non_digit() {
537 let result = try_parse_inline_math("$x$a");
539 assert_eq!(
540 result,
541 Some((3, "x")),
542 "Math followed by non-digit should parse"
543 );
544 }
545
546 #[test]
548 fn test_parse_display_math_simple() {
549 let result = try_parse_display_math("$$x = y$$");
550 assert_eq!(result, Some((9, "x = y")));
551 }
552
553 #[test]
554 fn test_parse_display_math_multiline() {
555 let result = try_parse_display_math("$$\nx = y\n$$");
556 assert_eq!(result, Some((11, "\nx = y\n")));
557 }
558
559 #[test]
560 fn test_parse_display_math_triple_dollars() {
561 let result = try_parse_display_math("$$$x = y$$$");
562 assert_eq!(result, Some((11, "x = y")));
563 }
564
565 #[test]
566 fn test_parse_display_math_no_close() {
567 let result = try_parse_display_math("$$no close");
568 assert_eq!(result, None);
569 }
570
571 #[test]
572 fn test_not_display_math() {
573 let result = try_parse_display_math("$single dollar");
574 assert_eq!(result, None);
575 }
576
577 #[test]
578 fn test_display_math_with_trailing_text() {
579 let result = try_parse_display_math("$$x = y$$ and more");
580 assert_eq!(result, Some((9, "x = y")));
581 }
582
583 #[test]
585 fn test_single_backslash_inline_math() {
586 let result = try_parse_single_backslash_inline_math(r"\(x^2\)");
587 assert_eq!(result, Some((7, "x^2")));
588 }
589
590 #[test]
591 fn test_single_backslash_inline_math_complex() {
592 let result = try_parse_single_backslash_inline_math(r"\(\frac{a}{b}\)");
593 assert_eq!(result, Some((15, r"\frac{a}{b}")));
594 }
595
596 #[test]
597 fn test_single_backslash_inline_math_no_close() {
598 let result = try_parse_single_backslash_inline_math(r"\(no close");
599 assert_eq!(result, None);
600 }
601
602 #[test]
603 fn test_single_backslash_inline_math_no_multiline() {
604 let result = try_parse_single_backslash_inline_math("\\(x =\ny\\)");
605 assert_eq!(result, None);
606 }
607
608 #[test]
609 fn test_single_backslash_display_math() {
610 let result = try_parse_single_backslash_display_math(r"\[E = mc^2\]");
611 assert_eq!(result, Some((12, "E = mc^2")));
612 }
613
614 #[test]
615 fn test_single_backslash_display_math_multiline() {
616 let result = try_parse_single_backslash_display_math("\\[\nx = y\n\\]");
617 assert_eq!(result, Some((11, "\nx = y\n")));
618 }
619
620 #[test]
621 fn test_single_backslash_display_math_no_close() {
622 let result = try_parse_single_backslash_display_math(r"\[no close");
623 assert_eq!(result, None);
624 }
625
626 #[test]
628 fn test_double_backslash_inline_math() {
629 let result = try_parse_double_backslash_inline_math(r"\\(x^2\\)");
630 assert_eq!(result, Some((9, "x^2")));
631 }
632
633 #[test]
634 fn test_double_backslash_inline_math_complex() {
635 let result = try_parse_double_backslash_inline_math(r"\\(\alpha + \beta\\)");
636 assert_eq!(result, Some((20, r"\alpha + \beta")));
637 }
638
639 #[test]
640 fn test_double_backslash_inline_math_no_close() {
641 let result = try_parse_double_backslash_inline_math(r"\\(no close");
642 assert_eq!(result, None);
643 }
644
645 #[test]
646 fn test_double_backslash_inline_math_no_multiline() {
647 let result = try_parse_double_backslash_inline_math("\\\\(x =\ny\\\\)");
648 assert_eq!(result, None);
649 }
650
651 #[test]
652 fn test_double_backslash_display_math() {
653 let result = try_parse_double_backslash_display_math(r"\\[E = mc^2\\]");
654 assert_eq!(result, Some((14, "E = mc^2")));
655 }
656
657 #[test]
658 fn test_double_backslash_display_math_multiline() {
659 let result = try_parse_double_backslash_display_math("\\\\[\nx = y\n\\\\]");
660 assert_eq!(result, Some((13, "\nx = y\n")));
661 }
662
663 #[test]
664 fn test_double_backslash_display_math_no_close() {
665 let result = try_parse_double_backslash_display_math(r"\\[no close");
666 assert_eq!(result, None);
667 }
668
669 #[test]
671 fn test_display_math_escaped_dollar() {
672 let result = try_parse_display_math(r"$$a = \$100$$");
674 assert_eq!(result, Some((13, r"a = \$100")));
675 }
676
677 #[test]
678 fn test_display_math_with_content_on_fence_line() {
679 let result = try_parse_display_math("$$x = y\n$$");
681 assert_eq!(result, Some((10, "x = y\n")));
682 }
683}