1use crate::inline::{extract_references, PositionedReference};
2use lex_core::lex::ast::traits::AstNode;
3use lex_core::lex::ast::{
4 Annotation, ContentItem, Definition, Document, Position, Session, TextContent,
5};
6
7pub fn for_each_text_content<F>(document: &Document, f: &mut F)
13where
14 F: FnMut(&TextContent),
15{
16 if let Some(title) = &document.title {
17 f(&title.content);
18 }
19 for annotation in document.annotations() {
20 visit_annotation_text(annotation, f);
21 }
22 visit_session_text(&document.root, true, f);
23}
24
25pub fn for_each_annotation<F>(document: &Document, f: &mut F)
35where
36 F: FnMut(&Annotation),
37{
38 for annotation in document.annotations() {
39 visit_annotation_recursive(annotation, f);
40 }
41 visit_session_annotations(&document.root, f);
42}
43
44pub fn collect_all_annotations(document: &Document) -> Vec<&Annotation> {
51 let mut annotations = Vec::new();
52 for annotation in document.annotations() {
53 collect_annotation_recursive(annotation, &mut annotations);
54 }
55 collect_annotations_into(&document.root, &mut annotations);
56 annotations
57}
58
59fn collect_annotations_into<'a>(session: &'a Session, out: &mut Vec<&'a Annotation>) {
60 for annotation in session.annotations() {
61 collect_annotation_recursive(annotation, out);
62 }
63 for child in session.children.iter() {
64 collect_content_annotations(child, out);
65 }
66}
67
68fn collect_annotation_recursive<'a>(annotation: &'a Annotation, out: &mut Vec<&'a Annotation>) {
69 out.push(annotation);
70 for child in annotation.children.iter() {
71 collect_content_annotations(child, out);
72 }
73}
74
75fn collect_content_annotations<'a>(item: &'a ContentItem, out: &mut Vec<&'a Annotation>) {
76 match item {
77 ContentItem::Annotation(annotation) => {
78 collect_annotation_recursive(annotation, out);
79 }
80 ContentItem::Paragraph(paragraph) => {
81 for annotation in paragraph.annotations() {
82 collect_annotation_recursive(annotation, out);
83 }
84 for line in ¶graph.lines {
85 collect_content_annotations(line, out);
86 }
87 }
88 ContentItem::List(list) => {
89 for annotation in list.annotations() {
90 collect_annotation_recursive(annotation, out);
91 }
92 for entry in &list.items {
93 collect_content_annotations(entry, out);
94 }
95 }
96 ContentItem::ListItem(list_item) => {
97 for annotation in list_item.annotations() {
98 collect_annotation_recursive(annotation, out);
99 }
100 for child in list_item.children.iter() {
101 collect_content_annotations(child, out);
102 }
103 }
104 ContentItem::Definition(definition) => {
105 for annotation in definition.annotations() {
106 collect_annotation_recursive(annotation, out);
107 }
108 for child in definition.children.iter() {
109 collect_content_annotations(child, out);
110 }
111 }
112 ContentItem::Session(session) => collect_annotations_into(session, out),
113 ContentItem::VerbatimBlock(verbatim) => {
114 for annotation in verbatim.annotations() {
115 collect_annotation_recursive(annotation, out);
116 }
117 }
118 ContentItem::Table(table) => {
119 for annotation in table.annotations() {
120 collect_annotation_recursive(annotation, out);
121 }
122 }
123 ContentItem::TextLine(_)
124 | ContentItem::VerbatimLine(_)
125 | ContentItem::BlankLineGroup(_) => {}
126 }
127}
128
129fn visit_annotation_recursive<F>(annotation: &Annotation, f: &mut F)
130where
131 F: FnMut(&Annotation),
132{
133 f(annotation);
134 for child in annotation.children.iter() {
135 visit_content_annotations(child, f);
136 }
137}
138
139fn visit_session_annotations<F>(session: &Session, f: &mut F)
140where
141 F: FnMut(&Annotation),
142{
143 for annotation in session.annotations() {
144 visit_annotation_recursive(annotation, f);
145 }
146 for child in session.children.iter() {
147 visit_content_annotations(child, f);
148 }
149}
150
151fn visit_content_annotations<F>(item: &ContentItem, f: &mut F)
152where
153 F: FnMut(&Annotation),
154{
155 match item {
156 ContentItem::Annotation(annotation) => {
157 visit_annotation_recursive(annotation, f);
158 }
159 ContentItem::Paragraph(paragraph) => {
160 for annotation in paragraph.annotations() {
161 visit_annotation_recursive(annotation, f);
162 }
163 for line in ¶graph.lines {
164 visit_content_annotations(line, f);
165 }
166 }
167 ContentItem::List(list) => {
168 for annotation in list.annotations() {
169 visit_annotation_recursive(annotation, f);
170 }
171 for entry in &list.items {
172 visit_content_annotations(entry, f);
173 }
174 }
175 ContentItem::ListItem(list_item) => {
176 for annotation in list_item.annotations() {
177 visit_annotation_recursive(annotation, f);
178 }
179 for child in list_item.children.iter() {
180 visit_content_annotations(child, f);
181 }
182 }
183 ContentItem::Definition(definition) => {
184 for annotation in definition.annotations() {
185 visit_annotation_recursive(annotation, f);
186 }
187 for child in definition.children.iter() {
188 visit_content_annotations(child, f);
189 }
190 }
191 ContentItem::Session(session) => visit_session_annotations(session, f),
192 ContentItem::VerbatimBlock(verbatim) => {
193 for annotation in verbatim.annotations() {
194 visit_annotation_recursive(annotation, f);
195 }
196 }
197 ContentItem::Table(table) => {
198 for annotation in table.annotations() {
199 visit_annotation_recursive(annotation, f);
200 }
201 }
202 ContentItem::TextLine(_)
203 | ContentItem::VerbatimLine(_)
204 | ContentItem::BlankLineGroup(_) => {}
205 }
206}
207
208pub fn find_definition_by_subject<'a>(
209 document: &'a Document,
210 target: &str,
211) -> Option<&'a Definition> {
212 find_definitions_by_subject(document, target)
213 .into_iter()
214 .next()
215}
216
217pub fn find_definitions_by_subject<'a>(
218 document: &'a Document,
219 target: &str,
220) -> Vec<&'a Definition> {
221 let normalized = normalize_key(target);
222 if normalized.is_empty() {
223 return Vec::new();
224 }
225 let mut matches = Vec::new();
226 for annotation in document.annotations() {
227 collect_definitions(annotation.children.iter(), &normalized, &mut matches);
228 }
229 collect_definitions(document.root.children.iter(), &normalized, &mut matches);
230 matches
231}
232
233pub fn find_definition_at_position(document: &Document, position: Position) -> Option<&Definition> {
234 for annotation in document.annotations() {
235 if let Some(definition) = find_definition_in_items(annotation.children.iter(), position) {
236 return Some(definition);
237 }
238 }
239 find_definition_in_items(document.root.children.iter(), position)
240}
241
242pub fn find_annotation_at_position(document: &Document, position: Position) -> Option<&Annotation> {
243 for annotation in document.annotations() {
244 if annotation.header_location().contains(position) {
245 return Some(annotation);
246 }
247 if let Some(found) = find_annotation_in_items(annotation.children.iter(), position) {
248 return Some(found);
249 }
250 }
251 find_annotation_in_session(&document.root, position, true)
252}
253
254pub fn find_session_at_position(document: &Document, position: Position) -> Option<&Session> {
255 find_session_in_branch(&document.root, position, true)
256}
257
258pub fn find_sessions_by_identifier<'a>(
259 document: &'a Document,
260 identifier: &str,
261) -> Vec<&'a Session> {
262 let normalized = normalize_key(identifier);
263 if normalized.is_empty() {
264 return Vec::new();
265 }
266 let mut matches = Vec::new();
267 collect_sessions_by_identifier(&document.root, &normalized, &mut matches, true);
268 matches
269}
270
271pub fn session_identifier(session: &Session) -> Option<String> {
272 extract_session_identifier(session.title.as_string())
273}
274
275pub fn reference_at_position(
276 document: &Document,
277 position: Position,
278) -> Option<PositionedReference> {
279 let mut result = None;
280 for_each_text_content(document, &mut |text| {
281 if result.is_some() {
282 return;
283 }
284 for reference in extract_references(text) {
285 if reference.range.contains(position) {
286 result = Some(reference);
287 break;
288 }
289 }
290 });
291 result
292}
293
294fn visit_session_text<F>(session: &Session, is_root: bool, f: &mut F)
295where
296 F: FnMut(&TextContent),
297{
298 if !is_root {
299 f(&session.title);
300 }
301 for annotation in session.annotations() {
302 visit_annotation_text(annotation, f);
303 }
304 for child in session.children.iter() {
305 visit_content_text(child, f);
306 }
307}
308
309fn visit_annotation_text<F>(annotation: &Annotation, f: &mut F)
310where
311 F: FnMut(&TextContent),
312{
313 for child in annotation.children.iter() {
314 visit_content_text(child, f);
315 }
316}
317
318fn visit_content_text<F>(item: &ContentItem, f: &mut F)
319where
320 F: FnMut(&TextContent),
321{
322 match item {
323 ContentItem::Paragraph(paragraph) => {
324 for line in ¶graph.lines {
325 if let ContentItem::TextLine(text_line) = line {
326 f(&text_line.content);
327 }
328 }
329 for annotation in paragraph.annotations() {
330 visit_annotation_text(annotation, f);
331 }
332 }
333 ContentItem::Session(session) => visit_session_text(session, false, f),
334 ContentItem::List(list) => {
335 for annotation in list.annotations() {
336 visit_annotation_text(annotation, f);
337 }
338 for entry in &list.items {
339 if let ContentItem::ListItem(list_item) = entry {
340 for text in &list_item.text {
341 f(text);
342 }
343 for annotation in list_item.annotations() {
344 visit_annotation_text(annotation, f);
345 }
346 for child in list_item.children.iter() {
347 visit_content_text(child, f);
348 }
349 }
350 }
351 }
352 ContentItem::ListItem(list_item) => {
353 for text in &list_item.text {
354 f(text);
355 }
356 for annotation in list_item.annotations() {
357 visit_annotation_text(annotation, f);
358 }
359 for child in list_item.children.iter() {
360 visit_content_text(child, f);
361 }
362 }
363 ContentItem::Definition(definition) => {
364 f(&definition.subject);
365 for annotation in definition.annotations() {
366 visit_annotation_text(annotation, f);
367 }
368 for child in definition.children.iter() {
369 visit_content_text(child, f);
370 }
371 }
372 ContentItem::Annotation(annotation) => visit_annotation_text(annotation, f),
373 ContentItem::VerbatimBlock(verbatim) => {
374 f(&verbatim.subject);
375 for annotation in verbatim.annotations() {
376 visit_annotation_text(annotation, f);
377 }
378 }
379 ContentItem::Table(table) => {
380 f(&table.subject);
381 for row in table.all_rows() {
382 for cell in &row.cells {
383 f(&cell.content);
384 }
385 }
386 for annotation in table.annotations() {
387 visit_annotation_text(annotation, f);
388 }
389 }
390 ContentItem::TextLine(_)
391 | ContentItem::VerbatimLine(_)
392 | ContentItem::BlankLineGroup(_) => {}
393 }
394}
395
396fn collect_definitions<'a>(
397 items: impl Iterator<Item = &'a ContentItem>,
398 target: &str,
399 matches: &mut Vec<&'a Definition>,
400) {
401 for item in items {
402 collect_definitions_in_content(item, target, matches);
403 }
404}
405
406fn collect_definitions_in_content<'a>(
407 item: &'a ContentItem,
408 target: &str,
409 matches: &mut Vec<&'a Definition>,
410) {
411 match item {
412 ContentItem::Definition(definition) => {
413 if subject_matches(definition, target) {
414 matches.push(definition);
415 }
416 collect_definitions(definition.children.iter(), target, matches);
417 }
418 ContentItem::Session(session) => {
419 collect_definitions(session.children.iter(), target, matches);
420 }
421 ContentItem::List(list) => {
422 for entry in &list.items {
423 if let ContentItem::ListItem(list_item) = entry {
424 collect_definitions(list_item.children.iter(), target, matches);
425 }
426 }
427 }
428 ContentItem::ListItem(list_item) => {
429 collect_definitions(list_item.children.iter(), target, matches);
430 }
431 ContentItem::Annotation(annotation) => {
432 collect_definitions(annotation.children.iter(), target, matches);
433 }
434 ContentItem::Paragraph(paragraph) => {
435 for annotation in paragraph.annotations() {
436 collect_definitions(annotation.children.iter(), target, matches);
437 }
438 }
439 _ => {}
440 }
441}
442
443fn find_definition_in_items<'a>(
444 items: impl Iterator<Item = &'a ContentItem>,
445 position: Position,
446) -> Option<&'a Definition> {
447 for item in items {
448 if let Some(definition) = find_definition_in_content(item, position) {
449 return Some(definition);
450 }
451 }
452 None
453}
454
455fn find_definition_in_content(item: &ContentItem, position: Position) -> Option<&Definition> {
456 match item {
457 ContentItem::Definition(definition) => {
458 if definition
459 .header_location()
460 .map(|range| range.contains(position))
461 .unwrap_or_else(|| definition.range().contains(position))
462 {
463 return Some(definition);
464 }
465 find_definition_in_items(definition.children.iter(), position)
466 }
467 ContentItem::Session(session) => {
468 find_definition_in_items(session.children.iter(), position)
469 }
470 ContentItem::List(list) => list.items.iter().find_map(|entry| match entry {
471 ContentItem::ListItem(list_item) => {
472 find_definition_in_items(list_item.children.iter(), position)
473 }
474 _ => None,
475 }),
476 ContentItem::ListItem(list_item) => {
477 find_definition_in_items(list_item.children.iter(), position)
478 }
479 ContentItem::Annotation(annotation) => {
480 find_definition_in_items(annotation.children.iter(), position)
481 }
482 ContentItem::Paragraph(paragraph) => paragraph
483 .annotations()
484 .iter()
485 .find_map(|annotation| find_definition_in_items(annotation.children.iter(), position)),
486 _ => None,
487 }
488}
489
490fn find_annotation_in_session(
491 session: &Session,
492 position: Position,
493 is_root: bool,
494) -> Option<&Annotation> {
495 if !is_root {
496 if let Some(annotation) = session
497 .annotations()
498 .iter()
499 .find(|ann| ann.header_location().contains(position))
500 {
501 return Some(annotation);
502 }
503 }
504 for child in session.children.iter() {
505 if let Some(annotation) = find_annotation_in_content(child, position) {
506 return Some(annotation);
507 }
508 }
509 None
510}
511
512fn find_annotation_in_content(item: &ContentItem, position: Position) -> Option<&Annotation> {
513 match item {
514 ContentItem::Paragraph(paragraph) => paragraph
515 .annotations()
516 .iter()
517 .find(|ann| ann.header_location().contains(position))
518 .or_else(|| find_annotation_in_items(paragraph.lines.iter(), position)),
519 ContentItem::Session(session) => find_annotation_in_session(session, position, false),
520 ContentItem::List(list) => {
521 if let Some(annotation) = list
522 .annotations()
523 .iter()
524 .find(|ann| ann.header_location().contains(position))
525 {
526 return Some(annotation);
527 }
528 for entry in &list.items {
529 if let ContentItem::ListItem(list_item) = entry {
530 if let Some(annotation) = list_item
531 .annotations()
532 .iter()
533 .find(|ann| ann.header_location().contains(position))
534 {
535 return Some(annotation);
536 }
537 if let Some(found) =
538 find_annotation_in_items(list_item.children.iter(), position)
539 {
540 return Some(found);
541 }
542 }
543 }
544 None
545 }
546 ContentItem::ListItem(list_item) => list_item
547 .annotations()
548 .iter()
549 .find(|ann| ann.header_location().contains(position))
550 .or_else(|| find_annotation_in_items(list_item.children.iter(), position)),
551 ContentItem::Definition(definition) => definition
552 .annotations()
553 .iter()
554 .find(|ann| ann.header_location().contains(position))
555 .or_else(|| find_annotation_in_items(definition.children.iter(), position)),
556 ContentItem::Annotation(annotation) => {
557 if annotation.header_location().contains(position) {
558 return Some(annotation);
559 }
560 find_annotation_in_items(annotation.children.iter(), position)
561 }
562 ContentItem::VerbatimBlock(verbatim) => verbatim
563 .annotations()
564 .iter()
565 .find(|ann| ann.header_location().contains(position))
566 .or_else(|| find_annotation_in_items(verbatim.children.iter(), position)),
567 ContentItem::TextLine(_) => None,
568 _ => None,
569 }
570}
571
572fn find_annotation_in_items<'a>(
573 items: impl Iterator<Item = &'a ContentItem>,
574 position: Position,
575) -> Option<&'a Annotation> {
576 for item in items {
577 if let Some(annotation) = find_annotation_in_content(item, position) {
578 return Some(annotation);
579 }
580 }
581 None
582}
583
584fn find_session_in_branch(
585 session: &Session,
586 position: Position,
587 is_root: bool,
588) -> Option<&Session> {
589 if !is_root {
590 if let Some(header) = session.header_location() {
591 if header.contains(position) {
592 return Some(session);
593 }
594 }
595 }
596 for child in session.children.iter() {
597 if let ContentItem::Session(child_session) = child {
598 if let Some(found) = find_session_in_branch(child_session, position, false) {
599 return Some(found);
600 }
601 }
602 }
603 None
604}
605
606fn collect_sessions_by_identifier<'a>(
607 session: &'a Session,
608 target: &str,
609 matches: &mut Vec<&'a Session>,
610 is_root: bool,
611) {
612 if !is_root {
613 let title = session.title.as_string();
614 let normalized_title = title.trim().to_ascii_lowercase();
615 let title_matches =
616 normalized_title.starts_with(target) && has_session_boundary(title, target.len());
617 let identifier_matches = session_identifier(session)
618 .as_deref()
619 .map(|id| id.to_ascii_lowercase() == target)
620 .unwrap_or(false);
621 if title_matches || identifier_matches {
622 matches.push(session);
623 }
624 }
625 for child in session.children.iter() {
626 if let ContentItem::Session(child_session) = child {
627 collect_sessions_by_identifier(child_session, target, matches, false);
628 }
629 }
630}
631
632fn has_session_boundary(title: &str, len: usize) -> bool {
633 let trimmed = title.trim();
634 if trimmed.len() <= len {
635 return trimmed.len() == len;
636 }
637 matches!(
638 trimmed.chars().nth(len),
639 Some(ch) if matches!(ch, ' ' | '\t' | ':' | '.')
640 )
641}
642
643fn subject_matches(definition: &Definition, target: &str) -> bool {
644 normalize_key(definition.subject.as_string()).eq(target)
645}
646
647fn normalize_key(input: &str) -> String {
648 input.trim().to_ascii_lowercase()
649}
650
651fn extract_session_identifier(title: &str) -> Option<String> {
652 let trimmed = title.trim();
653 if trimmed.is_empty() {
654 return None;
655 }
656 let mut identifier = String::new();
657 for ch in trimmed.chars() {
658 if ch.is_ascii_digit() || ch == '.' {
659 identifier.push(ch);
660 } else {
661 break;
662 }
663 }
664 if identifier.ends_with('.') {
665 identifier.pop();
666 }
667 if identifier.is_empty() {
668 None
669 } else {
670 Some(identifier)
671 }
672}
673
674fn is_notes_list(list: &lex_core::lex::ast::List) -> bool {
676 list.annotations()
677 .iter()
678 .any(|a| a.data.label.value.trim().eq_ignore_ascii_case("notes"))
679}
680
681fn has_notes_annotation(annotations: &[Annotation]) -> bool {
685 annotations
686 .iter()
687 .any(|a| a.data.label.value.trim().eq_ignore_ascii_case("notes"))
688}
689
690pub fn collect_footnote_definitions(
699 document: &Document,
700) -> Vec<(String, lex_core::lex::ast::Range)> {
701 let mut defs = Vec::new();
702 if has_notes_annotation(document.annotations()) {
704 collect_first_list_items(&document.root.children, &mut defs);
705 }
706 collect_notes_items_in_session(&document.root, &mut defs);
707 defs
708}
709
710fn collect_notes_items_in_session(
711 session: &Session,
712 out: &mut Vec<(String, lex_core::lex::ast::Range)>,
713) {
714 if has_notes_annotation(session.annotations()) {
716 collect_first_list_items(&session.children, out);
717 }
718 for item in session.children.iter() {
719 match item {
720 ContentItem::List(l) if is_notes_list(l) => {
721 collect_list_item_labels(l, out);
722 }
723 ContentItem::Session(s) => collect_notes_items_in_session(s, out),
724 ContentItem::Definition(d) => collect_notes_items_in_children(d.children.iter(), out),
725 _ => {}
726 }
727 }
728}
729
730fn collect_notes_items_in_children<'a>(
731 items: impl Iterator<Item = &'a ContentItem>,
732 out: &mut Vec<(String, lex_core::lex::ast::Range)>,
733) {
734 for item in items {
735 match item {
736 ContentItem::List(l) if is_notes_list(l) => {
737 collect_list_item_labels(l, out);
738 }
739 ContentItem::Session(s) => collect_notes_items_in_session(s, out),
740 _ => {}
741 }
742 }
743}
744
745fn collect_first_list_items(
748 children: &[ContentItem],
749 out: &mut Vec<(String, lex_core::lex::ast::Range)>,
750) {
751 for item in children {
752 if let ContentItem::List(l) = item {
753 collect_list_item_labels(l, out);
754 return;
755 }
756 }
757}
758
759fn collect_list_item_labels(
760 list: &lex_core::lex::ast::List,
761 out: &mut Vec<(String, lex_core::lex::ast::Range)>,
762) {
763 for entry in &list.items {
764 if let ContentItem::ListItem(li) = entry {
765 let marker = li.marker();
766 let label = marker
767 .trim()
768 .trim_end_matches(['.', ')', ':'].as_ref())
769 .trim();
770 if !label.is_empty() {
771 out.push((label.to_string(), li.range().clone()));
772 }
773 }
774 }
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use lex_core::lex::testing::lexplore::Lexplore;
781
782 #[test]
783 fn collects_footnotes_from_notes_annotated_list() {
784 let doc = Lexplore::footnotes(3).parse().unwrap();
785 let defs = collect_footnote_definitions(&doc);
786 let labels: Vec<&str> = defs.iter().map(|(l, _)| l.as_str()).collect();
787 assert_eq!(labels, vec!["1", "2"]);
788 }
789
790 #[test]
791 fn no_footnotes_without_notes_annotation() {
792 let doc = Lexplore::footnotes(4).parse().unwrap();
794 let defs = collect_footnote_definitions(&doc);
795 assert!(defs.is_empty());
796 }
797
798 #[test]
799 fn collects_footnotes_at_document_root() {
800 let doc = Lexplore::footnotes(2).parse().unwrap();
801 let defs = collect_footnote_definitions(&doc);
802 let labels: Vec<&str> = defs.iter().map(|(l, _)| l.as_str()).collect();
803 assert_eq!(labels, vec!["1", "2"]);
804 }
805
806 #[test]
807 fn multiple_notes_lists_in_different_sessions() {
808 let doc = Lexplore::footnotes(5).parse().unwrap();
810 let defs = collect_footnote_definitions(&doc);
811 assert_eq!(defs.len(), 4); }
813}