Skip to main content

panache_parser/syntax/
inlines.rs

1//! Inline AST node wrappers.
2
3use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
4
5pub struct InlineMath(SyntaxNode);
6
7impl AstNode for InlineMath {
8    type Language = PanacheLanguage;
9
10    fn can_cast(kind: SyntaxKind) -> bool {
11        kind == SyntaxKind::INLINE_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 InlineMath {
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::INLINE_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::INLINE_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::INLINE_MATH_MARKER)
50            .map(|token| token.text().to_string())
51            .collect::<Vec<_>>()
52            .join("")
53    }
54
55    pub fn content_range(&self) -> Option<rowan::TextRange> {
56        let mut markers = self
57            .0
58            .children_with_tokens()
59            .filter_map(|child| child.into_token())
60            .filter(|token| token.kind() == SyntaxKind::INLINE_MATH_MARKER);
61
62        let start = markers.next()?.text_range().end();
63        let end = markers.next()?.text_range().start();
64        (start <= end).then(|| rowan::TextRange::new(start, end))
65    }
66}
67
68pub struct CodeSpan(SyntaxNode);
69
70impl AstNode for CodeSpan {
71    type Language = PanacheLanguage;
72
73    fn can_cast(kind: SyntaxKind) -> bool {
74        kind == SyntaxKind::INLINE_CODE
75    }
76
77    fn cast(syntax: SyntaxNode) -> Option<Self> {
78        if Self::can_cast(syntax.kind()) {
79            Some(Self(syntax))
80        } else {
81            None
82        }
83    }
84
85    fn syntax(&self) -> &SyntaxNode {
86        &self.0
87    }
88}
89
90impl CodeSpan {
91    pub fn marker(&self) -> Option<String> {
92        self.0.children_with_tokens().find_map(|child| {
93            child.into_token().and_then(|token| {
94                (token.kind() == SyntaxKind::INLINE_CODE_MARKER).then(|| token.text().to_string())
95            })
96        })
97    }
98
99    pub fn content(&self) -> String {
100        self.0
101            .children_with_tokens()
102            .filter_map(|child| child.into_token())
103            .filter(|token| token.kind() == SyntaxKind::INLINE_CODE_CONTENT)
104            .map(|token| token.text().to_string())
105            .collect::<Vec<_>>()
106            .join("")
107    }
108
109    pub fn content_range(&self) -> Option<rowan::TextRange> {
110        let mut markers = self
111            .0
112            .children_with_tokens()
113            .filter_map(|child| child.into_token())
114            .filter(|token| token.kind() == SyntaxKind::INLINE_CODE_MARKER);
115
116        let start = markers.next()?.text_range().end();
117        let end = markers.next()?.text_range().start();
118        (start <= end).then(|| rowan::TextRange::new(start, end))
119    }
120}
121
122pub struct InlineHtml(SyntaxNode);
123
124impl AstNode for InlineHtml {
125    type Language = PanacheLanguage;
126
127    fn can_cast(kind: SyntaxKind) -> bool {
128        kind == SyntaxKind::INLINE_HTML
129    }
130
131    fn cast(syntax: SyntaxNode) -> Option<Self> {
132        if Self::can_cast(syntax.kind()) {
133            Some(Self(syntax))
134        } else {
135            None
136        }
137    }
138
139    fn syntax(&self) -> &SyntaxNode {
140        &self.0
141    }
142}
143
144impl InlineHtml {
145    pub fn verbatim(&self) -> String {
146        self.0
147            .children_with_tokens()
148            .filter_map(|child| child.into_token())
149            .filter(|token| token.kind() == SyntaxKind::INLINE_HTML_CONTENT)
150            .map(|token| token.text().to_string())
151            .collect()
152    }
153
154    pub fn is_comment(&self) -> bool {
155        self.0
156            .children_with_tokens()
157            .filter_map(|child| child.into_token())
158            .find(|token| token.kind() == SyntaxKind::INLINE_HTML_CONTENT)
159            .is_some_and(|token| token.text().starts_with("<!--"))
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn inline_html_discriminates_comments_from_tags() {
169        // Use <br/> rather than <span>: pandoc-dialect lifts <span>...</span>
170        // to its own INLINE_HTML_SPAN kind, so it wouldn't surface here.
171        let input = "Hi <!-- x --> <br/>\n";
172        let tree = crate::parse(input, None);
173        let spans: Vec<_> = tree.descendants().filter_map(InlineHtml::cast).collect();
174        assert_eq!(spans.len(), 2, "expected 2 InlineHtml nodes");
175        assert!(spans[0].is_comment(), "first span should be comment");
176        assert_eq!(spans[0].verbatim(), "<!-- x -->");
177        assert!(!spans[1].is_comment(), "second span should not be comment");
178        assert_eq!(spans[1].verbatim(), "<br/>");
179    }
180
181    #[test]
182    fn inline_math_extracts_markers_and_content() {
183        let input = "Before $x^2 + y^2$ after\n";
184        let tree = crate::parse(input, None);
185        let math = tree
186            .descendants()
187            .find_map(InlineMath::cast)
188            .expect("inline math");
189
190        assert_eq!(math.opening_marker().as_deref(), Some("$"));
191        assert_eq!(math.closing_marker().as_deref(), Some("$"));
192        assert_eq!(math.content(), "x^2 + y^2");
193        let range = math.content_range().expect("content range");
194        let start: usize = range.start().into();
195        let end: usize = range.end().into();
196        assert_eq!(&input[start..end], "x^2 + y^2");
197    }
198
199    #[test]
200    fn code_span_extracts_marker_and_content() {
201        let input = "Use `code` here\n";
202        let tree = crate::parse(input, None);
203        let code = tree
204            .descendants()
205            .find_map(CodeSpan::cast)
206            .expect("code span");
207
208        assert_eq!(code.marker().as_deref(), Some("`"));
209        assert_eq!(code.content(), "code");
210        let range = code.content_range().expect("content range");
211        let start: usize = range.start().into();
212        let end: usize = range.end().into();
213        assert_eq!(&input[start..end], "code");
214    }
215}