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
674pub fn find_notes_session(document: &Document) -> Option<&Session> {
682 let root_title = document.title();
684 if is_notes_title(root_title) {
685 return Some(&document.root);
686 }
687
688 for item in document.root.children.iter().rev() {
690 if let ContentItem::Session(session) = item {
691 let title = session.title.as_string();
692 if is_notes_title(title) {
693 return Some(session);
694 }
695 if is_list_only_session(session) {
697 return Some(session);
698 }
699 break;
701 }
702 }
703 None
704}
705
706fn is_notes_title(title: impl AsRef<str>) -> bool {
708 let title = title.as_ref();
709 let normalized = title.trim().trim_end_matches(':').to_lowercase();
710 normalized == "notes" || normalized == "footnotes"
711}
712
713fn is_list_only_session(session: &Session) -> bool {
715 if session.children.is_empty() {
716 return false;
717 }
718 session
719 .children
720 .iter()
721 .all(|child| matches!(child, ContentItem::List(_) | ContentItem::BlankLineGroup(_)))
722}
723
724pub fn collect_footnote_definitions(
732 document: &Document,
733) -> Vec<(String, lex_core::lex::ast::Range)> {
734 let mut defs = Vec::new();
735
736 for annotation in collect_all_annotations(document) {
738 let label = &annotation.data.label.value;
739 if !label.trim().is_empty() {
740 defs.push((label.clone(), annotation.header_location().clone()));
741 }
742 }
743
744 if let Some(session) = find_notes_session(document) {
746 collect_footnote_items_in_container(&session.children, &mut defs);
747 }
748 defs
749}
750
751fn collect_footnote_items_in_container(
752 items: &[ContentItem],
753 out: &mut Vec<(String, lex_core::lex::ast::Range)>,
754) {
755 for item in items {
756 match item {
757 ContentItem::List(l) => {
758 for entry in &l.items {
763 if let ContentItem::ListItem(li) = entry {
764 let marker = li.marker();
765 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 ContentItem::Session(s) => collect_footnote_items_in_container(&s.children, out),
777 _ => {}
778 }
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785 use lex_core::lex::parsing;
786
787 fn parse(source: &str) -> Document {
788 parsing::parse_document(source).expect("parse failed")
789 }
790
791 #[test]
792 fn find_notes_session_by_title() {
793 let doc = parse("Content\n\nNotes\n\n 1. A note\n");
794 let notes = find_notes_session(&doc);
795 assert!(notes.is_some());
796 assert_eq!(notes.unwrap().title.as_string().trim(), "Notes");
797 }
798
799 #[test]
800 fn find_notes_session_by_footnotes_title() {
801 let doc = parse("Content\n\nFootnotes\n\n 1. A note\n");
802 let notes = find_notes_session(&doc);
803 assert!(notes.is_some());
804 assert_eq!(notes.unwrap().title.as_string().trim(), "Footnotes");
805 }
806
807 #[test]
808 fn find_notes_session_implicit_list_only() {
809 let doc = parse("Content\n\nReferences\n\n 1. First ref\n 2. Second ref\n");
811 let notes = find_notes_session(&doc);
812 assert!(notes.is_some());
813 assert_eq!(notes.unwrap().title.as_string().trim(), "References");
814 }
815
816 #[test]
817 fn find_notes_session_none_when_last_has_paragraphs() {
818 let doc = parse("Content\n\nConclusion\n\n This is a paragraph.\n");
820 let notes = find_notes_session(&doc);
821 assert!(notes.is_none());
822 }
823
824 #[test]
825 fn find_notes_session_root_is_notes() {
826 let doc = parse("Notes\n\n 1. A note\n");
827 let notes = find_notes_session(&doc);
828 assert!(notes.is_some());
829 }
830}