1use crate::ir::events::Event;
55use crate::ir::nodes::*;
56
57#[derive(Debug, Clone, PartialEq)]
59pub enum ConversionError {
60 UnexpectedEnd(String),
62 MismatchedEvents { expected: String, found: String },
64 UnexpectedInline(String),
66 ExtraEvents,
68 UnclosedContainers(usize),
70}
71
72impl std::fmt::Display for ConversionError {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 ConversionError::UnexpectedEnd(msg) => write!(f, "Unexpected end event: {msg}"),
76 ConversionError::MismatchedEvents { expected, found } => {
77 write!(f, "Mismatched events: expected {expected}, found {found}")
78 }
79 ConversionError::UnexpectedInline(msg) => {
80 write!(f, "Unexpected inline content: {msg}")
81 }
82 ConversionError::ExtraEvents => write!(f, "Extra events after document end"),
83 ConversionError::UnclosedContainers(count) => {
84 write!(f, "Unclosed containers: {count} nodes remain on stack")
85 }
86 }
87 }
88}
89
90impl std::error::Error for ConversionError {}
91
92#[derive(Debug)]
94enum StackNode {
95 Document(Document),
96 Heading {
97 level: usize,
98 content: Vec<InlineContent>,
99 children: Vec<DocNode>,
100 },
101 Paragraph {
102 content: Vec<InlineContent>,
103 },
104 List {
105 items: Vec<ListItem>,
106 ordered: bool,
107 style: ListStyle,
108 },
109 ListItem {
110 content: Vec<InlineContent>,
111 children: Vec<DocNode>,
112 },
113 Definition {
114 term: Vec<InlineContent>,
115 description: Vec<DocNode>,
116 in_term: bool,
117 },
118 Verbatim {
119 language: Option<String>,
120 content: String,
121 },
122 Annotation {
123 label: String,
124 parameters: Vec<(String, String)>,
125 content: Vec<DocNode>,
126 },
127 Table {
128 rows: Vec<TableRow>,
129 header: Vec<TableRow>,
130 caption: Option<Vec<InlineContent>>,
131 },
132 TableRow {
133 cells: Vec<TableCell>,
134 header: bool,
135 },
136 TableCell {
137 content: Vec<DocNode>,
138 header: bool,
139 align: TableCellAlignment,
140 },
141}
142
143impl StackNode {
144 fn into_doc_node(self) -> DocNode {
146 match self {
147 StackNode::Document(doc) => DocNode::Document(doc),
148 StackNode::Heading {
149 level,
150 content,
151 children,
152 } => DocNode::Heading(Heading {
153 level,
154 content,
155 children,
156 }),
157 StackNode::Paragraph { content } => DocNode::Paragraph(Paragraph { content }),
158 StackNode::List {
159 items,
160 ordered,
161 style,
162 } => DocNode::List(List {
163 items,
164 ordered,
165 style,
166 }),
167 StackNode::ListItem { content, children } => {
168 DocNode::ListItem(ListItem { content, children })
169 }
170 StackNode::Definition {
171 term, description, ..
172 } => DocNode::Definition(Definition { term, description }),
173 StackNode::Verbatim { language, content } => {
174 if let Some(lang) = &language {
175 if let Some(label) = lang.strip_prefix("lex-metadata:") {
176 let (header, body) = if let Some((h, b)) = content.split_once('\n') {
180 (h, Some(b.to_string()))
181 } else {
182 (content.as_str(), None)
183 };
184
185 let mut parameters = vec![];
186 for part in header.split_whitespace() {
187 if let Some((key, value)) = part.split_once('=') {
188 parameters.push((key.to_string(), value.to_string()));
189 }
190 }
191
192 let mut content_nodes = vec![];
193 if let Some(text) = body {
194 let text = text.strip_suffix('\n').unwrap_or(&text);
195
196 if !text.is_empty() {
197 content_nodes.push(DocNode::Paragraph(Paragraph {
198 content: vec![InlineContent::Text(text.to_string())],
199 }));
200 }
201 }
202
203 return DocNode::Annotation(Annotation {
204 label: label.to_string(),
205 parameters,
206 content: content_nodes,
207 });
208 }
209 }
210 DocNode::Verbatim(Verbatim { language, content })
211 }
212 StackNode::Annotation {
213 label,
214 parameters,
215 content,
216 } => DocNode::Annotation(Annotation {
217 label,
218 parameters,
219 content,
220 }),
221 StackNode::Table {
222 rows,
223 header,
224 caption,
225 } => DocNode::Table(Table {
226 rows,
227 header,
228 caption,
229 }),
230 StackNode::TableRow { cells: _, .. } => {
231 panic!("TableRow cannot be converted directly to DocNode")
234 }
235 StackNode::TableCell { .. } => {
236 panic!("TableCell cannot be converted directly to DocNode")
238 }
239 }
240 }
241
242 fn type_name(&self) -> &str {
244 match self {
245 StackNode::Document(_) => "Document",
246 StackNode::Heading { .. } => "Heading",
247 StackNode::Paragraph { .. } => "Paragraph",
248 StackNode::List { .. } => "List",
249 StackNode::ListItem { .. } => "ListItem",
250 StackNode::Definition { .. } => "Definition",
251 StackNode::Verbatim { .. } => "Verbatim",
252 StackNode::Annotation { .. } => "Annotation",
253 StackNode::Table { .. } => "Table",
254 StackNode::TableRow { .. } => "TableRow",
255 StackNode::TableCell { .. } => "TableCell",
256 }
257 }
258
259 fn add_child(&mut self, child: DocNode) -> Result<(), ConversionError> {
261 match self {
262 StackNode::Document(doc) => {
263 doc.children.push(child);
264 Ok(())
265 }
266 StackNode::Heading { children, .. } => {
267 children.push(child);
268 Ok(())
269 }
270 StackNode::ListItem { children, .. } => {
271 children.push(child);
272 Ok(())
273 }
274 StackNode::List { items, .. } => {
275 if let DocNode::ListItem(item) = child {
276 items.push(item);
277 Ok(())
278 } else {
279 Err(ConversionError::MismatchedEvents {
280 expected: "ListItem".to_string(),
281 found: format!("{child:?}"),
282 })
283 }
284 }
285 StackNode::Definition {
286 description,
287 in_term,
288 ..
289 } => {
290 if *in_term {
291 Err(ConversionError::UnexpectedInline(
292 "Cannot add child to definition term".to_string(),
293 ))
294 } else {
295 description.push(child);
296 Ok(())
297 }
298 }
299 StackNode::Annotation { content, .. } => {
300 content.push(child);
301 Ok(())
302 }
303 StackNode::TableCell { content, .. } => {
304 content.push(child);
305 Ok(())
306 }
307 _ => Err(ConversionError::UnexpectedInline(format!(
308 "Node {} cannot have children",
309 self.type_name()
310 ))),
311 }
312 }
313
314 fn add_inline(&mut self, inline: InlineContent) -> Result<(), ConversionError> {
316 match self {
317 StackNode::Heading { content, .. } => {
318 content.push(inline);
319 Ok(())
320 }
321 StackNode::Paragraph { content } => {
322 content.push(inline);
323 Ok(())
324 }
325 StackNode::ListItem { content, .. } => {
326 content.push(inline);
327 Ok(())
328 }
329 StackNode::Definition { term, in_term, .. } => {
330 if *in_term {
331 term.push(inline);
332 Ok(())
333 } else {
334 Err(ConversionError::UnexpectedInline(
335 "Inline content in definition description".to_string(),
336 ))
337 }
338 }
339 StackNode::Verbatim { content, .. } => {
340 if let InlineContent::Text(text) = inline {
341 if !content.is_empty() {
342 content.push('\n');
343 }
344 content.push_str(&text);
345 Ok(())
346 } else {
347 Err(ConversionError::UnexpectedInline(
348 "Verbatim can only contain plain text".to_string(),
349 ))
350 }
351 }
352 _ => Err(ConversionError::UnexpectedInline(format!(
353 "Cannot add inline content to {}",
354 self.type_name()
355 ))),
356 }
357 }
358}
359
360fn finalize_container<F>(
361 stack: &mut Vec<StackNode>,
362 event_name: &str,
363 parent_label: &str,
364 validate: F,
365) -> Result<(), ConversionError>
366where
367 F: FnOnce(StackNode) -> Result<StackNode, ConversionError>,
368{
369 let node = stack
370 .pop()
371 .ok_or_else(|| ConversionError::UnexpectedEnd(format!("{event_name} with empty stack")))?;
372
373 let node = validate(node)?;
374
375 let doc_node = node.into_doc_node();
376 let parent = stack
377 .last_mut()
378 .ok_or_else(|| ConversionError::UnexpectedEnd(format!("No parent for {parent_label}")))?;
379 parent.add_child(doc_node)?;
380
381 Ok(())
382}
383
384fn auto_close_headings_at_or_deeper(
397 stack: &mut Vec<StackNode>,
398 new_level: usize,
399) -> Result<(), ConversionError> {
400 let mut headings_to_close = Vec::new();
402
403 for (i, node) in stack.iter().enumerate().rev() {
404 if let StackNode::Heading { level, .. } = node {
405 if *level >= new_level {
406 headings_to_close.push(i);
407 } else {
408 break;
410 }
411 } else {
412 break;
414 }
415 }
416
417 for _ in 0..headings_to_close.len() {
419 finalize_container(stack, "auto-close heading", "heading", |node| match node {
420 StackNode::Heading { .. } => Ok(node),
421 other => Err(ConversionError::MismatchedEvents {
422 expected: "Heading".to_string(),
423 found: other.type_name().to_string(),
424 }),
425 })?;
426 }
427
428 Ok(())
429}
430
431fn auto_close_all_headings(stack: &mut Vec<StackNode>) -> Result<(), ConversionError> {
436 let mut heading_count = 0;
438 for node in stack.iter().rev() {
439 if matches!(node, StackNode::Heading { .. }) {
440 heading_count += 1;
441 } else {
442 break;
444 }
445 }
446
447 for _ in 0..heading_count {
449 finalize_container(
450 stack,
451 "auto-close heading at end",
452 "heading",
453 |node| match node {
454 StackNode::Heading { .. } => Ok(node),
455 other => Err(ConversionError::MismatchedEvents {
456 expected: "Heading".to_string(),
457 found: other.type_name().to_string(),
458 }),
459 },
460 )?;
461 }
462
463 Ok(())
464}
465
466pub fn events_to_tree(events: &[Event]) -> Result<Document, ConversionError> {
495 if events.is_empty() {
496 return Ok(Document { children: vec![] });
497 }
498
499 let mut stack: Vec<StackNode> = Vec::new();
500 let mut event_iter = events.iter().peekable();
501
502 match event_iter.next() {
504 Some(Event::StartDocument) => {
505 stack.push(StackNode::Document(Document { children: vec![] }));
506 }
507 Some(other) => {
508 return Err(ConversionError::MismatchedEvents {
509 expected: "StartDocument".to_string(),
510 found: format!("{other:?}"),
511 });
512 }
513 None => return Ok(Document { children: vec![] }),
514 }
515
516 while let Some(event) = event_iter.next() {
518 match event {
519 Event::StartDocument => {
520 return Err(ConversionError::MismatchedEvents {
521 expected: "content or EndDocument".to_string(),
522 found: "StartDocument".to_string(),
523 });
524 }
525
526 Event::EndDocument => {
527 auto_close_all_headings(&mut stack)?;
530
531 if stack.len() != 1 {
533 return Err(ConversionError::UnclosedContainers(stack.len() - 1));
534 }
535 let doc_node = stack.pop().unwrap();
536 if let StackNode::Document(doc) = doc_node {
537 if event_iter.peek().is_some() {
539 return Err(ConversionError::ExtraEvents);
540 }
541 return Ok(doc);
542 } else {
543 return Err(ConversionError::MismatchedEvents {
544 expected: "Document".to_string(),
545 found: doc_node.type_name().to_string(),
546 });
547 }
548 }
549
550 Event::StartHeading(level) => {
551 auto_close_headings_at_or_deeper(&mut stack, *level)?;
554
555 let node = StackNode::Heading {
557 level: *level,
558 content: vec![],
559 children: vec![],
560 };
561 stack.push(node);
562 }
563
564 Event::EndHeading(level) => {
565 finalize_container(&mut stack, "EndHeading", "heading", |node| match node {
568 StackNode::Heading {
569 level: node_level, ..
570 } if node_level == *level => Ok(node),
571 StackNode::Heading {
572 level: node_level, ..
573 } => Err(ConversionError::MismatchedEvents {
574 expected: format!("EndHeading({node_level})"),
575 found: format!("EndHeading({level})"),
576 }),
577 other => Err(ConversionError::MismatchedEvents {
578 expected: "Heading".to_string(),
579 found: other.type_name().to_string(),
580 }),
581 })?;
582 }
583
584 Event::StartContent => {
585 }
588
589 Event::EndContent => {
590 }
592
593 Event::StartParagraph => {
594 stack.push(StackNode::Paragraph { content: vec![] });
595 }
596
597 Event::EndParagraph => {
598 finalize_container(&mut stack, "EndParagraph", "paragraph", |node| match node {
599 StackNode::Paragraph { .. } => Ok(node),
600 other => Err(ConversionError::MismatchedEvents {
601 expected: "Paragraph".to_string(),
602 found: other.type_name().to_string(),
603 }),
604 })?;
605 }
606
607 Event::StartList { ordered, style } => {
608 stack.push(StackNode::List {
609 items: vec![],
610 ordered: *ordered,
611 style: *style,
612 });
613 }
614
615 Event::EndList => {
616 finalize_container(&mut stack, "EndList", "list", |node| match node {
617 StackNode::List { .. } => Ok(node),
618 other => Err(ConversionError::MismatchedEvents {
619 expected: "List".to_string(),
620 found: other.type_name().to_string(),
621 }),
622 })?;
623 }
624
625 Event::StartListItem => {
626 stack.push(StackNode::ListItem {
627 content: vec![],
628 children: vec![],
629 });
630 }
631
632 Event::EndListItem => {
633 finalize_container(&mut stack, "EndListItem", "list item", |node| match node {
634 StackNode::ListItem { .. } => Ok(node),
635 other => Err(ConversionError::MismatchedEvents {
636 expected: "ListItem".to_string(),
637 found: other.type_name().to_string(),
638 }),
639 })?;
640 }
641
642 Event::StartDefinition => {
643 stack.push(StackNode::Definition {
644 term: vec![],
645 description: vec![],
646 in_term: false,
647 });
648 }
649
650 Event::EndDefinition => {
651 finalize_container(
652 &mut stack,
653 "EndDefinition",
654 "definition",
655 |node| match node {
656 StackNode::Definition { .. } => Ok(node),
657 other => Err(ConversionError::MismatchedEvents {
658 expected: "Definition".to_string(),
659 found: other.type_name().to_string(),
660 }),
661 },
662 )?;
663 }
664
665 Event::StartDefinitionTerm => {
666 if let Some(StackNode::Definition { in_term, .. }) = stack.last_mut() {
667 *in_term = true;
668 } else {
669 return Err(ConversionError::MismatchedEvents {
670 expected: "Definition on stack".to_string(),
671 found: "StartDefinitionTerm".to_string(),
672 });
673 }
674 }
675
676 Event::EndDefinitionTerm => {
677 if let Some(StackNode::Definition { in_term, .. }) = stack.last_mut() {
678 *in_term = false;
679 } else {
680 return Err(ConversionError::MismatchedEvents {
681 expected: "Definition on stack".to_string(),
682 found: "EndDefinitionTerm".to_string(),
683 });
684 }
685 }
686
687 Event::StartDefinitionDescription => {
688 }
690
691 Event::EndDefinitionDescription => {
692 }
694
695 Event::StartVerbatim(language) => {
696 stack.push(StackNode::Verbatim {
697 language: language.clone(),
698 content: String::new(),
699 });
700 }
701
702 Event::EndVerbatim => {
703 finalize_container(&mut stack, "EndVerbatim", "verbatim", |node| match node {
704 StackNode::Verbatim { .. } => Ok(node),
705 other => Err(ConversionError::MismatchedEvents {
706 expected: "Verbatim".to_string(),
707 found: other.type_name().to_string(),
708 }),
709 })?;
710 }
711
712 Event::StartAnnotation { label, parameters } => {
713 stack.push(StackNode::Annotation {
714 label: label.clone(),
715 parameters: parameters.clone(),
716 content: vec![],
717 });
718 }
719
720 Event::EndAnnotation { label } => {
721 finalize_container(
722 &mut stack,
723 "EndAnnotation",
724 "annotation",
725 |node| match node {
726 StackNode::Annotation {
727 label: ref node_label,
728 ..
729 } if node_label == label || label.is_empty() => Ok(node),
730 StackNode::Annotation {
731 label: ref node_label,
732 ..
733 } => Err(ConversionError::MismatchedEvents {
734 expected: format!("EndAnnotation({node_label})"),
735 found: format!("EndAnnotation({label})"),
736 }),
737 other => Err(ConversionError::MismatchedEvents {
738 expected: "Annotation".to_string(),
739 found: other.type_name().to_string(),
740 }),
741 },
742 )?;
743 }
744
745 Event::StartTable => {
746 stack.push(StackNode::Table {
747 rows: vec![],
748 header: vec![],
749 caption: None,
750 });
751 }
752
753 Event::EndTable => {
754 finalize_container(&mut stack, "EndTable", "table", |node| match node {
755 StackNode::Table { .. } => Ok(node),
756 other => Err(ConversionError::MismatchedEvents {
757 expected: "Table".to_string(),
758 found: other.type_name().to_string(),
759 }),
760 })?;
761 }
762
763 Event::StartTableRow { header } => {
764 stack.push(StackNode::TableRow {
765 cells: vec![],
766 header: *header,
767 });
768 }
769
770 Event::EndTableRow => {
771 let node = stack.pop().ok_or_else(|| {
774 ConversionError::UnexpectedEnd("EndTableRow with empty stack".to_string())
775 })?;
776
777 match node {
778 StackNode::TableRow { cells, header } => {
779 let row = TableRow { cells };
780 let parent = stack.last_mut().ok_or_else(|| {
781 ConversionError::UnexpectedEnd("No parent for table row".to_string())
782 })?;
783
784 match parent {
785 StackNode::Table {
786 rows,
787 header: table_header,
788 ..
789 } => {
790 if header {
791 table_header.push(row);
792 } else {
793 rows.push(row);
794 }
795 Ok(())
796 }
797 _ => Err(ConversionError::MismatchedEvents {
798 expected: "Table".to_string(),
799 found: parent.type_name().to_string(),
800 }),
801 }?;
802 }
803 other => {
804 return Err(ConversionError::MismatchedEvents {
805 expected: "TableRow".to_string(),
806 found: other.type_name().to_string(),
807 })
808 }
809 }
810 }
811
812 Event::StartTableCell { header, align } => {
813 stack.push(StackNode::TableCell {
814 content: vec![],
815 header: *header,
816 align: *align,
817 });
818 }
819
820 Event::EndTableCell => {
821 let node = stack.pop().ok_or_else(|| {
823 ConversionError::UnexpectedEnd("EndTableCell with empty stack".to_string())
824 })?;
825
826 match node {
827 StackNode::TableCell {
828 content,
829 header,
830 align,
831 } => {
832 let cell = TableCell {
833 content,
834 header,
835 align,
836 };
837 let parent = stack.last_mut().ok_or_else(|| {
838 ConversionError::UnexpectedEnd("No parent for table cell".to_string())
839 })?;
840
841 match parent {
842 StackNode::TableRow { cells, .. } => {
843 cells.push(cell);
844 Ok(())
845 }
846 _ => Err(ConversionError::MismatchedEvents {
847 expected: "TableRow".to_string(),
848 found: parent.type_name().to_string(),
849 }),
850 }?;
851 }
852 other => {
853 return Err(ConversionError::MismatchedEvents {
854 expected: "TableCell".to_string(),
855 found: other.type_name().to_string(),
856 })
857 }
858 }
859 }
860
861 Event::Image(image) => {
862 let parent = stack.last_mut().ok_or_else(|| {
863 ConversionError::UnexpectedEnd("Image event with empty stack".to_string())
864 })?;
865 parent.add_child(DocNode::Image(image.clone()))?;
866 }
867
868 Event::Video(video) => {
869 let parent = stack.last_mut().ok_or_else(|| {
870 ConversionError::UnexpectedEnd("Video event with empty stack".to_string())
871 })?;
872 parent.add_child(DocNode::Video(video.clone()))?;
873 }
874
875 Event::Audio(audio) => {
876 let parent = stack.last_mut().ok_or_else(|| {
877 ConversionError::UnexpectedEnd("Audio event with empty stack".to_string())
878 })?;
879 parent.add_child(DocNode::Audio(audio.clone()))?;
880 }
881
882 Event::Inline(inline) => {
883 let parent = stack.last_mut().ok_or_else(|| {
884 ConversionError::UnexpectedInline("Inline content with no parent".to_string())
885 })?;
886 parent.add_inline(inline.clone())?;
887 }
888 }
889 }
890
891 Err(ConversionError::UnclosedContainers(stack.len()))
893}
894
895#[cfg(test)]
896mod tests {
897 use super::*;
898
899 #[test]
900 fn test_empty_document() {
901 let events = vec![Event::StartDocument, Event::EndDocument];
902
903 let doc = events_to_tree(&events).unwrap();
904 assert_eq!(doc.children.len(), 0);
905 }
906
907 #[test]
908 fn test_simple_paragraph() {
909 let events = vec![
910 Event::StartDocument,
911 Event::StartParagraph,
912 Event::Inline(InlineContent::Text("Hello world".to_string())),
913 Event::EndParagraph,
914 Event::EndDocument,
915 ];
916
917 let doc = events_to_tree(&events).unwrap();
918 assert_eq!(doc.children.len(), 1);
919
920 match &doc.children[0] {
921 DocNode::Paragraph(para) => {
922 assert_eq!(para.content.len(), 1);
923 assert!(matches!(¶.content[0], InlineContent::Text(t) if t == "Hello world"));
924 }
925 _ => panic!("Expected Paragraph"),
926 }
927 }
928
929 #[test]
930 fn test_heading_with_content() {
931 let events = vec![
932 Event::StartDocument,
933 Event::StartHeading(1),
934 Event::Inline(InlineContent::Text("Title".to_string())),
935 Event::EndHeading(1),
936 Event::EndDocument,
937 ];
938
939 let doc = events_to_tree(&events).unwrap();
940 assert_eq!(doc.children.len(), 1);
941
942 match &doc.children[0] {
943 DocNode::Heading(heading) => {
944 assert_eq!(heading.level, 1);
945 assert_eq!(heading.content.len(), 1);
946 assert!(heading.children.is_empty());
947 }
948 _ => panic!("Expected Heading"),
949 }
950 }
951
952 #[test]
953 fn test_nested_heading_with_paragraph() {
954 let events = vec![
955 Event::StartDocument,
956 Event::StartHeading(1),
957 Event::Inline(InlineContent::Text("Title".to_string())),
958 Event::StartParagraph,
959 Event::Inline(InlineContent::Text("Content".to_string())),
960 Event::EndParagraph,
961 Event::EndHeading(1),
962 Event::EndDocument,
963 ];
964
965 let doc = events_to_tree(&events).unwrap();
966 assert_eq!(doc.children.len(), 1);
967
968 match &doc.children[0] {
969 DocNode::Heading(heading) => {
970 assert_eq!(heading.level, 1);
971 assert_eq!(heading.children.len(), 1);
972 assert!(matches!(&heading.children[0], DocNode::Paragraph(_)));
973 }
974 _ => panic!("Expected Heading"),
975 }
976 }
977
978 #[test]
979 fn test_list_with_items() {
980 let events = vec![
981 Event::StartDocument,
982 Event::StartList {
983 ordered: false,
984 style: ListStyle::Bullet,
985 },
986 Event::StartListItem,
987 Event::Inline(InlineContent::Text("Item 1".to_string())),
988 Event::EndListItem,
989 Event::StartListItem,
990 Event::Inline(InlineContent::Text("Item 2".to_string())),
991 Event::EndListItem,
992 Event::EndList,
993 Event::EndDocument,
994 ];
995
996 let doc = events_to_tree(&events).unwrap();
997 assert_eq!(doc.children.len(), 1);
998
999 match &doc.children[0] {
1000 DocNode::List(list) => {
1001 assert_eq!(list.items.len(), 2);
1002 }
1003 _ => panic!("Expected List"),
1004 }
1005 }
1006
1007 #[test]
1008 fn test_definition() {
1009 let events = vec![
1010 Event::StartDocument,
1011 Event::StartDefinition,
1012 Event::StartDefinitionTerm,
1013 Event::Inline(InlineContent::Text("Term".to_string())),
1014 Event::EndDefinitionTerm,
1015 Event::StartDefinitionDescription,
1016 Event::StartParagraph,
1017 Event::Inline(InlineContent::Text("Description".to_string())),
1018 Event::EndParagraph,
1019 Event::EndDefinitionDescription,
1020 Event::EndDefinition,
1021 Event::EndDocument,
1022 ];
1023
1024 let doc = events_to_tree(&events).unwrap();
1025 assert_eq!(doc.children.len(), 1);
1026
1027 match &doc.children[0] {
1028 DocNode::Definition(def) => {
1029 assert_eq!(def.term.len(), 1);
1030 assert_eq!(def.description.len(), 1);
1031 }
1032 _ => panic!("Expected Definition"),
1033 }
1034 }
1035
1036 #[test]
1037 fn test_verbatim() {
1038 let events = vec![
1039 Event::StartDocument,
1040 Event::StartVerbatim(Some("rust".to_string())),
1041 Event::Inline(InlineContent::Text("fn main() {}".to_string())),
1042 Event::EndVerbatim,
1043 Event::EndDocument,
1044 ];
1045
1046 let doc = events_to_tree(&events).unwrap();
1047 assert_eq!(doc.children.len(), 1);
1048
1049 match &doc.children[0] {
1050 DocNode::Verbatim(verb) => {
1051 assert_eq!(verb.language, Some("rust".to_string()));
1052 assert_eq!(verb.content, "fn main() {}");
1053 }
1054 _ => panic!("Expected Verbatim"),
1055 }
1056 }
1057
1058 #[test]
1059 fn test_annotation() {
1060 let events = vec![
1061 Event::StartDocument,
1062 Event::StartAnnotation {
1063 label: "note".to_string(),
1064 parameters: vec![("type".to_string(), "warning".to_string())],
1065 },
1066 Event::StartParagraph,
1067 Event::Inline(InlineContent::Text("Warning text".to_string())),
1068 Event::EndParagraph,
1069 Event::EndAnnotation {
1070 label: "note".to_string(),
1071 },
1072 Event::EndDocument,
1073 ];
1074
1075 let doc = events_to_tree(&events).unwrap();
1076 assert_eq!(doc.children.len(), 1);
1077
1078 match &doc.children[0] {
1079 DocNode::Annotation(anno) => {
1080 assert_eq!(anno.label, "note");
1081 assert_eq!(anno.parameters.len(), 1);
1082 assert_eq!(anno.content.len(), 1);
1083 }
1084 _ => panic!("Expected Annotation"),
1085 }
1086 }
1087
1088 #[test]
1089 fn test_complex_nested_document() {
1090 let events = vec![
1091 Event::StartDocument,
1092 Event::StartHeading(1),
1093 Event::Inline(InlineContent::Text("Chapter 1".to_string())),
1094 Event::StartHeading(2),
1095 Event::Inline(InlineContent::Text("Section 1.1".to_string())),
1096 Event::StartParagraph,
1097 Event::Inline(InlineContent::Text("Some text".to_string())),
1098 Event::EndParagraph,
1099 Event::StartList {
1100 ordered: false,
1101 style: ListStyle::Bullet,
1102 },
1103 Event::StartListItem,
1104 Event::Inline(InlineContent::Text("Item".to_string())),
1105 Event::EndListItem,
1106 Event::EndList,
1107 Event::EndHeading(2),
1108 Event::EndHeading(1),
1109 Event::EndDocument,
1110 ];
1111
1112 let doc = events_to_tree(&events).unwrap();
1113 assert_eq!(doc.children.len(), 1);
1114
1115 match &doc.children[0] {
1116 DocNode::Heading(h1) => {
1117 assert_eq!(h1.level, 1);
1118 assert_eq!(h1.children.len(), 1);
1119
1120 match &h1.children[0] {
1121 DocNode::Heading(h2) => {
1122 assert_eq!(h2.level, 2);
1123 assert_eq!(h2.children.len(), 2); }
1125 _ => panic!("Expected nested Heading"),
1126 }
1127 }
1128 _ => panic!("Expected top Heading"),
1129 }
1130 }
1131
1132 #[test]
1133 fn test_error_mismatched_end() {
1134 let events = vec![
1135 Event::StartDocument,
1136 Event::StartParagraph,
1137 Event::EndHeading(1), ];
1139
1140 let result = events_to_tree(&events);
1141 assert!(matches!(
1142 result,
1143 Err(ConversionError::MismatchedEvents { .. })
1144 ));
1145 }
1146
1147 #[test]
1148 fn test_error_unclosed_container() {
1149 let events = vec![
1150 Event::StartDocument,
1151 Event::StartParagraph,
1152 Event::EndDocument, ];
1154
1155 let result = events_to_tree(&events);
1156 assert!(matches!(
1157 result,
1158 Err(ConversionError::UnclosedContainers(_))
1159 ));
1160 }
1161
1162 #[test]
1163 fn test_error_extra_events() {
1164 let events = vec![
1165 Event::StartDocument,
1166 Event::EndDocument,
1167 Event::StartParagraph, ];
1169
1170 let result = events_to_tree(&events);
1171 assert!(matches!(result, Err(ConversionError::ExtraEvents)));
1172 }
1173
1174 #[test]
1175 fn test_error_mismatched_heading_level() {
1176 let events = vec![
1177 Event::StartDocument,
1178 Event::StartHeading(1),
1179 Event::EndHeading(2), Event::EndDocument,
1181 ];
1182
1183 let result = events_to_tree(&events);
1184 assert!(matches!(
1185 result,
1186 Err(ConversionError::MismatchedEvents { .. })
1187 ));
1188 }
1189
1190 #[test]
1191 fn test_round_trip() {
1192 use crate::ir::to_events::tree_to_events;
1193
1194 let original_doc = Document {
1195 children: vec![DocNode::Heading(Heading {
1196 level: 1,
1197 content: vec![InlineContent::Text("Title".to_string())],
1198 children: vec![DocNode::Paragraph(Paragraph {
1199 content: vec![InlineContent::Text("Content".to_string())],
1200 })],
1201 })],
1202 };
1203
1204 let events = tree_to_events(&DocNode::Document(original_doc.clone()));
1206
1207 let reconstructed = events_to_tree(&events).unwrap();
1209
1210 assert_eq!(original_doc, reconstructed);
1212 }
1213
1214 #[test]
1215 fn test_round_trip_complex() {
1216 use crate::ir::to_events::tree_to_events;
1217
1218 let original_doc = Document {
1219 children: vec![DocNode::Heading(Heading {
1220 level: 1,
1221 content: vec![
1222 InlineContent::Text("Title ".to_string()),
1223 InlineContent::Bold(vec![InlineContent::Text("bold".to_string())]),
1224 ],
1225 children: vec![
1226 DocNode::List(List {
1227 items: vec![
1228 ListItem {
1229 content: vec![InlineContent::Text("Item 1".to_string())],
1230 children: vec![],
1231 },
1232 ListItem {
1233 content: vec![InlineContent::Text("Item 2".to_string())],
1234 children: vec![DocNode::Paragraph(Paragraph {
1235 content: vec![InlineContent::Text("Nested".to_string())],
1236 })],
1237 },
1238 ],
1239 ordered: false,
1240 style: ListStyle::Bullet,
1241 }),
1242 DocNode::Definition(Definition {
1243 term: vec![InlineContent::Text("Term".to_string())],
1244 description: vec![DocNode::Paragraph(Paragraph {
1245 content: vec![InlineContent::Text("Desc".to_string())],
1246 })],
1247 }),
1248 ],
1249 })],
1250 };
1251
1252 let events = tree_to_events(&DocNode::Document(original_doc.clone()));
1253 let reconstructed = events_to_tree(&events).unwrap();
1254
1255 assert_eq!(original_doc, reconstructed);
1256 }
1257}