use super::trait_helpers::get_visual_header;
use super::traits::{AstNode, Container};
use super::{
Annotation, ContentItem, Definition, Document, List, ListItem, Paragraph, Range, Session,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AstSnapshot {
pub node_type: String,
pub label: String,
pub attributes: HashMap<String, String>,
pub range: Range,
pub children: Vec<AstSnapshot>,
}
impl AstSnapshot {
pub fn new(node_type: String, label: String, range: Range) -> Self {
Self {
node_type,
label,
attributes: HashMap::new(),
range,
children: Vec::new(),
}
}
pub fn with_attribute(mut self, key: String, value: String) -> Self {
self.attributes.insert(key, value);
self
}
pub fn with_child(mut self, child: AstSnapshot) -> Self {
self.children.push(child);
self
}
pub fn with_children(mut self, children: Vec<AstSnapshot>) -> Self {
self.children.extend(children);
self
}
}
pub fn snapshot_node<T: AstNode>(node: &T) -> AstSnapshot {
let node_type = node.node_type();
let label = node.display_label();
AstSnapshot::new(node_type.to_string(), label, node.range().clone())
}
pub fn snapshot_from_content(item: &ContentItem) -> AstSnapshot {
snapshot_from_content_with_options(item, false)
}
pub fn snapshot_from_content_with_options(item: &ContentItem, include_all: bool) -> AstSnapshot {
match item {
ContentItem::Session(session) => build_session_snapshot(session, include_all),
ContentItem::Paragraph(para) => build_paragraph_snapshot(para, include_all),
ContentItem::List(list) => build_list_snapshot(list, include_all),
ContentItem::ListItem(li) => build_list_item_snapshot(li, include_all),
ContentItem::Definition(def) => build_definition_snapshot(def, include_all),
ContentItem::VerbatimBlock(fb) => build_verbatim_block_snapshot(fb, include_all),
ContentItem::Table(t) => build_table_snapshot(t, include_all),
ContentItem::VerbatimLine(fl) => AstSnapshot::new(
"VerbatimLine".to_string(),
fl.display_label(),
fl.range().clone(),
),
ContentItem::Annotation(ann) => build_annotation_snapshot(ann, include_all),
ContentItem::TextLine(tl) => AstSnapshot::new(
"TextLine".to_string(),
tl.display_label(),
tl.range().clone(),
),
ContentItem::BlankLineGroup(blg) => AstSnapshot::new(
"BlankLineGroup".to_string(),
blg.display_label(),
blg.range().clone(),
),
}
}
pub fn snapshot_from_document(doc: &Document) -> AstSnapshot {
snapshot_from_document_with_options(doc, false)
}
pub fn snapshot_from_document_with_options(doc: &Document, include_all: bool) -> AstSnapshot {
let mut snapshot = AstSnapshot::new(
"Document".to_string(),
format!(
"Document ({} annotations, {} items)",
doc.annotations.len(),
doc.root.children.len()
),
doc.root.range().clone(),
);
if include_all {
for annotation in &doc.annotations {
snapshot.children.push(snapshot_from_content_with_options(
&ContentItem::Annotation(annotation.clone()),
include_all,
));
}
}
for child in &doc.root.children {
snapshot
.children
.push(snapshot_from_content_with_options(child, include_all));
}
snapshot
}
fn build_session_snapshot(session: &Session, include_all: bool) -> AstSnapshot {
let item = ContentItem::Session(session.clone());
let mut snapshot = AstSnapshot::new(
"Session".to_string(),
session.display_label(),
session.range().clone(),
);
if include_all {
if let Some(header) = get_visual_header(&item) {
snapshot.children.push(AstSnapshot::new(
"SessionTitle".to_string(),
header,
session.range().clone(), ));
}
}
if include_all {
for ann in &session.annotations {
snapshot.children.push(snapshot_from_content_with_options(
&ContentItem::Annotation(ann.clone()),
include_all,
));
}
}
for child in session.children() {
snapshot
.children
.push(snapshot_from_content_with_options(child, include_all));
}
snapshot
}
fn build_paragraph_snapshot(para: &Paragraph, include_all: bool) -> AstSnapshot {
let mut snapshot = AstSnapshot::new(
"Paragraph".to_string(),
para.display_label(),
para.range().clone(),
);
for line in ¶.lines {
snapshot
.children
.push(snapshot_from_content_with_options(line, include_all));
}
snapshot
}
fn build_list_snapshot(list: &List, include_all: bool) -> AstSnapshot {
let mut snapshot = AstSnapshot::new(
"List".to_string(),
list.display_label(),
list.range().clone(),
);
for item in &list.items {
snapshot
.children
.push(snapshot_from_content_with_options(item, include_all));
}
snapshot
}
fn build_list_item_snapshot(item: &ListItem, include_all: bool) -> AstSnapshot {
let mut snapshot = AstSnapshot::new(
"ListItem".to_string(),
item.display_label(),
item.range().clone(),
);
if include_all {
snapshot.children.push(AstSnapshot::new(
"Marker".to_string(),
item.marker.as_string().to_string(),
item.range().clone(), ));
for text_part in item.text.iter() {
snapshot.children.push(AstSnapshot::new(
"Text".to_string(),
text_part.as_string().to_string(),
item.range().clone(), ));
}
for ann in &item.annotations {
snapshot.children.push(snapshot_from_content_with_options(
&ContentItem::Annotation(ann.clone()),
include_all,
));
}
}
for child in item.children() {
snapshot
.children
.push(snapshot_from_content_with_options(child, include_all));
}
snapshot
}
fn build_definition_snapshot(def: &Definition, include_all: bool) -> AstSnapshot {
let item = ContentItem::Definition(def.clone());
let mut snapshot = AstSnapshot::new(
"Definition".to_string(),
def.display_label(),
def.range().clone(),
);
if include_all {
if let Some(header) = get_visual_header(&item) {
snapshot.children.push(AstSnapshot::new(
"Subject".to_string(),
header,
def.range().clone(), ));
}
for ann in &def.annotations {
snapshot.children.push(snapshot_from_content_with_options(
&ContentItem::Annotation(ann.clone()),
include_all,
));
}
}
for child in def.children() {
snapshot
.children
.push(snapshot_from_content_with_options(child, include_all));
}
snapshot
}
fn build_annotation_snapshot(ann: &Annotation, include_all: bool) -> AstSnapshot {
let item = ContentItem::Annotation(ann.clone());
let mut snapshot = AstSnapshot::new(
"Annotation".to_string(),
ann.display_label(),
ann.range().clone(),
);
if include_all {
if let Some(header) = get_visual_header(&item) {
snapshot.children.push(AstSnapshot::new(
"Label".to_string(),
header,
ann.range().clone(), ));
}
for param in &ann.data.parameters {
snapshot.children.push(AstSnapshot::new(
"Parameter".to_string(),
format!("{}={}", param.key, param.value),
ann.range().clone(), ));
}
}
for child in ann.children() {
snapshot
.children
.push(snapshot_from_content_with_options(child, include_all));
}
snapshot
}
fn build_table_snapshot(t: &super::Table, include_all: bool) -> AstSnapshot {
let label = format!(
"{} ({} header + {} body rows)",
t.display_label(),
t.header_rows.len(),
t.body_rows.len()
);
let mut snapshot = AstSnapshot::new("Table".to_string(), label, t.range().clone());
if include_all {
for row in t.all_rows() {
for cell in &row.cells {
if cell.has_block_content() {
let mut cell_snapshot = AstSnapshot::new(
"TableCell".to_string(),
cell.content.as_string().to_string(),
cell.location.clone(),
);
for child in cell.children.iter() {
cell_snapshot
.children
.push(snapshot_from_content_with_options(child, include_all));
}
snapshot.children.push(cell_snapshot);
}
}
}
}
snapshot
}
fn build_verbatim_block_snapshot(fb: &super::Verbatim, include_all: bool) -> AstSnapshot {
let group_count = fb.group_len();
let group_word = if group_count == 1 { "group" } else { "groups" };
let label = format!("{} ({} {})", fb.display_label(), group_count, group_word);
let mut snapshot = AstSnapshot::new("VerbatimBlock".to_string(), label, fb.range().clone());
for (idx, group) in fb.group().enumerate() {
let label = if group_count == 1 {
group.subject.as_string().to_string()
} else {
format!(
"{} (group {} of {})",
group.subject.as_string(),
idx + 1,
group_count
)
};
let mut group_snapshot = AstSnapshot::new(
"VerbatimGroup".to_string(),
label,
fb.range().clone(), );
for child in group.children.iter() {
group_snapshot
.children
.push(snapshot_from_content_with_options(child, include_all));
}
snapshot.children.push(group_snapshot);
}
snapshot
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lex::ast::elements::annotation::Annotation;
use crate::lex::ast::elements::paragraph::Paragraph;
use crate::lex::ast::elements::session::Session;
use crate::lex::ast::elements::typed_content::ContentElement;
#[test]
fn test_snapshot_from_document_empty() {
let doc = Document::new();
let snapshot = snapshot_from_document(&doc);
assert_eq!(snapshot.node_type, "Document");
assert_eq!(snapshot.label, "Document (0 annotations, 0 items)");
assert!(snapshot.children.is_empty());
}
#[test]
fn test_snapshot_from_document_with_content() {
let mut doc = Document::new();
doc.root
.children
.push(ContentItem::Paragraph(Paragraph::from_line(
"Test".to_string(),
)));
doc.root
.children
.push(ContentItem::Session(Session::with_title(
"Section".to_string(),
)));
let snapshot = snapshot_from_document(&doc);
assert_eq!(snapshot.node_type, "Document");
assert_eq!(snapshot.label, "Document (0 annotations, 2 items)");
assert_eq!(snapshot.children.len(), 2);
assert_eq!(snapshot.children[0].node_type, "Paragraph");
assert_eq!(snapshot.children[1].node_type, "Session");
}
#[test]
fn test_snapshot_excludes_annotations() {
use crate::lex::ast::elements::label::Label;
let annotation = Annotation::new(
Label::new("test-label".to_string()),
vec![],
Vec::<ContentElement>::new(),
);
let doc = Document::with_annotations_and_content(
vec![annotation],
vec![ContentItem::Paragraph(Paragraph::from_line(
"Test".to_string(),
))],
);
let snapshot = snapshot_from_document(&doc);
assert_eq!(snapshot.label, "Document (1 annotations, 1 items)");
assert_eq!(snapshot.children.len(), 1);
assert_eq!(snapshot.children[0].node_type, "Paragraph");
assert!(snapshot
.children
.iter()
.all(|child| child.node_type != "Annotation"));
}
#[test]
fn test_snapshot_from_document_preserves_structure() {
let mut session = Session::with_title("Main".to_string());
session
.children
.push(ContentItem::Paragraph(Paragraph::from_line(
"Para 1".to_string(),
)));
let mut doc = Document::new();
doc.root.children.push(ContentItem::Session(session));
let snapshot = snapshot_from_document(&doc);
assert_eq!(snapshot.node_type, "Document");
assert_eq!(snapshot.children.len(), 1);
let session_snapshot = &snapshot.children[0];
assert_eq!(session_snapshot.node_type, "Session");
assert_eq!(session_snapshot.children.len(), 1);
assert_eq!(session_snapshot.children[0].node_type, "Paragraph");
}
}