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, Verbatim,
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_verbatim_at_position(document: &Document, position: Position) -> Option<&Verbatim> {
264 for annotation in document.annotations() {
265 if let Some(found) = find_verbatim_in_items(annotation.children.iter(), position) {
266 return Some(found);
267 }
268 }
269 find_verbatim_in_session(&document.root, position)
270}
271
272fn find_verbatim_in_session(session: &Session, position: Position) -> Option<&Verbatim> {
273 find_verbatim_in_items(session.children.iter(), position)
274}
275
276fn find_verbatim_in_items<'a, I>(items: I, position: Position) -> Option<&'a Verbatim>
277where
278 I: IntoIterator<Item = &'a ContentItem>,
279{
280 for item in items {
281 match item {
282 ContentItem::VerbatimBlock(v) => {
283 if v.location.contains(position) {
284 return Some(v);
285 }
286 }
287 ContentItem::Session(s) => {
288 if let Some(found) = find_verbatim_in_session(s, position) {
289 return Some(found);
290 }
291 }
292 ContentItem::Definition(d) => {
293 if let Some(found) = find_verbatim_in_items(d.children.iter(), position) {
294 return Some(found);
295 }
296 }
297 ContentItem::List(list) => {
298 for entry in &list.items {
299 if let ContentItem::ListItem(li) = entry {
300 if let Some(found) = find_verbatim_in_items(li.children.iter(), position) {
301 return Some(found);
302 }
303 }
304 }
305 }
306 ContentItem::Annotation(a) => {
307 if let Some(found) = find_verbatim_in_items(a.children.iter(), position) {
308 return Some(found);
309 }
310 }
311 ContentItem::Table(table) => {
312 if let Some(found) = find_verbatim_in_items(table.cell_children_iter(), position) {
317 return Some(found);
318 }
319 }
320 _ => {}
321 }
322 }
323 None
324}
325
326pub fn find_sessions_by_identifier<'a>(
327 document: &'a Document,
328 identifier: &str,
329) -> Vec<&'a Session> {
330 let normalized = normalize_key(identifier);
331 if normalized.is_empty() {
332 return Vec::new();
333 }
334 let mut matches = Vec::new();
335 collect_sessions_by_identifier(&document.root, &normalized, &mut matches, true);
336 matches
337}
338
339pub fn session_identifier(session: &Session) -> Option<String> {
340 extract_session_identifier(session.title.as_string())
341}
342
343pub fn reference_at_position(
344 document: &Document,
345 position: Position,
346) -> Option<PositionedReference> {
347 let mut result = None;
348 for_each_text_content(document, &mut |text| {
349 if result.is_some() {
350 return;
351 }
352 for reference in extract_references(text) {
353 if reference.range.contains(position) {
354 result = Some(reference);
355 break;
356 }
357 }
358 });
359 result
360}
361
362fn visit_session_text<F>(session: &Session, is_root: bool, f: &mut F)
363where
364 F: FnMut(&TextContent),
365{
366 if !is_root {
367 f(&session.title);
368 }
369 for annotation in session.annotations() {
370 visit_annotation_text(annotation, f);
371 }
372 for child in session.children.iter() {
373 visit_content_text(child, f);
374 }
375}
376
377fn visit_annotation_text<F>(annotation: &Annotation, f: &mut F)
378where
379 F: FnMut(&TextContent),
380{
381 for child in annotation.children.iter() {
382 visit_content_text(child, f);
383 }
384}
385
386fn visit_content_text<F>(item: &ContentItem, f: &mut F)
387where
388 F: FnMut(&TextContent),
389{
390 match item {
391 ContentItem::Paragraph(paragraph) => {
392 for line in ¶graph.lines {
393 if let ContentItem::TextLine(text_line) = line {
394 f(&text_line.content);
395 }
396 }
397 for annotation in paragraph.annotations() {
398 visit_annotation_text(annotation, f);
399 }
400 }
401 ContentItem::Session(session) => visit_session_text(session, false, f),
402 ContentItem::List(list) => {
403 for annotation in list.annotations() {
404 visit_annotation_text(annotation, f);
405 }
406 for entry in &list.items {
407 if let ContentItem::ListItem(list_item) = entry {
408 for text in &list_item.text {
409 f(text);
410 }
411 for annotation in list_item.annotations() {
412 visit_annotation_text(annotation, f);
413 }
414 for child in list_item.children.iter() {
415 visit_content_text(child, f);
416 }
417 }
418 }
419 }
420 ContentItem::ListItem(list_item) => {
421 for text in &list_item.text {
422 f(text);
423 }
424 for annotation in list_item.annotations() {
425 visit_annotation_text(annotation, f);
426 }
427 for child in list_item.children.iter() {
428 visit_content_text(child, f);
429 }
430 }
431 ContentItem::Definition(definition) => {
432 f(&definition.subject);
433 for annotation in definition.annotations() {
434 visit_annotation_text(annotation, f);
435 }
436 for child in definition.children.iter() {
437 visit_content_text(child, f);
438 }
439 }
440 ContentItem::Annotation(annotation) => visit_annotation_text(annotation, f),
441 ContentItem::VerbatimBlock(verbatim) => {
442 f(&verbatim.subject);
443 for annotation in verbatim.annotations() {
444 visit_annotation_text(annotation, f);
445 }
446 }
447 ContentItem::Table(table) => {
448 f(&table.subject);
449 for row in table.all_rows() {
450 for cell in &row.cells {
451 f(&cell.content);
452 }
453 }
454 for annotation in table.annotations() {
455 visit_annotation_text(annotation, f);
456 }
457 }
458 ContentItem::TextLine(_)
459 | ContentItem::VerbatimLine(_)
460 | ContentItem::BlankLineGroup(_) => {}
461 }
462}
463
464fn collect_definitions<'a>(
465 items: impl Iterator<Item = &'a ContentItem>,
466 target: &str,
467 matches: &mut Vec<&'a Definition>,
468) {
469 for item in items {
470 collect_definitions_in_content(item, target, matches);
471 }
472}
473
474fn collect_definitions_in_content<'a>(
475 item: &'a ContentItem,
476 target: &str,
477 matches: &mut Vec<&'a Definition>,
478) {
479 match item {
480 ContentItem::Definition(definition) => {
481 if subject_matches(definition, target) {
482 matches.push(definition);
483 }
484 collect_definitions(definition.children.iter(), target, matches);
485 }
486 ContentItem::Session(session) => {
487 collect_definitions(session.children.iter(), target, matches);
488 }
489 ContentItem::List(list) => {
490 for entry in &list.items {
491 if let ContentItem::ListItem(list_item) = entry {
492 collect_definitions(list_item.children.iter(), target, matches);
493 }
494 }
495 }
496 ContentItem::ListItem(list_item) => {
497 collect_definitions(list_item.children.iter(), target, matches);
498 }
499 ContentItem::Annotation(annotation) => {
500 collect_definitions(annotation.children.iter(), target, matches);
501 }
502 ContentItem::Paragraph(paragraph) => {
503 for annotation in paragraph.annotations() {
504 collect_definitions(annotation.children.iter(), target, matches);
505 }
506 }
507 _ => {}
508 }
509}
510
511fn find_definition_in_items<'a>(
512 items: impl Iterator<Item = &'a ContentItem>,
513 position: Position,
514) -> Option<&'a Definition> {
515 for item in items {
516 if let Some(definition) = find_definition_in_content(item, position) {
517 return Some(definition);
518 }
519 }
520 None
521}
522
523fn find_definition_in_content(item: &ContentItem, position: Position) -> Option<&Definition> {
524 match item {
525 ContentItem::Definition(definition) => {
526 if definition
527 .header_location()
528 .map(|range| range.contains(position))
529 .unwrap_or_else(|| definition.range().contains(position))
530 {
531 return Some(definition);
532 }
533 find_definition_in_items(definition.children.iter(), position)
534 }
535 ContentItem::Session(session) => {
536 find_definition_in_items(session.children.iter(), position)
537 }
538 ContentItem::List(list) => list.items.iter().find_map(|entry| match entry {
539 ContentItem::ListItem(list_item) => {
540 find_definition_in_items(list_item.children.iter(), position)
541 }
542 _ => None,
543 }),
544 ContentItem::ListItem(list_item) => {
545 find_definition_in_items(list_item.children.iter(), position)
546 }
547 ContentItem::Annotation(annotation) => {
548 find_definition_in_items(annotation.children.iter(), position)
549 }
550 ContentItem::Paragraph(paragraph) => paragraph
551 .annotations()
552 .iter()
553 .find_map(|annotation| find_definition_in_items(annotation.children.iter(), position)),
554 _ => None,
555 }
556}
557
558fn find_annotation_in_session(
559 session: &Session,
560 position: Position,
561 is_root: bool,
562) -> Option<&Annotation> {
563 if !is_root {
564 if let Some(annotation) = session
565 .annotations()
566 .iter()
567 .find(|ann| ann.header_location().contains(position))
568 {
569 return Some(annotation);
570 }
571 }
572 for child in session.children.iter() {
573 if let Some(annotation) = find_annotation_in_content(child, position) {
574 return Some(annotation);
575 }
576 }
577 None
578}
579
580fn find_annotation_in_content(item: &ContentItem, position: Position) -> Option<&Annotation> {
581 match item {
582 ContentItem::Paragraph(paragraph) => paragraph
583 .annotations()
584 .iter()
585 .find(|ann| ann.header_location().contains(position))
586 .or_else(|| find_annotation_in_items(paragraph.lines.iter(), position)),
587 ContentItem::Session(session) => find_annotation_in_session(session, position, false),
588 ContentItem::List(list) => {
589 if let Some(annotation) = list
590 .annotations()
591 .iter()
592 .find(|ann| ann.header_location().contains(position))
593 {
594 return Some(annotation);
595 }
596 for entry in &list.items {
597 if let ContentItem::ListItem(list_item) = entry {
598 if let Some(annotation) = list_item
599 .annotations()
600 .iter()
601 .find(|ann| ann.header_location().contains(position))
602 {
603 return Some(annotation);
604 }
605 if let Some(found) =
606 find_annotation_in_items(list_item.children.iter(), position)
607 {
608 return Some(found);
609 }
610 }
611 }
612 None
613 }
614 ContentItem::ListItem(list_item) => list_item
615 .annotations()
616 .iter()
617 .find(|ann| ann.header_location().contains(position))
618 .or_else(|| find_annotation_in_items(list_item.children.iter(), position)),
619 ContentItem::Definition(definition) => definition
620 .annotations()
621 .iter()
622 .find(|ann| ann.header_location().contains(position))
623 .or_else(|| find_annotation_in_items(definition.children.iter(), position)),
624 ContentItem::Annotation(annotation) => {
625 if annotation.header_location().contains(position) {
626 return Some(annotation);
627 }
628 find_annotation_in_items(annotation.children.iter(), position)
629 }
630 ContentItem::VerbatimBlock(verbatim) => verbatim
631 .annotations()
632 .iter()
633 .find(|ann| ann.header_location().contains(position))
634 .or_else(|| find_annotation_in_items(verbatim.children.iter(), position)),
635 ContentItem::TextLine(_) => None,
636 _ => None,
637 }
638}
639
640fn find_annotation_in_items<'a>(
641 items: impl Iterator<Item = &'a ContentItem>,
642 position: Position,
643) -> Option<&'a Annotation> {
644 for item in items {
645 if let Some(annotation) = find_annotation_in_content(item, position) {
646 return Some(annotation);
647 }
648 }
649 None
650}
651
652fn find_session_in_branch(
653 session: &Session,
654 position: Position,
655 is_root: bool,
656) -> Option<&Session> {
657 if !is_root {
658 if let Some(header) = session.header_location() {
659 if header.contains(position) {
660 return Some(session);
661 }
662 }
663 }
664 for child in session.children.iter() {
665 if let ContentItem::Session(child_session) = child {
666 if let Some(found) = find_session_in_branch(child_session, position, false) {
667 return Some(found);
668 }
669 }
670 }
671 None
672}
673
674fn collect_sessions_by_identifier<'a>(
675 session: &'a Session,
676 target: &str,
677 matches: &mut Vec<&'a Session>,
678 is_root: bool,
679) {
680 if !is_root {
681 let title = session.title.as_string();
682 let normalized_title = title.trim().to_ascii_lowercase();
683 let title_matches =
684 normalized_title.starts_with(target) && has_session_boundary(title, target.len());
685 let identifier_matches = session_identifier(session)
686 .as_deref()
687 .map(|id| id.to_ascii_lowercase() == target)
688 .unwrap_or(false);
689 if title_matches || identifier_matches {
690 matches.push(session);
691 }
692 }
693 for child in session.children.iter() {
694 if let ContentItem::Session(child_session) = child {
695 collect_sessions_by_identifier(child_session, target, matches, false);
696 }
697 }
698}
699
700fn has_session_boundary(title: &str, len: usize) -> bool {
701 let trimmed = title.trim();
702 if trimmed.len() <= len {
703 return trimmed.len() == len;
704 }
705 matches!(
706 trimmed.chars().nth(len),
707 Some(ch) if matches!(ch, ' ' | '\t' | ':' | '.')
708 )
709}
710
711fn subject_matches(definition: &Definition, target: &str) -> bool {
712 normalize_key(definition.subject.as_string()).eq(target)
713}
714
715fn normalize_key(input: &str) -> String {
716 input.trim().to_ascii_lowercase()
717}
718
719fn extract_session_identifier(title: &str) -> Option<String> {
720 let trimmed = title.trim();
721 if trimmed.is_empty() {
722 return None;
723 }
724 let mut identifier = String::new();
725 for ch in trimmed.chars() {
726 if ch.is_ascii_digit() || ch == '.' {
727 identifier.push(ch);
728 } else {
729 break;
730 }
731 }
732 if identifier.ends_with('.') {
733 identifier.pop();
734 }
735 if identifier.is_empty() {
736 None
737 } else {
738 Some(identifier)
739 }
740}
741
742fn is_notes_list(list: &lex_core::lex::ast::List) -> bool {
747 list.annotations().iter().any(label_is_notes)
748}
749
750fn has_notes_annotation(annotations: &[Annotation]) -> bool {
754 annotations.iter().any(label_is_notes)
755}
756
757fn label_is_notes(a: &Annotation) -> bool {
758 let v = a.data.label.value.trim();
759 v.eq_ignore_ascii_case("lex.notes") || v.eq_ignore_ascii_case("notes")
760}
761
762pub fn collect_footnote_definitions(
771 document: &Document,
772) -> Vec<(String, lex_core::lex::ast::Range)> {
773 let mut defs = Vec::new();
774 if has_notes_annotation(document.annotations()) {
776 collect_first_list_items(&document.root.children, &mut defs);
777 }
778 collect_notes_items_in_session(&document.root, &mut defs);
779 defs
780}
781
782fn collect_notes_items_in_session(
783 session: &Session,
784 out: &mut Vec<(String, lex_core::lex::ast::Range)>,
785) {
786 if has_notes_annotation(session.annotations()) {
788 collect_first_list_items(&session.children, out);
789 }
790 for item in session.children.iter() {
791 match item {
792 ContentItem::List(l) if is_notes_list(l) => {
793 collect_list_item_labels(l, out);
794 }
795 ContentItem::Session(s) => collect_notes_items_in_session(s, out),
796 ContentItem::Definition(d) => collect_notes_items_in_children(d.children.iter(), out),
797 _ => {}
798 }
799 }
800}
801
802fn collect_notes_items_in_children<'a>(
803 items: impl Iterator<Item = &'a ContentItem>,
804 out: &mut Vec<(String, lex_core::lex::ast::Range)>,
805) {
806 for item in items {
807 match item {
808 ContentItem::List(l) if is_notes_list(l) => {
809 collect_list_item_labels(l, out);
810 }
811 ContentItem::Session(s) => collect_notes_items_in_session(s, out),
812 _ => {}
813 }
814 }
815}
816
817fn collect_first_list_items(
820 children: &[ContentItem],
821 out: &mut Vec<(String, lex_core::lex::ast::Range)>,
822) {
823 for item in children {
824 if let ContentItem::List(l) = item {
825 collect_list_item_labels(l, out);
826 return;
827 }
828 }
829}
830
831fn collect_list_item_labels(
832 list: &lex_core::lex::ast::List,
833 out: &mut Vec<(String, lex_core::lex::ast::Range)>,
834) {
835 for entry in &list.items {
836 if let ContentItem::ListItem(li) = entry {
837 let marker = li.marker();
838 let label = marker
839 .trim()
840 .trim_end_matches(['.', ')', ':'].as_ref())
841 .trim();
842 if !label.is_empty() {
843 out.push((label.to_string(), li.range().clone()));
844 }
845 }
846 }
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852 use lex_core::lex::testing::lexplore::Lexplore;
853
854 #[test]
855 fn collects_footnotes_from_notes_annotated_list() {
856 let doc = Lexplore::footnotes(3).parse().unwrap();
857 let defs = collect_footnote_definitions(&doc);
858 let labels: Vec<&str> = defs.iter().map(|(l, _)| l.as_str()).collect();
859 assert_eq!(labels, vec!["1", "2"]);
860 }
861
862 #[test]
863 fn no_footnotes_without_notes_annotation() {
864 let doc = Lexplore::footnotes(4).parse().unwrap();
866 let defs = collect_footnote_definitions(&doc);
867 assert!(defs.is_empty());
868 }
869
870 #[test]
871 fn collects_footnotes_at_document_root() {
872 let doc = Lexplore::footnotes(2).parse().unwrap();
873 let defs = collect_footnote_definitions(&doc);
874 let labels: Vec<&str> = defs.iter().map(|(l, _)| l.as_str()).collect();
875 assert_eq!(labels, vec!["1", "2"]);
876 }
877
878 #[test]
879 fn multiple_notes_lists_in_different_sessions() {
880 let doc = Lexplore::footnotes(5).parse().unwrap();
882 let defs = collect_footnote_definitions(&doc);
883 assert_eq!(defs.len(), 4); }
885}