Skip to main content

panache_parser/syntax/
math.rs

1//! Math AST node wrappers.
2
3use 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}