panache_parser/syntax/
links.rs1use 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 pub fn text(&self) -> Option<LinkText> {
31 support::child(&self.0)
32 }
33
34 pub fn dest(&self) -> Option<LinkDest> {
36 support::child(&self.0)
37 }
38
39 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 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 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 pub fn raw_label(&self) -> String {
123 self.0.text().to_string()
124 }
125}
126
127pub struct LinkDest(SyntaxNode);
128
129impl AstNode for LinkDest {
130 type Language = PanacheLanguage;
131
132 fn can_cast(kind: SyntaxKind) -> bool {
133 kind == SyntaxKind::LINK_DEST
134 }
135
136 fn cast(syntax: SyntaxNode) -> Option<Self> {
137 if Self::can_cast(syntax.kind()) {
138 Some(Self(syntax))
139 } else {
140 None
141 }
142 }
143
144 fn syntax(&self) -> &SyntaxNode {
145 &self.0
146 }
147}
148
149impl LinkDest {
150 pub fn url(&self) -> String {
152 self.0.text().to_string()
153 }
154
155 pub fn url_content(&self) -> String {
157 let text = self.0.text().to_string();
158 text.trim_start_matches('(')
159 .trim_end_matches(')')
160 .to_string()
161 }
162
163 pub fn hash_anchor_id_range(&self) -> Option<rowan::TextRange> {
165 let text = self.0.text().to_string();
166 let hash_idx = text.find('#')?;
167 let after_hash = &text[hash_idx + 1..];
168 let id_len = after_hash
169 .chars()
170 .take_while(|ch| !ch.is_whitespace() && *ch != ')')
171 .map(char::len_utf8)
172 .sum::<usize>();
173 if id_len == 0 {
174 return None;
175 }
176 let node_start: usize = self.0.text_range().start().into();
177 let start = rowan::TextSize::from((node_start + hash_idx + 1) as u32);
178 let end = rowan::TextSize::from((node_start + hash_idx + 1 + id_len) as u32);
179 Some(rowan::TextRange::new(start, end))
180 }
181
182 pub fn hash_anchor_id(&self) -> Option<String> {
184 let text = self.0.text().to_string();
185 let hash_idx = text.find('#')?;
186 let after_hash = &text[hash_idx + 1..];
187 let id_len = after_hash
188 .chars()
189 .take_while(|ch| !ch.is_whitespace() && *ch != ')')
190 .map(char::len_utf8)
191 .sum::<usize>();
192 if id_len == 0 {
193 return None;
194 }
195 Some(after_hash[..id_len].to_string())
196 }
197}
198
199pub struct LinkRef(SyntaxNode);
200
201impl AstNode for LinkRef {
202 type Language = PanacheLanguage;
203
204 fn can_cast(kind: SyntaxKind) -> bool {
205 kind == SyntaxKind::LINK_REF
206 }
207
208 fn cast(syntax: SyntaxNode) -> Option<Self> {
209 if Self::can_cast(syntax.kind()) {
210 Some(Self(syntax))
211 } else {
212 None
213 }
214 }
215
216 fn syntax(&self) -> &SyntaxNode {
217 &self.0
218 }
219}
220
221impl LinkRef {
222 pub fn label(&self) -> String {
224 self.0
225 .children_with_tokens()
226 .filter_map(|it| it.into_token())
227 .filter(|token| token.kind() == SyntaxKind::TEXT)
228 .map(|token| token.text().to_string())
229 .collect()
230 }
231
232 pub fn label_range(&self) -> Option<rowan::TextRange> {
234 self.0
235 .children_with_tokens()
236 .filter_map(|it| it.into_token())
237 .find(|token| token.kind() == SyntaxKind::TEXT)
238 .map(|token| token.text_range())
239 }
240
241 pub fn label_value_range(&self) -> Option<rowan::TextRange> {
243 self.label_range()
244 }
245}
246
247pub struct ImageLink(SyntaxNode);
248
249impl AstNode for ImageLink {
250 type Language = PanacheLanguage;
251
252 fn can_cast(kind: SyntaxKind) -> bool {
253 kind == SyntaxKind::IMAGE_LINK
254 }
255
256 fn cast(syntax: SyntaxNode) -> Option<Self> {
257 if Self::can_cast(syntax.kind()) {
258 Some(Self(syntax))
259 } else {
260 None
261 }
262 }
263
264 fn syntax(&self) -> &SyntaxNode {
265 &self.0
266 }
267}
268
269impl ImageLink {
270 pub fn alt(&self) -> Option<ImageAlt> {
272 support::child(&self.0)
273 }
274
275 pub fn dest(&self) -> Option<LinkDest> {
277 support::child(&self.0)
278 }
279
280 pub fn reference(&self) -> Option<LinkRef> {
282 support::child(&self.0)
283 }
284
285 pub fn reference_label(&self) -> Option<String> {
287 self.reference().map(|link_ref| link_ref.label())
288 }
289
290 pub fn reference_label_range(&self) -> Option<rowan::TextRange> {
292 self.reference().and_then(|link_ref| link_ref.label_range())
293 }
294}
295
296pub struct ImageAlt(SyntaxNode);
297
298impl AstNode for ImageAlt {
299 type Language = PanacheLanguage;
300
301 fn can_cast(kind: SyntaxKind) -> bool {
302 kind == SyntaxKind::IMAGE_ALT
303 }
304
305 fn cast(syntax: SyntaxNode) -> Option<Self> {
306 if Self::can_cast(syntax.kind()) {
307 Some(Self(syntax))
308 } else {
309 None
310 }
311 }
312
313 fn syntax(&self) -> &SyntaxNode {
314 &self.0
315 }
316}
317
318impl ImageAlt {
319 pub fn text(&self) -> String {
321 self.0
322 .descendants_with_tokens()
323 .filter_map(|it| it.into_token())
324 .filter(|token| token.kind() == SyntaxKind::TEXT)
325 .map(|token| token.text().to_string())
326 .collect()
327 }
328}
329
330pub struct Figure(SyntaxNode);
331
332impl AstNode for Figure {
333 type Language = PanacheLanguage;
334
335 fn can_cast(kind: SyntaxKind) -> bool {
336 kind == SyntaxKind::FIGURE
337 }
338
339 fn cast(syntax: SyntaxNode) -> Option<Self> {
340 if Self::can_cast(syntax.kind()) {
341 Some(Self(syntax))
342 } else {
343 None
344 }
345 }
346
347 fn syntax(&self) -> &SyntaxNode {
348 &self.0
349 }
350}
351
352impl Figure {
353 pub fn image(&self) -> Option<ImageLink> {
355 support::child(&self.0)
356 }
357}
358
359pub struct UnresolvedReference(SyntaxNode);
369
370impl AstNode for UnresolvedReference {
371 type Language = PanacheLanguage;
372
373 fn can_cast(kind: SyntaxKind) -> bool {
374 kind == SyntaxKind::UNRESOLVED_REFERENCE
375 }
376
377 fn cast(syntax: SyntaxNode) -> Option<Self> {
378 if Self::can_cast(syntax.kind()) {
379 Some(Self(syntax))
380 } else {
381 None
382 }
383 }
384
385 fn syntax(&self) -> &SyntaxNode {
386 &self.0
387 }
388}
389
390impl UnresolvedReference {
391 pub fn is_image(&self) -> bool {
395 self.0.text().to_string().as_bytes().first() == Some(&b'!')
396 }
397
398 pub fn text(&self) -> String {
402 if let Some(link_text) = support::child::<LinkText>(&self.0) {
406 return link_text.text_content();
407 }
408 if let Some(image_alt) = support::child::<ImageAlt>(&self.0) {
409 return image_alt.text();
410 }
411 self.0
412 .descendants_with_tokens()
413 .filter_map(|it| it.into_token())
414 .filter(|token| token.kind() == SyntaxKind::TEXT)
415 .map(|token| token.text().to_string())
416 .collect()
417 }
418
419 pub fn label(&self) -> Option<String> {
423 support::child::<LinkRef>(&self.0).map(|r| r.label())
424 }
425
426 pub fn text_range(&self) -> rowan::TextRange {
428 self.0.text_range()
429 }
430}
431
432pub struct WikiLink(SyntaxNode);
438
439impl AstNode for WikiLink {
440 type Language = PanacheLanguage;
441
442 fn can_cast(kind: SyntaxKind) -> bool {
443 matches!(kind, SyntaxKind::WIKI_LINK | SyntaxKind::IMAGE_WIKI_LINK)
444 }
445
446 fn cast(syntax: SyntaxNode) -> Option<Self> {
447 if Self::can_cast(syntax.kind()) {
448 Some(Self(syntax))
449 } else {
450 None
451 }
452 }
453
454 fn syntax(&self) -> &SyntaxNode {
455 &self.0
456 }
457}
458
459impl WikiLink {
460 pub fn is_image(&self) -> bool {
463 self.0.kind() == SyntaxKind::IMAGE_WIKI_LINK
464 }
465
466 pub fn url(&self) -> Option<String> {
468 self.0
469 .children()
470 .find(|n| n.kind() == SyntaxKind::WIKI_LINK_URL)
471 .map(|n| n.text().to_string())
472 }
473
474 pub fn title(&self) -> Option<String> {
476 self.0
477 .children()
478 .find(|n| n.kind() == SyntaxKind::WIKI_LINK_TITLE)
479 .map(|n| n.text().to_string())
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::{AstNode, ImageLink, UnresolvedReference};
486
487 #[test]
488 fn image_reference_label_and_range_are_extracted() {
489 let input = "![Alt text][img]\n\n[img]: /url\n";
491 let tree = crate::parse(input, None);
492 let image = tree
493 .descendants()
494 .find_map(ImageLink::cast)
495 .expect("image link");
496
497 assert_eq!(image.reference_label().as_deref(), Some("img"));
498
499 let range = image.reference_label_range().expect("label range");
500 let start: usize = range.start().into();
501 let end: usize = range.end().into();
502 assert_eq!(&input[start..end], "img");
503 }
504
505 #[test]
506 fn unresolved_image_reference_label_is_extracted() {
507 let input = "![Alt text][img]";
510 let tree = crate::parse(input, None);
511 let unresolved = tree
512 .descendants()
513 .find_map(UnresolvedReference::cast)
514 .expect("unresolved reference");
515
516 assert!(unresolved.is_image(), "expected image-shape unresolved ref");
517 assert_eq!(unresolved.label().as_deref(), Some("img"));
518 }
519
520 #[test]
521 fn unresolved_link_reference_label_is_extracted() {
522 let input = "[link text][missing]";
523 let tree = crate::parse(input, None);
524 let unresolved = tree
525 .descendants()
526 .find_map(UnresolvedReference::cast)
527 .expect("unresolved reference");
528
529 assert!(!unresolved.is_image(), "expected link-shape unresolved ref");
530 assert_eq!(unresolved.label().as_deref(), Some("missing"));
531 }
532
533 #[test]
534 fn unresolved_shortcut_reference_has_no_label() {
535 let input = "[no refdef]";
536 let tree = crate::parse(input, None);
537 let unresolved = tree
538 .descendants()
539 .find_map(UnresolvedReference::cast)
540 .expect("unresolved reference");
541
542 assert!(!unresolved.is_image());
543 assert!(unresolved.label().is_none());
544 }
545}