panache_parser/syntax/
inlines.rs1use 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 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}