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