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