lex_core/lex/ast/elements/
session.rs1use super::super::range::{Position, Range};
61use super::super::text_content::TextContent;
62use super::super::traits::{AstNode, Container, Visitor, VisualStructure};
63use super::annotation::Annotation;
64use super::container::SessionContainer;
65use super::content_item::ContentItem;
66use super::definition::Definition;
67use super::list::{List, ListItem};
68use super::paragraph::Paragraph;
69use super::table::Table;
70use super::typed_content::SessionContent;
71use super::verbatim::Verbatim;
72use std::fmt;
73
74#[derive(Debug, Clone, PartialEq)]
76pub struct Session {
77 pub title: TextContent,
78 pub marker: Option<super::sequence_marker::SequenceMarker>,
79 pub children: SessionContainer,
80 pub annotations: Vec<Annotation>,
81 pub location: Range,
82}
83
84impl Session {
85 fn default_location() -> Range {
86 Range::new(0..0, Position::new(0, 0), Position::new(0, 0))
87 }
88 pub fn new(title: TextContent, children: Vec<SessionContent>) -> Self {
89 Self {
90 title,
91 marker: None,
92 children: SessionContainer::from_typed(children),
93 annotations: Vec::new(),
94 location: Self::default_location(),
95 }
96 }
97 pub fn with_title(title: String) -> Self {
98 Self {
99 title: TextContent::from_string(title, None),
100 marker: None,
101 children: SessionContainer::empty(),
102 annotations: Vec::new(),
103 location: Self::default_location(),
104 }
105 }
106
107 pub fn at(mut self, location: Range) -> Self {
109 self.location = location;
110 self
111 }
112
113 pub fn annotations(&self) -> &[Annotation] {
115 &self.annotations
116 }
117
118 pub fn header_location(&self) -> Option<&Range> {
120 self.title.location.as_ref()
121 }
122
123 pub fn body_location(&self) -> Option<Range> {
125 Range::bounding_box(self.children.iter().map(|item| item.range()))
126 }
127
128 pub fn title_text(&self) -> &str {
140 if let Some(marker) = &self.marker {
141 let full_title = self.title.as_string();
142 let marker_text = marker.as_str();
143
144 if let Some(pos) = full_title.find(marker_text) {
146 let after_marker = &full_title[pos + marker_text.len()..];
148 return after_marker.trim_start();
149 }
150 }
151
152 self.title.as_string()
154 }
155
156 pub fn full_title(&self) -> &str {
168 self.title.as_string()
169 }
170
171 pub fn annotations_mut(&mut self) -> &mut Vec<Annotation> {
173 &mut self.annotations
174 }
175
176 pub fn iter_annotations(&self) -> std::slice::Iter<'_, Annotation> {
178 self.annotations.iter()
179 }
180
181 pub fn iter_annotation_contents(&self) -> impl Iterator<Item = &ContentItem> {
183 self.annotations
184 .iter()
185 .flat_map(|annotation| annotation.children())
186 }
187
188 pub fn iter_items(&self) -> impl Iterator<Item = &ContentItem> {
195 self.children.iter()
196 }
197
198 pub fn iter_paragraphs(&self) -> impl Iterator<Item = &Paragraph> {
200 self.children.iter_paragraphs()
201 }
202
203 pub fn iter_sessions(&self) -> impl Iterator<Item = &Session> {
205 self.children.iter_sessions()
206 }
207
208 pub fn iter_lists(&self) -> impl Iterator<Item = &List> {
210 self.children.iter_lists()
211 }
212
213 pub fn iter_verbatim_blocks(&self) -> impl Iterator<Item = &Verbatim> {
215 self.children.iter_verbatim_blocks()
216 }
217
218 pub fn iter_all_nodes(&self) -> Box<dyn Iterator<Item = &ContentItem> + '_> {
220 self.children.iter_all_nodes()
221 }
222
223 pub fn iter_all_nodes_with_depth(
225 &self,
226 ) -> Box<dyn Iterator<Item = (&ContentItem, usize)> + '_> {
227 self.children.iter_all_nodes_with_depth()
228 }
229
230 pub fn iter_paragraphs_recursive(&self) -> Box<dyn Iterator<Item = &Paragraph> + '_> {
232 self.children.iter_paragraphs_recursive()
233 }
234
235 pub fn iter_sessions_recursive(&self) -> Box<dyn Iterator<Item = &Session> + '_> {
237 self.children.iter_sessions_recursive()
238 }
239
240 pub fn iter_lists_recursive(&self) -> Box<dyn Iterator<Item = &List> + '_> {
242 self.children.iter_lists_recursive()
243 }
244
245 pub fn iter_verbatim_blocks_recursive(&self) -> Box<dyn Iterator<Item = &Verbatim> + '_> {
247 self.children.iter_verbatim_blocks_recursive()
248 }
249
250 pub fn iter_list_items_recursive(&self) -> Box<dyn Iterator<Item = &ListItem> + '_> {
252 self.children.iter_list_items_recursive()
253 }
254
255 pub fn iter_definitions_recursive(&self) -> Box<dyn Iterator<Item = &Definition> + '_> {
257 self.children.iter_definitions_recursive()
258 }
259
260 pub fn iter_annotations_recursive(&self) -> Box<dyn Iterator<Item = &Annotation> + '_> {
262 self.children.iter_annotations_recursive()
263 }
264
265 pub fn first_paragraph(&self) -> Option<&Paragraph> {
267 self.children.first_paragraph()
268 }
269
270 pub fn first_session(&self) -> Option<&Session> {
272 self.children.first_session()
273 }
274
275 pub fn first_list(&self) -> Option<&List> {
277 self.children.first_list()
278 }
279
280 pub fn first_definition(&self) -> Option<&Definition> {
282 self.children.first_definition()
283 }
284
285 pub fn first_annotation(&self) -> Option<&Annotation> {
287 self.children.first_annotation()
288 }
289
290 pub fn first_verbatim(&self) -> Option<&Verbatim> {
292 self.children.first_verbatim()
293 }
294
295 pub fn expect_paragraph(&self) -> &Paragraph {
297 self.children.expect_paragraph()
298 }
299
300 pub fn expect_session(&self) -> &Session {
302 self.children.expect_session()
303 }
304
305 pub fn expect_list(&self) -> &List {
307 self.children.expect_list()
308 }
309
310 pub fn expect_definition(&self) -> &Definition {
312 self.children.expect_definition()
313 }
314
315 pub fn expect_annotation(&self) -> &Annotation {
317 self.children.expect_annotation()
318 }
319
320 pub fn expect_verbatim(&self) -> &Verbatim {
322 self.children.expect_verbatim()
323 }
324
325 pub fn expect_table(&self) -> &Table {
327 self.children.expect_table()
328 }
329
330 pub fn find_paragraphs<F>(&self, predicate: F) -> Vec<&Paragraph>
332 where
333 F: Fn(&Paragraph) -> bool,
334 {
335 self.children.find_paragraphs(predicate)
336 }
337
338 pub fn find_sessions<F>(&self, predicate: F) -> Vec<&Session>
340 where
341 F: Fn(&Session) -> bool,
342 {
343 self.children.find_sessions(predicate)
344 }
345
346 pub fn find_lists<F>(&self, predicate: F) -> Vec<&List>
348 where
349 F: Fn(&List) -> bool,
350 {
351 self.children.find_lists(predicate)
352 }
353
354 pub fn find_definitions<F>(&self, predicate: F) -> Vec<&Definition>
356 where
357 F: Fn(&Definition) -> bool,
358 {
359 self.children.find_definitions(predicate)
360 }
361
362 pub fn find_annotations<F>(&self, predicate: F) -> Vec<&Annotation>
364 where
365 F: Fn(&Annotation) -> bool,
366 {
367 self.children.find_annotations(predicate)
368 }
369
370 pub fn find_nodes<F>(&self, predicate: F) -> Vec<&ContentItem>
372 where
373 F: Fn(&ContentItem) -> bool,
374 {
375 self.children.find_nodes(predicate)
376 }
377
378 pub fn find_nodes_at_depth(&self, target_depth: usize) -> Vec<&ContentItem> {
380 self.children.find_nodes_at_depth(target_depth)
381 }
382
383 pub fn find_nodes_in_depth_range(
385 &self,
386 min_depth: usize,
387 max_depth: usize,
388 ) -> Vec<&ContentItem> {
389 self.children
390 .find_nodes_in_depth_range(min_depth, max_depth)
391 }
392
393 pub fn find_nodes_with_depth<F>(&self, target_depth: usize, predicate: F) -> Vec<&ContentItem>
395 where
396 F: Fn(&ContentItem) -> bool,
397 {
398 self.children.find_nodes_with_depth(target_depth, predicate)
399 }
400
401 pub fn count_by_type(&self) -> (usize, usize, usize, usize) {
403 self.children.count_by_type()
404 }
405
406 pub fn element_at(&self, pos: Position) -> Option<&ContentItem> {
408 self.children.element_at(pos)
409 }
410
411 pub fn visual_line_at(&self, pos: Position) -> Option<&ContentItem> {
413 self.children.visual_line_at(pos)
414 }
415
416 pub fn block_element_at(&self, pos: Position) -> Option<&ContentItem> {
418 self.children.block_element_at(pos)
419 }
420
421 pub fn find_nodes_at_position(&self, position: Position) -> Vec<&dyn AstNode> {
423 self.children.find_nodes_at_position(position)
424 }
425
426 pub fn node_path_at_position(&self, pos: Position) -> Vec<&dyn AstNode> {
428 let path = self.children.node_path_at_position(pos);
429 if !path.is_empty() {
430 let mut nodes: Vec<&dyn AstNode> = Vec::with_capacity(path.len() + 1);
431 nodes.push(self);
432 for item in path {
433 nodes.push(item);
434 }
435 nodes
436 } else if self.location.contains(pos) {
437 vec![self]
438 } else {
439 Vec::new()
440 }
441 }
442
443 pub fn format_at_position(&self, position: Position) -> String {
445 self.children.format_at_position(position)
446 }
447
448 pub fn find_annotation_by_label(&self, label: &str) -> Option<&Annotation> {
470 self.iter_annotations_recursive()
471 .find(|ann| ann.data.label.value == label)
472 }
473
474 pub fn find_annotations_by_label(&self, label: &str) -> Vec<&Annotation> {
494 self.iter_annotations_recursive()
495 .filter(|ann| ann.data.label.value == label)
496 .collect()
497 }
498
499 pub fn iter_all_references(
526 &self,
527 ) -> Box<dyn Iterator<Item = crate::lex::inlines::ReferenceInline> + '_> {
528 use crate::lex::inlines::InlineNode;
529
530 let extract_refs = |text_content: &TextContent| {
532 let inlines = text_content.inline_items();
533 inlines
534 .into_iter()
535 .filter_map(|node| {
536 if let InlineNode::Reference { data, .. } = node {
537 Some(data)
538 } else {
539 None
540 }
541 })
542 .collect::<Vec<_>>()
543 };
544
545 let title_refs = extract_refs(&self.title);
547
548 let paragraphs: Vec<_> = self.iter_paragraphs_recursive().collect();
550
551 let para_refs: Vec<_> = paragraphs
553 .into_iter()
554 .flat_map(|para| {
555 para.lines
557 .iter()
558 .filter_map(|item| {
559 if let super::content_item::ContentItem::TextLine(text_line) = item {
560 Some(&text_line.content)
561 } else {
562 None
563 }
564 })
565 .flat_map(extract_refs)
566 .collect::<Vec<_>>()
567 })
568 .collect();
569
570 Box::new(title_refs.into_iter().chain(para_refs))
571 }
572
573 pub fn find_references_to(&self, target: &str) -> Vec<crate::lex::inlines::ReferenceInline> {
591 use crate::lex::inlines::ReferenceType;
592
593 self.iter_all_references()
594 .filter(|reference| match &reference.reference_type {
595 ReferenceType::FootnoteNumber { number } => target == number.to_string(),
596 ReferenceType::FootnoteLabeled { label } => target == label,
597 ReferenceType::Session { target: ref_target } => target == ref_target,
598 ReferenceType::General { target: ref_target } => target == ref_target,
599 ReferenceType::Citation(data) => data.keys.iter().any(|key| key == target),
600 _ => false,
601 })
602 .collect()
603 }
604}
605
606impl AstNode for Session {
607 fn node_type(&self) -> &'static str {
608 "Session"
609 }
610 fn display_label(&self) -> String {
611 self.title.as_string().to_string()
612 }
613 fn range(&self) -> &Range {
614 &self.location
615 }
616
617 fn accept(&self, visitor: &mut dyn Visitor) {
618 visitor.visit_session(self);
619 super::super::traits::visit_children(visitor, &self.children);
620 visitor.leave_session(self);
621 }
622}
623
624impl VisualStructure for Session {
625 fn is_source_line_node(&self) -> bool {
626 true
627 }
628
629 fn has_visual_header(&self) -> bool {
630 true
631 }
632}
633
634impl Container for Session {
635 fn label(&self) -> &str {
636 self.title.as_string()
637 }
638 fn children(&self) -> &[ContentItem] {
639 &self.children
640 }
641 fn children_mut(&mut self) -> &mut Vec<ContentItem> {
642 self.children.as_mut_vec()
643 }
644}
645
646impl fmt::Display for Session {
647 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
648 write!(
649 f,
650 "Session('{}', {} items)",
651 self.title.as_string(),
652 self.children.len()
653 )
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use super::super::paragraph::Paragraph;
660 use super::*;
661
662 #[test]
669 fn test_session_creation() {
670 let mut session = Session::with_title("Introduction".to_string());
671 session
672 .children_mut()
673 .push(ContentItem::Paragraph(Paragraph::from_line(
674 "Content".to_string(),
675 )));
676 assert_eq!(session.label(), "Introduction");
677 assert_eq!(session.children.len(), 1);
678 }
679
680 #[test]
681 fn test_session_location_builder() {
682 let location = super::super::super::range::Range::new(
683 0..0,
684 super::super::super::range::Position::new(1, 0),
685 super::super::super::range::Position::new(1, 10),
686 );
687 let session = Session::with_title("Title".to_string()).at(location.clone());
688 assert_eq!(session.location, location);
689 }
690
691 #[test]
692 fn test_session_header_and_body_locations() {
693 let title_range = Range::new(0..5, Position::new(0, 0), Position::new(0, 5));
694 let child_range = Range::new(10..20, Position::new(1, 0), Position::new(2, 0));
695 let title = TextContent::from_string("Title".to_string(), Some(title_range.clone()));
696 let child = Paragraph::from_line("Child".to_string()).at(child_range.clone());
697 let child_item = ContentItem::Paragraph(child);
698 let session = Session::new(title, vec![SessionContent::from(child_item)]).at(Range::new(
699 0..25,
700 Position::new(0, 0),
701 Position::new(2, 0),
702 ));
703
704 assert_eq!(session.header_location(), Some(&title_range));
705 assert_eq!(session.body_location().unwrap().span, child_range.span);
706 }
707
708 #[test]
709 fn test_session_annotations() {
710 let mut session = Session::with_title("Test".to_string());
711 assert_eq!(session.annotations().len(), 0);
712
713 session
714 .annotations_mut()
715 .push(Annotation::marker(super::super::label::Label::new(
716 "test".to_string(),
717 )));
718 assert_eq!(session.annotations().len(), 1);
719 assert_eq!(session.iter_annotations().count(), 1);
720 }
721
722 #[test]
723 fn test_session_delegation_to_container() {
724 let mut session = Session::with_title("Root".to_string());
726 session
727 .children
728 .push(ContentItem::Paragraph(Paragraph::from_line(
729 "Para 1".to_string(),
730 )));
731 session
732 .children
733 .push(ContentItem::Paragraph(Paragraph::from_line(
734 "Para 2".to_string(),
735 )));
736
737 assert_eq!(session.iter_paragraphs().count(), 2);
739 assert_eq!(session.first_paragraph().unwrap().text(), "Para 1");
740 assert_eq!(session.count_by_type(), (2, 0, 0, 0));
741 }
742
743 mod sequence_marker_integration {
744 use super::*;
745 use crate::lex::ast::elements::{DecorationStyle, Form, Separator};
746 use crate::lex::loader::DocumentLoader;
747
748 #[test]
749 fn parse_extracts_numerical_period_marker() {
750 let source = "1. First Session:\n\n Content here";
751 let doc = DocumentLoader::from_string(source)
752 .parse()
753 .expect("parse failed");
754
755 let session = doc
756 .root
757 .children
758 .get(0)
759 .and_then(|item| {
760 if let ContentItem::Session(session) = item {
761 Some(session)
762 } else {
763 None
764 }
765 })
766 .expect("expected session");
767
768 assert!(session.marker.is_some());
769 let marker = session.marker.as_ref().unwrap();
770 assert_eq!(marker.style, DecorationStyle::Numerical);
771 assert_eq!(marker.separator, Separator::Period);
772 assert_eq!(marker.form, Form::Short);
773 assert_eq!(marker.raw_text.as_string(), "1.");
774 }
775
776 #[test]
777 fn parse_extracts_numerical_paren_marker() {
778 let source = "1) Second Session:\n\n Content here";
779 let doc = DocumentLoader::from_string(source)
780 .parse()
781 .expect("parse failed");
782
783 let session = doc
784 .root
785 .children
786 .get(0)
787 .and_then(|item| {
788 if let ContentItem::Session(session) = item {
789 Some(session)
790 } else {
791 None
792 }
793 })
794 .expect("expected session");
795
796 assert!(session.marker.is_some());
797 let marker = session.marker.as_ref().unwrap();
798 assert_eq!(marker.style, DecorationStyle::Numerical);
799 assert_eq!(marker.separator, Separator::Parenthesis);
800 assert_eq!(marker.form, Form::Short);
801 assert_eq!(marker.raw_text.as_string(), "1)");
802 }
803
804 #[test]
805 fn parse_extracts_alphabetical_marker() {
806 let source = "a. Alpha Session:\n\n Content here";
807 let doc = DocumentLoader::from_string(source)
808 .parse()
809 .expect("parse failed");
810
811 let session = doc
812 .root
813 .children
814 .get(0)
815 .and_then(|item| {
816 if let ContentItem::Session(session) = item {
817 Some(session)
818 } else {
819 None
820 }
821 })
822 .expect("expected session");
823
824 assert!(session.marker.is_some());
825 let marker = session.marker.as_ref().unwrap();
826 assert_eq!(marker.style, DecorationStyle::Alphabetical);
827 assert_eq!(marker.separator, Separator::Period);
828 assert_eq!(marker.form, Form::Short);
829 assert_eq!(marker.raw_text.as_string(), "a.");
830 }
831
832 #[test]
833 fn parse_extracts_roman_marker() {
834 let source = "I. Roman Session:\n\n Content here";
835 let doc = DocumentLoader::from_string(source)
836 .parse()
837 .expect("parse failed");
838
839 let session = doc
840 .root
841 .children
842 .get(0)
843 .and_then(|item| {
844 if let ContentItem::Session(session) = item {
845 Some(session)
846 } else {
847 None
848 }
849 })
850 .expect("expected session");
851
852 assert!(session.marker.is_some());
853 let marker = session.marker.as_ref().unwrap();
854 assert_eq!(marker.style, DecorationStyle::Roman);
855 assert_eq!(marker.separator, Separator::Period);
856 assert_eq!(marker.form, Form::Short);
857 assert_eq!(marker.raw_text.as_string(), "I.");
858 }
859
860 #[test]
861 fn parse_extracts_extended_numerical_marker() {
862 let source = "1.2.3 Extended Session:\n\n Content here";
863 let doc = DocumentLoader::from_string(source)
864 .parse()
865 .expect("parse failed");
866
867 let session = doc
868 .root
869 .children
870 .get(0)
871 .and_then(|item| {
872 if let ContentItem::Session(session) = item {
873 Some(session)
874 } else {
875 None
876 }
877 })
878 .expect("expected session");
879
880 assert!(session.marker.is_some());
881 let marker = session.marker.as_ref().unwrap();
882 assert_eq!(marker.style, DecorationStyle::Numerical);
883 assert_eq!(marker.separator, Separator::Period);
884 assert_eq!(marker.form, Form::Extended);
885 assert_eq!(marker.raw_text.as_string(), "1.2.3");
886 }
887
888 #[test]
889 fn parse_extracts_double_paren_marker() {
890 let source = "(1) Parens Session:\n\n Content here";
891 let doc = DocumentLoader::from_string(source)
892 .parse()
893 .expect("parse failed");
894
895 let session = doc
896 .root
897 .children
898 .get(0)
899 .and_then(|item| {
900 if let ContentItem::Session(session) = item {
901 Some(session)
902 } else {
903 None
904 }
905 })
906 .expect("expected session");
907
908 assert!(session.marker.is_some());
909 let marker = session.marker.as_ref().unwrap();
910 assert_eq!(marker.style, DecorationStyle::Numerical);
911 assert_eq!(marker.separator, Separator::DoubleParens);
912 assert_eq!(marker.form, Form::Short);
913 assert_eq!(marker.raw_text.as_string(), "(1)");
914 }
915
916 #[test]
917 fn session_without_marker_has_none() {
918 let source = "Plain Session:\n\n Content here";
919 let doc = DocumentLoader::from_string(source)
920 .parse()
921 .expect("parse failed");
922
923 let session = doc
924 .root
925 .children
926 .get(0)
927 .and_then(|item| {
928 if let ContentItem::Session(session) = item {
929 Some(session)
930 } else {
931 None
932 }
933 })
934 .expect("expected session");
935
936 assert!(session.marker.is_none());
937 }
938
939 #[test]
940 fn title_text_excludes_marker() {
941 let source = "1. Introduction:\n\n Content here";
942 let doc = DocumentLoader::from_string(source)
943 .parse()
944 .expect("parse failed");
945
946 let session = doc
947 .root
948 .children
949 .get(0)
950 .and_then(|item| {
951 if let ContentItem::Session(session) = item {
952 Some(session)
953 } else {
954 None
955 }
956 })
957 .expect("expected session");
958
959 assert_eq!(session.title_text(), "Introduction:");
961 assert_eq!(session.full_title(), "1. Introduction:");
962 }
963
964 #[test]
965 fn title_text_without_marker_returns_full_title() {
966 let source = "Plain Title:\n\n Content here";
967 let doc = DocumentLoader::from_string(source)
968 .parse()
969 .expect("parse failed");
970
971 let session = doc
972 .root
973 .children
974 .get(0)
975 .and_then(|item| {
976 if let ContentItem::Session(session) = item {
977 Some(session)
978 } else {
979 None
980 }
981 })
982 .expect("expected session");
983
984 assert_eq!(session.title_text(), "Plain Title:");
986 assert_eq!(session.full_title(), "Plain Title:");
987 }
988
989 #[test]
990 fn plain_dash_not_valid_for_sessions() {
991 let source = "- Not A Marker:\n\n Content here";
994 let doc = DocumentLoader::from_string(source)
995 .parse()
996 .expect("parse failed");
997
998 let session = doc
999 .root
1000 .children
1001 .get(0)
1002 .and_then(|item| {
1003 if let ContentItem::Session(session) = item {
1004 Some(session)
1005 } else {
1006 None
1007 }
1008 })
1009 .expect("expected session");
1010
1011 assert!(
1013 session.marker.is_none(),
1014 "Dash should not be treated as a marker for sessions"
1015 );
1016 assert_eq!(session.full_title(), "- Not A Marker:");
1017 }
1018 }
1019}