use super::super::range::{Position, Range};
use super::super::text_content::TextContent;
use super::super::traits::{AstNode, Container, Visitor, VisualStructure};
use super::annotation::Annotation;
use super::container::SessionContainer;
use super::content_item::ContentItem;
use super::definition::Definition;
use super::list::{List, ListItem};
use super::paragraph::Paragraph;
use super::table::Table;
use super::typed_content::SessionContent;
use super::verbatim::Verbatim;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct Session {
pub title: TextContent,
pub marker: Option<super::sequence_marker::SequenceMarker>,
pub children: SessionContainer,
pub annotations: Vec<Annotation>,
pub location: Range,
}
impl Session {
fn default_location() -> Range {
Range::new(0..0, Position::new(0, 0), Position::new(0, 0))
}
pub fn new(title: TextContent, children: Vec<SessionContent>) -> Self {
Self {
title,
marker: None,
children: SessionContainer::from_typed(children),
annotations: Vec::new(),
location: Self::default_location(),
}
}
pub fn with_title(title: String) -> Self {
Self {
title: TextContent::from_string(title, None),
marker: None,
children: SessionContainer::empty(),
annotations: Vec::new(),
location: Self::default_location(),
}
}
pub fn at(mut self, location: Range) -> Self {
self.location = location;
self
}
pub fn annotations(&self) -> &[Annotation] {
&self.annotations
}
pub fn header_location(&self) -> Option<&Range> {
self.title.location.as_ref()
}
pub fn body_location(&self) -> Option<Range> {
Range::bounding_box(self.children.iter().map(|item| item.range()))
}
pub fn title_text(&self) -> &str {
if let Some(marker) = &self.marker {
let full_title = self.title.as_string();
let marker_text = marker.as_str();
if let Some(pos) = full_title.find(marker_text) {
let after_marker = &full_title[pos + marker_text.len()..];
return after_marker.trim_start();
}
}
self.title.as_string()
}
pub fn full_title(&self) -> &str {
self.title.as_string()
}
pub fn annotations_mut(&mut self) -> &mut Vec<Annotation> {
&mut self.annotations
}
pub fn iter_annotations(&self) -> std::slice::Iter<'_, Annotation> {
self.annotations.iter()
}
pub fn iter_annotation_contents(&self) -> impl Iterator<Item = &ContentItem> {
self.annotations
.iter()
.flat_map(|annotation| annotation.children())
}
pub fn iter_items(&self) -> impl Iterator<Item = &ContentItem> {
self.children.iter()
}
pub fn iter_paragraphs(&self) -> impl Iterator<Item = &Paragraph> {
self.children.iter_paragraphs()
}
pub fn iter_sessions(&self) -> impl Iterator<Item = &Session> {
self.children.iter_sessions()
}
pub fn iter_lists(&self) -> impl Iterator<Item = &List> {
self.children.iter_lists()
}
pub fn iter_verbatim_blocks(&self) -> impl Iterator<Item = &Verbatim> {
self.children.iter_verbatim_blocks()
}
pub fn iter_all_nodes(&self) -> Box<dyn Iterator<Item = &ContentItem> + '_> {
self.children.iter_all_nodes()
}
pub fn iter_all_nodes_with_depth(
&self,
) -> Box<dyn Iterator<Item = (&ContentItem, usize)> + '_> {
self.children.iter_all_nodes_with_depth()
}
pub fn iter_paragraphs_recursive(&self) -> Box<dyn Iterator<Item = &Paragraph> + '_> {
self.children.iter_paragraphs_recursive()
}
pub fn iter_sessions_recursive(&self) -> Box<dyn Iterator<Item = &Session> + '_> {
self.children.iter_sessions_recursive()
}
pub fn iter_lists_recursive(&self) -> Box<dyn Iterator<Item = &List> + '_> {
self.children.iter_lists_recursive()
}
pub fn iter_verbatim_blocks_recursive(&self) -> Box<dyn Iterator<Item = &Verbatim> + '_> {
self.children.iter_verbatim_blocks_recursive()
}
pub fn iter_list_items_recursive(&self) -> Box<dyn Iterator<Item = &ListItem> + '_> {
self.children.iter_list_items_recursive()
}
pub fn iter_definitions_recursive(&self) -> Box<dyn Iterator<Item = &Definition> + '_> {
self.children.iter_definitions_recursive()
}
pub fn iter_annotations_recursive(&self) -> Box<dyn Iterator<Item = &Annotation> + '_> {
self.children.iter_annotations_recursive()
}
pub fn first_paragraph(&self) -> Option<&Paragraph> {
self.children.first_paragraph()
}
pub fn first_session(&self) -> Option<&Session> {
self.children.first_session()
}
pub fn first_list(&self) -> Option<&List> {
self.children.first_list()
}
pub fn first_definition(&self) -> Option<&Definition> {
self.children.first_definition()
}
pub fn first_annotation(&self) -> Option<&Annotation> {
self.children.first_annotation()
}
pub fn first_verbatim(&self) -> Option<&Verbatim> {
self.children.first_verbatim()
}
pub fn expect_paragraph(&self) -> &Paragraph {
self.children.expect_paragraph()
}
pub fn expect_session(&self) -> &Session {
self.children.expect_session()
}
pub fn expect_list(&self) -> &List {
self.children.expect_list()
}
pub fn expect_definition(&self) -> &Definition {
self.children.expect_definition()
}
pub fn expect_annotation(&self) -> &Annotation {
self.children.expect_annotation()
}
pub fn expect_verbatim(&self) -> &Verbatim {
self.children.expect_verbatim()
}
pub fn expect_table(&self) -> &Table {
self.children.expect_table()
}
pub fn find_paragraphs<F>(&self, predicate: F) -> Vec<&Paragraph>
where
F: Fn(&Paragraph) -> bool,
{
self.children.find_paragraphs(predicate)
}
pub fn find_sessions<F>(&self, predicate: F) -> Vec<&Session>
where
F: Fn(&Session) -> bool,
{
self.children.find_sessions(predicate)
}
pub fn find_lists<F>(&self, predicate: F) -> Vec<&List>
where
F: Fn(&List) -> bool,
{
self.children.find_lists(predicate)
}
pub fn find_definitions<F>(&self, predicate: F) -> Vec<&Definition>
where
F: Fn(&Definition) -> bool,
{
self.children.find_definitions(predicate)
}
pub fn find_annotations<F>(&self, predicate: F) -> Vec<&Annotation>
where
F: Fn(&Annotation) -> bool,
{
self.children.find_annotations(predicate)
}
pub fn find_nodes<F>(&self, predicate: F) -> Vec<&ContentItem>
where
F: Fn(&ContentItem) -> bool,
{
self.children.find_nodes(predicate)
}
pub fn find_nodes_at_depth(&self, target_depth: usize) -> Vec<&ContentItem> {
self.children.find_nodes_at_depth(target_depth)
}
pub fn find_nodes_in_depth_range(
&self,
min_depth: usize,
max_depth: usize,
) -> Vec<&ContentItem> {
self.children
.find_nodes_in_depth_range(min_depth, max_depth)
}
pub fn find_nodes_with_depth<F>(&self, target_depth: usize, predicate: F) -> Vec<&ContentItem>
where
F: Fn(&ContentItem) -> bool,
{
self.children.find_nodes_with_depth(target_depth, predicate)
}
pub fn count_by_type(&self) -> (usize, usize, usize, usize) {
self.children.count_by_type()
}
pub fn element_at(&self, pos: Position) -> Option<&ContentItem> {
self.children.element_at(pos)
}
pub fn visual_line_at(&self, pos: Position) -> Option<&ContentItem> {
self.children.visual_line_at(pos)
}
pub fn block_element_at(&self, pos: Position) -> Option<&ContentItem> {
self.children.block_element_at(pos)
}
pub fn find_nodes_at_position(&self, position: Position) -> Vec<&dyn AstNode> {
self.children.find_nodes_at_position(position)
}
pub fn node_path_at_position(&self, pos: Position) -> Vec<&dyn AstNode> {
let path = self.children.node_path_at_position(pos);
if !path.is_empty() {
let mut nodes: Vec<&dyn AstNode> = Vec::with_capacity(path.len() + 1);
nodes.push(self);
for item in path {
nodes.push(item);
}
nodes
} else if self.location.contains(pos) {
vec![self]
} else {
Vec::new()
}
}
pub fn format_at_position(&self, position: Position) -> String {
self.children.format_at_position(position)
}
pub fn find_annotation_by_label(&self, label: &str) -> Option<&Annotation> {
self.iter_annotations_recursive()
.find(|ann| ann.data.label.value == label)
}
pub fn find_annotations_by_label(&self, label: &str) -> Vec<&Annotation> {
self.iter_annotations_recursive()
.filter(|ann| ann.data.label.value == label)
.collect()
}
pub fn iter_all_references(
&self,
) -> Box<dyn Iterator<Item = crate::lex::inlines::ReferenceInline> + '_> {
use crate::lex::inlines::InlineNode;
let extract_refs = |text_content: &TextContent| {
let inlines = text_content.inline_items();
inlines
.into_iter()
.filter_map(|node| {
if let InlineNode::Reference { data, .. } = node {
Some(data)
} else {
None
}
})
.collect::<Vec<_>>()
};
let title_refs = extract_refs(&self.title);
let paragraphs: Vec<_> = self.iter_paragraphs_recursive().collect();
let para_refs: Vec<_> = paragraphs
.into_iter()
.flat_map(|para| {
para.lines
.iter()
.filter_map(|item| {
if let super::content_item::ContentItem::TextLine(text_line) = item {
Some(&text_line.content)
} else {
None
}
})
.flat_map(extract_refs)
.collect::<Vec<_>>()
})
.collect();
Box::new(title_refs.into_iter().chain(para_refs))
}
pub fn find_references_to(&self, target: &str) -> Vec<crate::lex::inlines::ReferenceInline> {
use crate::lex::inlines::ReferenceType;
self.iter_all_references()
.filter(|reference| match &reference.reference_type {
ReferenceType::FootnoteNumber { number } => target == number.to_string(),
ReferenceType::AnnotationReference { label } => target == label,
ReferenceType::Session { target: ref_target } => target == ref_target,
ReferenceType::General { target: ref_target } => target == ref_target,
ReferenceType::Citation(data) => data.keys.iter().any(|key| key == target),
_ => false,
})
.collect()
}
}
impl AstNode for Session {
fn node_type(&self) -> &'static str {
"Session"
}
fn display_label(&self) -> String {
self.title.as_string().to_string()
}
fn range(&self) -> &Range {
&self.location
}
fn accept(&self, visitor: &mut dyn Visitor) {
visitor.visit_session(self);
super::super::traits::visit_children(visitor, &self.children);
visitor.leave_session(self);
}
}
impl VisualStructure for Session {
fn is_source_line_node(&self) -> bool {
true
}
fn has_visual_header(&self) -> bool {
true
}
}
impl Container for Session {
fn label(&self) -> &str {
self.title.as_string()
}
fn children(&self) -> &[ContentItem] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<ContentItem> {
self.children.as_mut_vec()
}
}
impl fmt::Display for Session {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Session('{}', {} items)",
self.title.as_string(),
self.children.len()
)
}
}
#[cfg(test)]
mod tests {
use super::super::paragraph::Paragraph;
use super::*;
#[test]
fn test_session_creation() {
let mut session = Session::with_title("Introduction".to_string());
session
.children_mut()
.push(ContentItem::Paragraph(Paragraph::from_line(
"Content".to_string(),
)));
assert_eq!(session.label(), "Introduction");
assert_eq!(session.children.len(), 1);
}
#[test]
fn test_session_location_builder() {
let location = super::super::super::range::Range::new(
0..0,
super::super::super::range::Position::new(1, 0),
super::super::super::range::Position::new(1, 10),
);
let session = Session::with_title("Title".to_string()).at(location.clone());
assert_eq!(session.location, location);
}
#[test]
fn test_session_header_and_body_locations() {
let title_range = Range::new(0..5, Position::new(0, 0), Position::new(0, 5));
let child_range = Range::new(10..20, Position::new(1, 0), Position::new(2, 0));
let title = TextContent::from_string("Title".to_string(), Some(title_range.clone()));
let child = Paragraph::from_line("Child".to_string()).at(child_range.clone());
let child_item = ContentItem::Paragraph(child);
let session = Session::new(title, vec![SessionContent::from(child_item)]).at(Range::new(
0..25,
Position::new(0, 0),
Position::new(2, 0),
));
assert_eq!(session.header_location(), Some(&title_range));
assert_eq!(session.body_location().unwrap().span, child_range.span);
}
#[test]
fn test_session_annotations() {
let mut session = Session::with_title("Test".to_string());
assert_eq!(session.annotations().len(), 0);
session
.annotations_mut()
.push(Annotation::marker(super::super::label::Label::new(
"test".to_string(),
)));
assert_eq!(session.annotations().len(), 1);
assert_eq!(session.iter_annotations().count(), 1);
}
#[test]
fn test_session_delegation_to_container() {
let mut session = Session::with_title("Root".to_string());
session
.children
.push(ContentItem::Paragraph(Paragraph::from_line(
"Para 1".to_string(),
)));
session
.children
.push(ContentItem::Paragraph(Paragraph::from_line(
"Para 2".to_string(),
)));
assert_eq!(session.iter_paragraphs().count(), 2);
assert_eq!(session.first_paragraph().unwrap().text(), "Para 1");
assert_eq!(session.count_by_type(), (2, 0, 0, 0));
}
mod sequence_marker_integration {
use super::*;
use crate::lex::ast::elements::{DecorationStyle, Form, Separator};
use crate::lex::loader::DocumentLoader;
#[test]
fn parse_extracts_numerical_period_marker() {
let source = "1. First Session:\n\n Content here";
let doc = DocumentLoader::from_string(source)
.parse()
.expect("parse failed");
let session = doc
.root
.children
.get(0)
.and_then(|item| {
if let ContentItem::Session(session) = item {
Some(session)
} else {
None
}
})
.expect("expected session");
assert!(session.marker.is_some());
let marker = session.marker.as_ref().unwrap();
assert_eq!(marker.style, DecorationStyle::Numerical);
assert_eq!(marker.separator, Separator::Period);
assert_eq!(marker.form, Form::Short);
assert_eq!(marker.raw_text.as_string(), "1.");
}
#[test]
fn parse_extracts_numerical_paren_marker() {
let source = "1) Second Session:\n\n Content here";
let doc = DocumentLoader::from_string(source)
.parse()
.expect("parse failed");
let session = doc
.root
.children
.get(0)
.and_then(|item| {
if let ContentItem::Session(session) = item {
Some(session)
} else {
None
}
})
.expect("expected session");
assert!(session.marker.is_some());
let marker = session.marker.as_ref().unwrap();
assert_eq!(marker.style, DecorationStyle::Numerical);
assert_eq!(marker.separator, Separator::Parenthesis);
assert_eq!(marker.form, Form::Short);
assert_eq!(marker.raw_text.as_string(), "1)");
}
#[test]
fn parse_extracts_alphabetical_marker() {
let source = "a. Alpha Session:\n\n Content here";
let doc = DocumentLoader::from_string(source)
.parse()
.expect("parse failed");
let session = doc
.root
.children
.get(0)
.and_then(|item| {
if let ContentItem::Session(session) = item {
Some(session)
} else {
None
}
})
.expect("expected session");
assert!(session.marker.is_some());
let marker = session.marker.as_ref().unwrap();
assert_eq!(marker.style, DecorationStyle::Alphabetical);
assert_eq!(marker.separator, Separator::Period);
assert_eq!(marker.form, Form::Short);
assert_eq!(marker.raw_text.as_string(), "a.");
}
#[test]
fn parse_extracts_roman_marker() {
let source = "I. Roman Session:\n\n Content here";
let doc = DocumentLoader::from_string(source)
.parse()
.expect("parse failed");
let session = doc
.root
.children
.get(0)
.and_then(|item| {
if let ContentItem::Session(session) = item {
Some(session)
} else {
None
}
})
.expect("expected session");
assert!(session.marker.is_some());
let marker = session.marker.as_ref().unwrap();
assert_eq!(marker.style, DecorationStyle::Roman);
assert_eq!(marker.separator, Separator::Period);
assert_eq!(marker.form, Form::Short);
assert_eq!(marker.raw_text.as_string(), "I.");
}
#[test]
fn parse_extracts_extended_numerical_marker() {
let source = "1.2.3 Extended Session:\n\n Content here";
let doc = DocumentLoader::from_string(source)
.parse()
.expect("parse failed");
let session = doc
.root
.children
.get(0)
.and_then(|item| {
if let ContentItem::Session(session) = item {
Some(session)
} else {
None
}
})
.expect("expected session");
assert!(session.marker.is_some());
let marker = session.marker.as_ref().unwrap();
assert_eq!(marker.style, DecorationStyle::Numerical);
assert_eq!(marker.separator, Separator::Period);
assert_eq!(marker.form, Form::Extended);
assert_eq!(marker.raw_text.as_string(), "1.2.3");
}
#[test]
fn parse_extracts_double_paren_marker() {
let source = "(1) Parens Session:\n\n Content here";
let doc = DocumentLoader::from_string(source)
.parse()
.expect("parse failed");
let session = doc
.root
.children
.get(0)
.and_then(|item| {
if let ContentItem::Session(session) = item {
Some(session)
} else {
None
}
})
.expect("expected session");
assert!(session.marker.is_some());
let marker = session.marker.as_ref().unwrap();
assert_eq!(marker.style, DecorationStyle::Numerical);
assert_eq!(marker.separator, Separator::DoubleParens);
assert_eq!(marker.form, Form::Short);
assert_eq!(marker.raw_text.as_string(), "(1)");
}
#[test]
fn session_without_marker_has_none() {
let source = "Plain Session:\n\n Content here";
let doc = DocumentLoader::from_string(source)
.parse()
.expect("parse failed");
let session = doc
.root
.children
.get(0)
.and_then(|item| {
if let ContentItem::Session(session) = item {
Some(session)
} else {
None
}
})
.expect("expected session");
assert!(session.marker.is_none());
}
#[test]
fn title_text_excludes_marker() {
let source = "1. Introduction:\n\n Content here";
let doc = DocumentLoader::from_string(source)
.parse()
.expect("parse failed");
let session = doc
.root
.children
.get(0)
.and_then(|item| {
if let ContentItem::Session(session) = item {
Some(session)
} else {
None
}
})
.expect("expected session");
assert_eq!(session.title_text(), "Introduction:");
assert_eq!(session.full_title(), "1. Introduction:");
}
#[test]
fn title_text_without_marker_returns_full_title() {
let source = "Plain Title:\n\n Content here";
let doc = DocumentLoader::from_string(source)
.parse()
.expect("parse failed");
let session = doc
.root
.children
.get(0)
.and_then(|item| {
if let ContentItem::Session(session) = item {
Some(session)
} else {
None
}
})
.expect("expected session");
assert_eq!(session.title_text(), "Plain Title:");
assert_eq!(session.full_title(), "Plain Title:");
}
#[test]
fn plain_dash_not_valid_for_sessions() {
let source = "- Not A Marker:\n\n Content here";
let doc = DocumentLoader::from_string(source)
.parse()
.expect("parse failed");
let session = doc
.root
.children
.get(0)
.and_then(|item| {
if let ContentItem::Session(session) = item {
Some(session)
} else {
None
}
})
.expect("expected session");
assert!(
session.marker.is_none(),
"Dash should not be treated as a marker for sessions"
);
assert_eq!(session.full_title(), "- Not A Marker:");
}
}
}