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 {
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 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}