panache_parser/syntax/
references.rs1use super::ast::support;
4use super::links::Link;
5use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
6
7pub struct ReferenceDefinition(SyntaxNode);
8
9impl AstNode for ReferenceDefinition {
10 type Language = PanacheLanguage;
11
12 fn can_cast(kind: SyntaxKind) -> bool {
13 kind == SyntaxKind::REFERENCE_DEFINITION
14 }
15
16 fn cast(syntax: SyntaxNode) -> Option<Self> {
17 if Self::can_cast(syntax.kind()) {
18 Some(Self(syntax))
19 } else {
20 None
21 }
22 }
23
24 fn syntax(&self) -> &SyntaxNode {
25 &self.0
26 }
27}
28
29impl ReferenceDefinition {
30 pub fn link(&self) -> Option<Link> {
32 support::child(&self.0)
33 }
34
35 pub fn label(&self) -> String {
37 self.link()
38 .and_then(|link| link.text())
39 .map(|text| text.text_content())
40 .unwrap_or_default()
41 }
42
43 pub fn destination(&self) -> Option<String> {
45 let tail = self
46 .0
47 .children_with_tokens()
48 .filter_map(|child| child.into_token())
49 .find(|token| token.kind() == SyntaxKind::TEXT)?
50 .text()
51 .to_string();
52
53 let after_colon = tail.trim_start().strip_prefix(':')?.trim_start();
54 if after_colon.is_empty() {
55 return None;
56 }
57
58 Some(after_colon.to_string())
59 }
60
61 pub fn label_value_range(&self) -> Option<rowan::TextRange> {
63 let link = self.link()?;
64
65 if let Some(range) = link
66 .reference()
67 .and_then(|reference| reference.label_value_range())
68 {
69 return Some(range);
70 }
71
72 link.text()?
73 .syntax()
74 .descendants_with_tokens()
75 .find_map(|elem| {
76 elem.into_token()
77 .filter(|token| token.kind() == SyntaxKind::TEXT)
78 .map(|token| token.text_range())
79 })
80 }
81}
82
83pub struct FootnoteReference(SyntaxNode);
84
85impl AstNode for FootnoteReference {
86 type Language = PanacheLanguage;
87
88 fn can_cast(kind: SyntaxKind) -> bool {
89 kind == SyntaxKind::FOOTNOTE_REFERENCE
90 }
91
92 fn cast(syntax: SyntaxNode) -> Option<Self> {
93 if Self::can_cast(syntax.kind()) {
94 Some(Self(syntax))
95 } else {
96 None
97 }
98 }
99
100 fn syntax(&self) -> &SyntaxNode {
101 &self.0
102 }
103}
104
105impl FootnoteReference {
106 pub fn id(&self) -> String {
108 if let Some(id) = self
109 .0
110 .children_with_tokens()
111 .filter_map(|child| child.into_token())
112 .find(|token| token.kind() == SyntaxKind::FOOTNOTE_LABEL_ID)
113 {
114 return id.text().to_string();
115 }
116
117 let tokens: Vec<_> = self
118 .0
119 .children_with_tokens()
120 .filter_map(|child| child.into_token())
121 .filter(|token| token.kind() == SyntaxKind::TEXT)
122 .map(|token| token.text().to_string())
123 .collect();
124
125 if tokens.len() >= 2 && tokens[0] == "[^" {
126 tokens[1].clone()
127 } else {
128 String::new()
129 }
130 }
131
132 pub fn id_range(&self) -> rowan::TextRange {
134 self.0.text_range()
135 }
136
137 pub fn id_value_range(&self) -> Option<rowan::TextRange> {
139 if let Some(id) = self
140 .0
141 .children_with_tokens()
142 .filter_map(|child| child.into_token())
143 .find(|token| token.kind() == SyntaxKind::FOOTNOTE_LABEL_ID)
144 {
145 return Some(id.text_range());
146 }
147
148 let tokens: Vec<_> = self
149 .0
150 .children_with_tokens()
151 .filter_map(|child| child.into_token())
152 .filter(|token| token.kind() == SyntaxKind::TEXT)
153 .collect();
154
155 if tokens.len() >= 2 && tokens[0].text() == "[^" {
156 Some(tokens[1].text_range())
157 } else {
158 None
159 }
160 }
161}
162
163pub struct FootnoteDefinition(SyntaxNode);
164
165impl AstNode for FootnoteDefinition {
166 type Language = PanacheLanguage;
167
168 fn can_cast(kind: SyntaxKind) -> bool {
169 kind == SyntaxKind::FOOTNOTE_DEFINITION
170 }
171
172 fn cast(syntax: SyntaxNode) -> Option<Self> {
173 if Self::can_cast(syntax.kind()) {
174 Some(Self(syntax))
175 } else {
176 None
177 }
178 }
179
180 fn syntax(&self) -> &SyntaxNode {
181 &self.0
182 }
183}
184
185impl FootnoteDefinition {
186 pub fn id(&self) -> String {
188 if let Some(id) = self
189 .0
190 .children_with_tokens()
191 .filter_map(|child| child.into_token())
192 .find(|token| token.kind() == SyntaxKind::FOOTNOTE_LABEL_ID)
193 {
194 return id.text().to_string();
195 }
196
197 self.0
198 .children_with_tokens()
199 .filter_map(|child| child.into_token())
200 .find(|token| token.kind() == SyntaxKind::FOOTNOTE_REFERENCE)
201 .and_then(|token| {
202 let text = token.text();
203 if text.starts_with("[^") && text.contains("]:") {
204 text.trim_start_matches("[^")
205 .split(']')
206 .next()
207 .map(String::from)
208 } else {
209 None
210 }
211 })
212 .unwrap_or_default()
213 }
214
215 pub fn id_value_range(&self) -> Option<rowan::TextRange> {
217 if let Some(id) = self
218 .0
219 .children_with_tokens()
220 .filter_map(|child| child.into_token())
221 .find(|token| token.kind() == SyntaxKind::FOOTNOTE_LABEL_ID)
222 {
223 return Some(id.text_range());
224 }
225
226 let marker = self
227 .0
228 .children_with_tokens()
229 .filter_map(|child| child.into_token())
230 .find(|token| token.kind() == SyntaxKind::FOOTNOTE_REFERENCE)?;
231
232 let marker_text = marker.text();
233 if !marker_text.starts_with("[^") {
234 return None;
235 }
236
237 let close_bracket = marker_text.find(']')?;
238 if close_bracket <= 2 {
239 return None;
240 }
241
242 if marker_text.as_bytes().get(close_bracket + 1) != Some(&b':') {
243 return None;
244 }
245
246 let token_start = marker.text_range().start();
247 let id_start = token_start + rowan::TextSize::from(2);
248 let id_end = token_start + rowan::TextSize::from(close_bracket as u32);
249 Some(rowan::TextRange::new(id_start, id_end))
250 }
251
252 pub fn content(&self) -> String {
255 self.0
257 .children_with_tokens()
258 .filter_map(|child| match child {
259 rowan::NodeOrToken::Node(node) => Some(node.text().to_string()),
260 rowan::NodeOrToken::Token(token)
261 if !matches!(
262 token.kind(),
263 SyntaxKind::FOOTNOTE_REFERENCE
264 | SyntaxKind::FOOTNOTE_LABEL_START
265 | SyntaxKind::FOOTNOTE_LABEL_ID
266 | SyntaxKind::FOOTNOTE_LABEL_END
267 | SyntaxKind::FOOTNOTE_LABEL_COLON
268 ) =>
269 {
270 Some(token.text().to_string())
271 }
272 _ => None,
273 })
274 .collect::<Vec<_>>()
275 .join("")
276 }
277
278 pub fn is_simple(&self) -> bool {
281 let content = self.content();
285
286 if content.contains("\n\n") {
288 return false;
289 }
290
291 if content
294 .lines()
295 .skip(1)
296 .any(|line| line.len() > 8 && line.starts_with(" "))
297 {
298 return false;
299 }
300
301 for line in content.lines().skip(1) {
303 let trimmed = line.trim_start();
304 if trimmed.starts_with("- ")
305 || trimmed.starts_with("* ")
306 || trimmed.starts_with("+ ")
307 || (trimmed
308 .chars()
309 .next()
310 .map(|c| c.is_ascii_digit())
311 .unwrap_or(false)
312 && trimmed.chars().skip(1).any(|c| c == '.'))
313 {
314 return false;
315 }
316 }
317
318 if self
320 .0
321 .descendants()
322 .any(|node| node.kind() == SyntaxKind::LIST)
323 {
324 return false;
325 }
326
327 true
328 }
329}
330
331pub struct InlineFootnote(SyntaxNode);
332
333impl AstNode for InlineFootnote {
334 type Language = PanacheLanguage;
335
336 fn can_cast(kind: SyntaxKind) -> bool {
337 kind == SyntaxKind::INLINE_FOOTNOTE
338 }
339
340 fn cast(syntax: SyntaxNode) -> Option<Self> {
341 if Self::can_cast(syntax.kind()) {
342 Some(Self(syntax))
343 } else {
344 None
345 }
346 }
347
348 fn syntax(&self) -> &SyntaxNode {
349 &self.0
350 }
351}
352
353impl InlineFootnote {
354 pub fn content(&self) -> String {
356 self.0
357 .children_with_tokens()
358 .filter_map(|child| {
359 if let Some(token) = child.as_token() {
360 if token.kind() != SyntaxKind::INLINE_FOOTNOTE_START
362 && token.kind() != SyntaxKind::INLINE_FOOTNOTE_END
363 {
364 Some(token.text().to_string())
365 } else {
366 None
367 }
368 } else {
369 child.as_node().map(|node| node.text().to_string())
371 }
372 })
373 .collect::<Vec<_>>()
374 .join("")
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::parse;
382
383 #[test]
384 fn test_reference_definition_destination() {
385 let input = "[ref]: https://example.com \"Title\"";
386 let root = parse(input, None);
387 let def = root
388 .descendants()
389 .find_map(ReferenceDefinition::cast)
390 .expect("Should find ReferenceDefinition");
391
392 assert_eq!(def.label(), "ref");
393 assert_eq!(
394 def.destination().as_deref(),
395 Some("https://example.com \"Title\"")
396 );
397 assert!(def.label_value_range().is_some());
398 }
399
400 #[test]
401 fn test_footnote_definition_single_line() {
402 let input = "[^1]: This is a simple footnote.";
403 let root = parse(input, None);
404 let def = root
405 .descendants()
406 .find_map(FootnoteDefinition::cast)
407 .expect("Should find FootnoteDefinition");
408
409 assert_eq!(def.id(), "1");
410 assert_eq!(
411 def.id_value_range()
412 .map(|range| {
413 let start: usize = range.start().into();
414 let end: usize = range.end().into();
415 input[start..end].to_string()
416 })
417 .as_deref(),
418 Some("1")
419 );
420 assert_eq!(def.content().trim(), "This is a simple footnote.");
421 assert!(def.is_simple(), "Single line footnote should be simple");
422 }
423
424 #[test]
425 fn test_footnote_definition_multiline() {
426 let input = "[^1]: First line\n Second line";
427 let root = parse(input, None);
428 let def = root
429 .descendants()
430 .find_map(FootnoteDefinition::cast)
431 .expect("Should find FootnoteDefinition");
432
433 assert_eq!(def.id(), "1");
434 let content = def.content();
435 assert!(content.contains("First line"));
436 assert!(content.contains("Second line"));
437 assert!(def.is_simple(), "Continuation lines should still be simple");
438 }
439
440 #[test]
441 fn test_footnote_definition_with_formatting() {
442 let input = "[^note]: Text with *emphasis* and `code`.";
443 let root = parse(input, None);
444 let def = root
445 .descendants()
446 .find_map(FootnoteDefinition::cast)
447 .expect("Should find FootnoteDefinition");
448
449 assert_eq!(def.id(), "note");
450 assert_eq!(
451 def.id_value_range()
452 .map(|range| {
453 let start: usize = range.start().into();
454 let end: usize = range.end().into();
455 input[start..end].to_string()
456 })
457 .as_deref(),
458 Some("note")
459 );
460 let content = def.content();
461 assert!(content.contains("*emphasis*"));
462 assert!(content.contains("`code`"));
463 }
464
465 #[test]
466 fn test_footnote_definition_empty() {
467 let input = "[^1]: ";
468 let root = parse(input, None);
469 let def = root
470 .descendants()
471 .find_map(FootnoteDefinition::cast)
472 .expect("Should find FootnoteDefinition");
473
474 assert_eq!(def.id(), "1");
475 assert!(def.content().trim().is_empty());
476 }
477
478 #[test]
479 fn test_footnote_reference_id() {
480 let input = "[^test]";
481 let root = parse(input, None);
482 let ref_node = root
483 .descendants()
484 .find_map(FootnoteReference::cast)
485 .expect("Should find FootnoteReference");
486
487 assert_eq!(ref_node.id(), "test");
488 assert_eq!(
489 ref_node
490 .id_value_range()
491 .map(|range| {
492 let start: usize = range.start().into();
493 let end: usize = range.end().into();
494 input[start..end].to_string()
495 })
496 .as_deref(),
497 Some("test")
498 );
499 }
500
501 #[test]
502 fn test_footnote_definition_is_simple() {
503 let input = "[^1]: Simple text.";
505 let root = parse(input, None);
506 let def = root
507 .descendants()
508 .find_map(FootnoteDefinition::cast)
509 .unwrap();
510 assert!(def.is_simple());
511
512 let input2 = "[^1]: First line\n continuation.";
514 let root2 = parse(input2, None);
515 let def2 = root2
516 .descendants()
517 .find_map(FootnoteDefinition::cast)
518 .unwrap();
519 assert!(def2.is_simple());
520 }
521
522 #[test]
523 fn test_footnote_definition_is_complex() {
524 let input = "[^1]: First para.\n\n Second para.";
526 let root = parse(input, None);
527 let def = root
528 .descendants()
529 .find_map(FootnoteDefinition::cast)
530 .unwrap();
531 assert!(!def.is_simple(), "Multi-paragraph should not be simple");
532
533 let input2 = "[^1]: Text\n - Item 1\n - Item 2";
535 let root2 = parse(input2, None);
536 let def2 = root2
537 .descendants()
538 .find_map(FootnoteDefinition::cast)
539 .unwrap();
540 assert!(!def2.is_simple(), "Footnote with list should not be simple");
541
542 let input3 = "[^1]: Text\n\n code block";
544 let root3 = parse(input3, None);
545 let def3 = root3
546 .descendants()
547 .find_map(FootnoteDefinition::cast)
548 .unwrap();
549 assert!(
550 !def3.is_simple(),
551 "Footnote with code block should not be simple"
552 );
553 }
554
555 #[test]
556 fn test_inline_footnote_content() {
557 let input = "Text^[This is an inline note] more text.";
558 let root = parse(input, None);
559 let inline = root
560 .descendants()
561 .find_map(InlineFootnote::cast)
562 .expect("Should find InlineFootnote");
563
564 assert_eq!(inline.content(), "This is an inline note");
565 }
566
567 #[test]
568 fn test_inline_footnote_with_formatting() {
569 let input = "Text^[Note with *emphasis* and `code`] more.";
570 let root = parse(input, None);
571 let inline = root
572 .descendants()
573 .find_map(InlineFootnote::cast)
574 .expect("Should find InlineFootnote");
575
576 let content = inline.content();
577 assert!(content.contains("emphasis"));
578 assert!(content.contains("code"));
579 }
580}