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