Skip to main content

lex_babel/common/
flat_to_nested.rs

1//! Converts a flat event stream back to a nested IR tree structure.
2//!
3//! # The High-Level Concept
4//!
5//! The core challenge is to reconstruct a tree structure from a linear sequence of events.
6//! The algorithm uses a stack to keep track of the current nesting level. The stack acts as
7//! a memory of "open" containers. When we encounter a `Start` event for a container (like a
8//! heading or list), we push it onto the stack, making it the new "current" container. When
9//! we see its corresponding `End` event, we pop it off, returning to the parent container.
10//!
11//! # Auto-Closing Headings (For Flat Formats)
12//!
13//! This converter includes special logic for headings to support flat document formats
14//! (Markdown, HTML, LaTeX) where headings don't have explicit close markers. When a new
15//! `StartHeading(level)` event is encountered, the converter automatically closes any
16//! currently open headings at the same or deeper level before opening the new heading.
17//!
18//! This means format parsers can simply emit `StartHeading` events without worrying about
19//! emitting matching `EndHeading` events - the generic converter handles the hierarchy.
20//!
21//! Example event stream from Markdown parser:
22//! ```text
23//! StartDocument
24//! StartHeading(1)         <- Opens h1
25//! StartHeading(2)         <- Auto-closes nothing, opens h2 nested in h1
26//! StartHeading(1)         <- Auto-closes h2 and previous h1, opens new h1
27//! EndDocument             <- Auto-closes remaining h1
28//! ```
29//!
30//! # The Algorithm
31//!
32//! 1. **Initialization:**
33//!    - Create the root `Document` node
34//!    - Create an empty stack
35//!    - Push the root onto the stack as the current container
36//!
37//! 2. **Processing `Start` Events:**
38//!    - Create a new empty `DocNode` for that element
39//!    - Add it as a child to the current parent (top of stack)
40//!    - Push it onto the stack as the new current container
41//!
42//! 3. **Processing Content Events (Inline):**
43//!    - Add the content to the current parent (top of stack)
44//!    - Do NOT modify the stack (content is a leaf)
45//!
46//! 4. **Processing `End` Events:**
47//!    - Pop the node off the stack
48//!    - Validate that the popped node matches the End event
49//!
50//! 5. **Completion:**
51//!    - The stack should contain only the root Document node
52//!    - This root contains the complete reconstructed AST
53
54use crate::ir::events::Event;
55use crate::ir::nodes::*;
56
57/// Error type for flat-to-nested conversion
58#[derive(Debug, Clone, PartialEq)]
59pub enum ConversionError {
60    /// Stack was empty when trying to pop
61    UnexpectedEnd(String),
62    /// Mismatched start/end events
63    MismatchedEvents { expected: String, found: String },
64    /// Unexpected inline content in wrong context
65    UnexpectedInline(String),
66    /// Events remaining after document end
67    ExtraEvents,
68    /// Stack not empty at end (unclosed containers)
69    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/// Represents a node being built on the stack
93#[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    /// Convert to a DocNode (used when popping from stack)
147    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                        // Convert back to Annotation
185                        // Format: " key=val key2=val2\nBody"
186
187                        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                // TableRow is not a DocNode, it's part of Table
244                // This should not happen if logic is correct (TableRow is consumed by Table)
245                panic!("TableRow cannot be converted directly to DocNode")
246            }
247            StackNode::TableCell { .. } => {
248                // TableCell is not a DocNode
249                panic!("TableCell cannot be converted directly to DocNode")
250            }
251        }
252    }
253
254    /// Get the node type name for error messages
255    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    /// Add a child DocNode to this container
272    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    /// Add inline content to this node
327    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
396/// Auto-close any open headings at the same or deeper level
397///
398/// This implements the common pattern for flat document formats (Markdown, HTML, LaTeX)
399/// where headings don't have explicit close markers. When we encounter a new heading,
400/// we need to close any currently open headings at the same or deeper level.
401///
402/// Example:
403/// ```text
404/// # Chapter 1        <- Opens h1
405/// ## Section 1.1     <- Opens h2 (nested in h1)
406/// # Chapter 2        <- Closes h2, closes h1, opens new h1
407/// ```
408fn auto_close_headings_at_or_deeper(
409    stack: &mut Vec<StackNode>,
410    new_level: usize,
411) -> Result<(), ConversionError> {
412    // Find all headings to close (from top of stack backwards)
413    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                // Found a parent heading at lower level, stop
421                break;
422            }
423        } else {
424            // Hit a non-heading container, stop looking
425            break;
426        }
427    }
428
429    // Close headings in reverse order (deepest first)
430    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
443/// Auto-close all open headings at document end
444///
445/// This ensures all headings are properly closed when we reach EndDocument,
446/// which is necessary for flat formats that don't have explicit heading close markers.
447fn auto_close_all_headings(stack: &mut Vec<StackNode>) -> Result<(), ConversionError> {
448    // Count how many headings are open
449    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            // Stop at first non-heading
455            break;
456        }
457    }
458
459    // Close all headings
460    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
478/// Converts a flat event stream back to a nested IR tree.
479///
480/// # Arguments
481///
482/// * `events` - The flat sequence of events to process
483///
484/// # Returns
485///
486/// * `Ok(Document)` - The reconstructed document tree
487/// * `Err(ConversionError)` - If the event stream is malformed
488///
489/// # Example
490///
491/// ```ignore
492/// use lex_babel::ir::events::Event;
493/// use lex_babel::common::flat_to_nested::events_to_tree;
494///
495/// let events = vec![
496///     Event::StartDocument,
497///     Event::StartParagraph,
498///     Event::Inline(InlineContent::Text("Hello".to_string())),
499///     Event::EndParagraph,
500///     Event::EndDocument,
501/// ];
502///
503/// let doc = events_to_tree(&events)?;
504/// assert_eq!(doc.children.len(), 1);
505/// ```
506pub 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    // Expect StartDocument as first event
515    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    // Process events
529    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 any remaining open headings before closing document
540                // This handles flat formats where headings may not have explicit EndHeading events
541                auto_close_all_headings(&mut stack)?;
542
543                // Pop the document from stack
544                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                    // Check for extra events
550                    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 any open headings at same or deeper level
564                // This handles flat formats (Markdown, HTML) where headings don't have explicit close markers
565                auto_close_headings_at_or_deeper(&mut stack, *level)?;
566
567                // Push new heading
568                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                // Explicit EndHeading is optional - used by nested_to_flat for export
578                // Validate that the top of stack is a heading at this level
579                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                // Content markers don't affect tree structure - they're used by serializers
598                // to create visual wrappers for indented content
599            }
600
601            Event::EndContent => {
602                // Content markers don't affect tree structure
603            }
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                // Just a marker, definition is already in description mode after EndDefinitionTerm
706            }
707
708            Event::EndDefinitionDescription => {
709                // Just a marker, no action needed
710            }
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                // TableRow is special: it's not a DocNode, so finalize_container won't work directly
790                // We need to pop it and add it to the Table parent manually
791                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                // TableCell is special:            Event::EndTableCell => {
840                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    // If we reach here, document wasn't properly closed
910    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!(&para.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); // paragraph and list
1147                    }
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), // Wrong end!
1161        ];
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, // Missing EndParagraph
1176        ];
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, // Extra after end!
1191        ];
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), // Wrong level!
1203            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        // Convert to events
1228        let events = tree_to_events(&DocNode::Document(original_doc.clone()));
1229
1230        // Convert back to tree
1231        let reconstructed = events_to_tree(&events).unwrap();
1232
1233        // Should match
1234        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}