panache_parser/syntax/
math.rs1use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
4
5pub fn math_content_text(math: &SyntaxNode) -> String {
14 let Some(content) = math
15 .children()
16 .find(|node| node.kind() == SyntaxKind::MATH_CONTENT)
17 else {
18 return String::new();
19 };
20 content
21 .descendants_with_tokens()
22 .filter_map(|el| el.into_token())
23 .filter(|tok| is_math_content_token(tok.kind()))
24 .map(|tok| tok.text().to_string())
25 .collect()
26}
27
28fn is_math_content_token(kind: SyntaxKind) -> bool {
31 matches!(
32 kind,
33 SyntaxKind::MATH_TEXT
34 | SyntaxKind::MATH_SPACE
35 | SyntaxKind::MATH_NEWLINE
36 | SyntaxKind::MATH_COMMAND
37 | SyntaxKind::MATH_GROUP_OPEN
38 | SyntaxKind::MATH_GROUP_CLOSE
39 | SyntaxKind::MATH_ALIGN
40 | SyntaxKind::MATH_SCRIPT
41 | SyntaxKind::MATH_OPERATOR
42 | SyntaxKind::MATH_LINE_BREAK
43 | SyntaxKind::MATH_COMMENT
44 | SyntaxKind::MATH_EQUATION_LABEL
45 )
46}
47
48pub struct DisplayMath(SyntaxNode);
49
50impl AstNode for DisplayMath {
51 type Language = PanacheLanguage;
52
53 fn can_cast(kind: SyntaxKind) -> bool {
54 kind == SyntaxKind::DISPLAY_MATH
55 }
56
57 fn cast(syntax: SyntaxNode) -> Option<Self> {
58 if Self::can_cast(syntax.kind()) {
59 Some(Self(syntax))
60 } else {
61 None
62 }
63 }
64
65 fn syntax(&self) -> &SyntaxNode {
66 &self.0
67 }
68}
69
70impl DisplayMath {
71 pub fn opening_marker(&self) -> Option<String> {
72 self.0.children_with_tokens().find_map(|child| {
73 child.into_token().and_then(|token| {
74 (token.kind() == SyntaxKind::DISPLAY_MATH_MARKER).then(|| token.text().to_string())
75 })
76 })
77 }
78
79 pub fn closing_marker(&self) -> Option<String> {
80 self.0
81 .children_with_tokens()
82 .filter_map(|child| child.into_token())
83 .filter(|token| token.kind() == SyntaxKind::DISPLAY_MATH_MARKER)
84 .nth(1)
85 .map(|token| token.text().to_string())
86 }
87
88 pub fn content(&self) -> String {
92 math_content_text(&self.0)
93 }
94
95 pub fn is_environment_form(&self) -> bool {
96 let opening = self.opening_marker().unwrap_or_default();
97 let closing = self.closing_marker().unwrap_or_default();
98 opening.starts_with("\\begin{") && closing.starts_with("\\end{")
99 }
100
101 pub fn has_unescaped_single_dollar_in_content(&self) -> bool {
102 let content = self.content();
103 let chars: Vec<char> = content.chars().collect();
104 let mut idx = 0usize;
105 let mut backslashes = 0usize;
106
107 while idx < chars.len() {
108 let ch = chars[idx];
109 if ch == '\\' {
110 backslashes += 1;
111 idx += 1;
112 continue;
113 }
114
115 let escaped = backslashes % 2 == 1;
116 backslashes = 0;
117 if ch == '$' && !escaped {
118 if idx + 1 < chars.len() && chars[idx + 1] == '$' {
119 idx += 2;
120 continue;
121 }
122 return true;
123 }
124 idx += 1;
125 }
126
127 false
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::parse;
135
136 #[test]
137 fn display_math_dollar_markers_and_content() {
138 let tree = parse("$$\nx^2 + y^2\n$$\n", None);
139 let math = tree
140 .descendants()
141 .find_map(DisplayMath::cast)
142 .expect("display math");
143
144 assert_eq!(math.opening_marker().as_deref(), Some("$$"));
145 assert_eq!(math.closing_marker().as_deref(), Some("$$"));
146 assert!(math.content().contains("x^2 + y^2"));
147 assert!(!math.is_environment_form());
148 }
149
150 #[test]
151 fn display_math_environment_form_detection() {
152 let tree = parse("\\begin{align}\na &= b\\\\\n\\end{align}\n", None);
153 let math = tree
154 .descendants()
155 .find_map(DisplayMath::cast)
156 .expect("display math");
157
158 assert!(math.is_environment_form());
159 assert_eq!(math.opening_marker().as_deref(), Some("\\begin{align}"));
160 assert_eq!(math.closing_marker().as_deref(), Some("\\end{align}\n"));
161 }
162
163 #[test]
164 fn display_math_detects_unescaped_single_dollar() {
165 let tree = parse("$$\nalpha $beta$ gamma\n$$\n", None);
166 let math = tree
167 .descendants()
168 .find_map(DisplayMath::cast)
169 .expect("display math");
170 assert!(math.has_unescaped_single_dollar_in_content());
171 }
172}