Skip to main content

panache_parser/syntax/
links.rs

1//! Link and image AST node wrappers.
2
3use super::ast::support;
4use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
5
6pub struct Link(SyntaxNode);
7
8impl AstNode for Link {
9    type Language = PanacheLanguage;
10
11    fn can_cast(kind: SyntaxKind) -> bool {
12        kind == SyntaxKind::LINK
13    }
14
15    fn cast(syntax: SyntaxNode) -> Option<Self> {
16        if Self::can_cast(syntax.kind()) {
17            Some(Self(syntax))
18        } else {
19            None
20        }
21    }
22
23    fn syntax(&self) -> &SyntaxNode {
24        &self.0
25    }
26}
27
28impl Link {
29    /// Returns the link text node.
30    pub fn text(&self) -> Option<LinkText> {
31        support::child(&self.0)
32    }
33
34    /// Returns the link destination node.
35    pub fn dest(&self) -> Option<LinkDest> {
36        support::child(&self.0)
37    }
38
39    /// Returns the reference label for reference-style links.
40    pub fn reference(&self) -> Option<LinkRef> {
41        support::child(&self.0)
42    }
43}
44
45pub struct AutoLink(SyntaxNode);
46
47impl AstNode for AutoLink {
48    type Language = PanacheLanguage;
49
50    fn can_cast(kind: SyntaxKind) -> bool {
51        kind == SyntaxKind::AUTO_LINK
52    }
53
54    fn cast(syntax: SyntaxNode) -> Option<Self> {
55        if Self::can_cast(syntax.kind()) {
56            Some(Self(syntax))
57        } else {
58            None
59        }
60    }
61
62    fn syntax(&self) -> &SyntaxNode {
63        &self.0
64    }
65}
66
67impl AutoLink {
68    /// Returns the autolink target text without angle brackets.
69    pub fn target(&self) -> String {
70        self.0
71            .children_with_tokens()
72            .filter_map(|it| it.into_token())
73            .filter(|token| token.kind() == SyntaxKind::TEXT)
74            .map(|token| token.text().to_string())
75            .collect()
76    }
77}
78
79pub struct LinkText(SyntaxNode);
80
81impl AstNode for LinkText {
82    type Language = PanacheLanguage;
83
84    fn can_cast(kind: SyntaxKind) -> bool {
85        kind == SyntaxKind::LINK_TEXT
86    }
87
88    fn cast(syntax: SyntaxNode) -> Option<Self> {
89        if Self::can_cast(syntax.kind()) {
90            Some(Self(syntax))
91        } else {
92            None
93        }
94    }
95
96    fn syntax(&self) -> &SyntaxNode {
97        &self.0
98    }
99}
100
101impl LinkText {
102    /// Returns the text content.
103    pub fn text_content(&self) -> String {
104        self.0
105            .descendants_with_tokens()
106            .filter_map(|it| it.into_token())
107            .filter(|token| token.kind() == SyntaxKind::TEXT)
108            .map(|token| token.text().to_string())
109            .collect()
110    }
111}
112
113pub struct LinkDest(SyntaxNode);
114
115impl AstNode for LinkDest {
116    type Language = PanacheLanguage;
117
118    fn can_cast(kind: SyntaxKind) -> bool {
119        kind == SyntaxKind::LINK_DEST
120    }
121
122    fn cast(syntax: SyntaxNode) -> Option<Self> {
123        if Self::can_cast(syntax.kind()) {
124            Some(Self(syntax))
125        } else {
126            None
127        }
128    }
129
130    fn syntax(&self) -> &SyntaxNode {
131        &self.0
132    }
133}
134
135impl LinkDest {
136    /// Returns the URL/destination as a string (with surrounding parentheses).
137    pub fn url(&self) -> String {
138        self.0.text().to_string()
139    }
140
141    /// Returns the URL without parentheses.
142    pub fn url_content(&self) -> String {
143        let text = self.0.text().to_string();
144        text.trim_start_matches('(')
145            .trim_end_matches(')')
146            .to_string()
147    }
148
149    /// Returns the range for a hash-anchor id within destination text (without '#').
150    pub fn hash_anchor_id_range(&self) -> Option<rowan::TextRange> {
151        let text = self.0.text().to_string();
152        let hash_idx = text.find('#')?;
153        let after_hash = &text[hash_idx + 1..];
154        let id_len = after_hash
155            .chars()
156            .take_while(|ch| !ch.is_whitespace() && *ch != ')')
157            .map(char::len_utf8)
158            .sum::<usize>();
159        if id_len == 0 {
160            return None;
161        }
162        let node_start: usize = self.0.text_range().start().into();
163        let start = rowan::TextSize::from((node_start + hash_idx + 1) as u32);
164        let end = rowan::TextSize::from((node_start + hash_idx + 1 + id_len) as u32);
165        Some(rowan::TextRange::new(start, end))
166    }
167
168    /// Returns the hash-anchor id within destination text (without '#').
169    pub fn hash_anchor_id(&self) -> Option<String> {
170        let text = self.0.text().to_string();
171        let hash_idx = text.find('#')?;
172        let after_hash = &text[hash_idx + 1..];
173        let id_len = after_hash
174            .chars()
175            .take_while(|ch| !ch.is_whitespace() && *ch != ')')
176            .map(char::len_utf8)
177            .sum::<usize>();
178        if id_len == 0 {
179            return None;
180        }
181        Some(after_hash[..id_len].to_string())
182    }
183}
184
185pub struct LinkRef(SyntaxNode);
186
187impl AstNode for LinkRef {
188    type Language = PanacheLanguage;
189
190    fn can_cast(kind: SyntaxKind) -> bool {
191        kind == SyntaxKind::LINK_REF
192    }
193
194    fn cast(syntax: SyntaxNode) -> Option<Self> {
195        if Self::can_cast(syntax.kind()) {
196            Some(Self(syntax))
197        } else {
198            None
199        }
200    }
201
202    fn syntax(&self) -> &SyntaxNode {
203        &self.0
204    }
205}
206
207impl LinkRef {
208    /// Returns the reference label text.
209    pub fn label(&self) -> String {
210        self.0
211            .children_with_tokens()
212            .filter_map(|it| it.into_token())
213            .filter(|token| token.kind() == SyntaxKind::TEXT)
214            .map(|token| token.text().to_string())
215            .collect()
216    }
217
218    /// Returns the text range for the reference label (without brackets).
219    pub fn label_range(&self) -> Option<rowan::TextRange> {
220        self.0
221            .children_with_tokens()
222            .filter_map(|it| it.into_token())
223            .find(|token| token.kind() == SyntaxKind::TEXT)
224            .map(|token| token.text_range())
225    }
226
227    /// Returns the text range for the label value (without brackets).
228    pub fn label_value_range(&self) -> Option<rowan::TextRange> {
229        self.label_range()
230    }
231}
232
233pub struct ImageLink(SyntaxNode);
234
235impl AstNode for ImageLink {
236    type Language = PanacheLanguage;
237
238    fn can_cast(kind: SyntaxKind) -> bool {
239        kind == SyntaxKind::IMAGE_LINK
240    }
241
242    fn cast(syntax: SyntaxNode) -> Option<Self> {
243        if Self::can_cast(syntax.kind()) {
244            Some(Self(syntax))
245        } else {
246            None
247        }
248    }
249
250    fn syntax(&self) -> &SyntaxNode {
251        &self.0
252    }
253}
254
255impl ImageLink {
256    /// Returns the alt text node.
257    pub fn alt(&self) -> Option<ImageAlt> {
258        support::child(&self.0)
259    }
260
261    /// Returns the image destination.
262    pub fn dest(&self) -> Option<LinkDest> {
263        support::child(&self.0)
264    }
265
266    /// Returns the reference label for reference-style images.
267    pub fn reference(&self) -> Option<LinkRef> {
268        support::child(&self.0)
269    }
270
271    /// Returns the reference label text for reference-style images.
272    pub fn reference_label(&self) -> Option<String> {
273        self.reference().map(|link_ref| link_ref.label())
274    }
275
276    /// Returns the text range for the reference label in reference-style images.
277    pub fn reference_label_range(&self) -> Option<rowan::TextRange> {
278        self.reference().and_then(|link_ref| link_ref.label_range())
279    }
280}
281
282pub struct ImageAlt(SyntaxNode);
283
284impl AstNode for ImageAlt {
285    type Language = PanacheLanguage;
286
287    fn can_cast(kind: SyntaxKind) -> bool {
288        kind == SyntaxKind::IMAGE_ALT
289    }
290
291    fn cast(syntax: SyntaxNode) -> Option<Self> {
292        if Self::can_cast(syntax.kind()) {
293            Some(Self(syntax))
294        } else {
295            None
296        }
297    }
298
299    fn syntax(&self) -> &SyntaxNode {
300        &self.0
301    }
302}
303
304impl ImageAlt {
305    /// Returns the alt text content.
306    pub fn text(&self) -> String {
307        self.0
308            .descendants_with_tokens()
309            .filter_map(|it| it.into_token())
310            .filter(|token| token.kind() == SyntaxKind::TEXT)
311            .map(|token| token.text().to_string())
312            .collect()
313    }
314}
315
316pub struct Figure(SyntaxNode);
317
318impl AstNode for Figure {
319    type Language = PanacheLanguage;
320
321    fn can_cast(kind: SyntaxKind) -> bool {
322        kind == SyntaxKind::FIGURE
323    }
324
325    fn cast(syntax: SyntaxNode) -> Option<Self> {
326        if Self::can_cast(syntax.kind()) {
327            Some(Self(syntax))
328        } else {
329            None
330        }
331    }
332
333    fn syntax(&self) -> &SyntaxNode {
334        &self.0
335    }
336}
337
338impl Figure {
339    /// Returns the image link within the figure.
340    pub fn image(&self) -> Option<ImageLink> {
341        support::child(&self.0)
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::{AstNode, ImageLink};
348
349    #[test]
350    fn image_reference_label_and_range_are_extracted() {
351        let input = "![Alt text][img]";
352        let tree = crate::parse(input, None);
353        let image = tree
354            .descendants()
355            .find_map(ImageLink::cast)
356            .expect("image link");
357
358        assert_eq!(image.reference_label().as_deref(), Some("img"));
359
360        let range = image.reference_label_range().expect("label range");
361        let start: usize = range.start().into();
362        let end: usize = range.end().into();
363        assert_eq!(&input[start..end], "img");
364    }
365}