panache_parser/syntax/
math.rs1use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
4
5pub struct DisplayMath(SyntaxNode);
6
7impl AstNode for DisplayMath {
8 type Language = PanacheLanguage;
9
10 fn can_cast(kind: SyntaxKind) -> bool {
11 kind == SyntaxKind::DISPLAY_MATH
12 }
13
14 fn cast(syntax: SyntaxNode) -> Option<Self> {
15 if Self::can_cast(syntax.kind()) {
16 Some(Self(syntax))
17 } else {
18 None
19 }
20 }
21
22 fn syntax(&self) -> &SyntaxNode {
23 &self.0
24 }
25}
26
27impl DisplayMath {
28 pub fn opening_marker(&self) -> Option<String> {
29 self.0.children_with_tokens().find_map(|child| {
30 child.into_token().and_then(|token| {
31 (token.kind() == SyntaxKind::DISPLAY_MATH_MARKER).then(|| token.text().to_string())
32 })
33 })
34 }
35
36 pub fn closing_marker(&self) -> Option<String> {
37 self.0
38 .children_with_tokens()
39 .filter_map(|child| child.into_token())
40 .filter(|token| token.kind() == SyntaxKind::DISPLAY_MATH_MARKER)
41 .nth(1)
42 .map(|token| token.text().to_string())
43 }
44
45 pub fn content(&self) -> String {
46 self.0
47 .children_with_tokens()
48 .filter_map(|child| child.into_token())
49 .filter(|token| token.kind() == SyntaxKind::TEXT)
50 .map(|token| token.text().to_string())
51 .collect::<Vec<_>>()
52 .join("")
53 }
54
55 pub fn is_environment_form(&self) -> bool {
56 let opening = self.opening_marker().unwrap_or_default();
57 let closing = self.closing_marker().unwrap_or_default();
58 opening.starts_with("\\begin{") && closing.starts_with("\\end{")
59 }
60
61 pub fn has_unescaped_single_dollar_in_content(&self) -> bool {
62 let content = self.content();
63 let chars: Vec<char> = content.chars().collect();
64 let mut idx = 0usize;
65 let mut backslashes = 0usize;
66
67 while idx < chars.len() {
68 let ch = chars[idx];
69 if ch == '\\' {
70 backslashes += 1;
71 idx += 1;
72 continue;
73 }
74
75 let escaped = backslashes % 2 == 1;
76 backslashes = 0;
77 if ch == '$' && !escaped {
78 if idx + 1 < chars.len() && chars[idx + 1] == '$' {
79 idx += 2;
80 continue;
81 }
82 return true;
83 }
84 idx += 1;
85 }
86
87 false
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::parse;
95
96 #[test]
97 fn display_math_dollar_markers_and_content() {
98 let tree = parse("$$\nx^2 + y^2\n$$\n", None);
99 let math = tree
100 .descendants()
101 .find_map(DisplayMath::cast)
102 .expect("display math");
103
104 assert_eq!(math.opening_marker().as_deref(), Some("$$"));
105 assert_eq!(math.closing_marker().as_deref(), Some("$$"));
106 assert!(math.content().contains("x^2 + y^2"));
107 assert!(!math.is_environment_form());
108 }
109
110 #[test]
111 fn display_math_environment_form_detection() {
112 let tree = parse("\\begin{align}\na &= b\\\\\n\\end{align}\n", None);
113 let math = tree
114 .descendants()
115 .find_map(DisplayMath::cast)
116 .expect("display math");
117
118 assert!(math.is_environment_form());
119 assert_eq!(math.opening_marker().as_deref(), Some("\\begin{align}"));
120 assert_eq!(math.closing_marker().as_deref(), Some("\\end{align}\n"));
121 }
122
123 #[test]
124 fn display_math_detects_unescaped_single_dollar() {
125 let tree = parse("$$\nalpha $beta$ gamma\n$$\n", None);
126 let math = tree
127 .descendants()
128 .find_map(DisplayMath::cast)
129 .expect("display math");
130 assert!(math.has_unescaped_single_dollar_in_content());
131 }
132}