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