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    },
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    /// Convert to a DocNode (used when popping from stack)
144    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                        // Convert back to Annotation
168                        // Format: " key=val key2=val2\nBody"
169
170                        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                // TableRow is not a DocNode, it's part of Table
223                // This should not happen if logic is correct (TableRow is consumed by Table)
224                panic!("TableRow cannot be converted directly to DocNode")
225            }
226            StackNode::TableCell { .. } => {
227                // TableCell is not a DocNode
228                panic!("TableCell cannot be converted directly to DocNode")
229            }
230        }
231    }
232
233    /// Get the node type name for error messages
234    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    /// Add a child DocNode to this container
251    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    /// Add inline content to this node
306    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
375/// Auto-close any open headings at the same or deeper level
376///
377/// This implements the common pattern for flat document formats (Markdown, HTML, LaTeX)
378/// where headings don't have explicit close markers. When we encounter a new heading,
379/// we need to close any currently open headings at the same or deeper level.
380///
381/// Example:
382/// ```text
383/// # Chapter 1        <- Opens h1
384/// ## Section 1.1     <- Opens h2 (nested in h1)
385/// # Chapter 2        <- Closes h2, closes h1, opens new h1
386/// ```
387fn auto_close_headings_at_or_deeper(
388    stack: &mut Vec<StackNode>,
389    new_level: usize,
390) -> Result<(), ConversionError> {
391    // Find all headings to close (from top of stack backwards)
392    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                // Found a parent heading at lower level, stop
400                break;
401            }
402        } else {
403            // Hit a non-heading container, stop looking
404            break;
405        }
406    }
407
408    // Close headings in reverse order (deepest first)
409    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
422/// Auto-close all open headings at document end
423///
424/// This ensures all headings are properly closed when we reach EndDocument,
425/// which is necessary for flat formats that don't have explicit heading close markers.
426fn auto_close_all_headings(stack: &mut Vec<StackNode>) -> Result<(), ConversionError> {
427    // Count how many headings are open
428    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            // Stop at first non-heading
434            break;
435        }
436    }
437
438    // Close all headings
439    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
457/// Converts a flat event stream back to a nested IR tree.
458///
459/// # Arguments
460///
461/// * `events` - The flat sequence of events to process
462///
463/// # Returns
464///
465/// * `Ok(Document)` - The reconstructed document tree
466/// * `Err(ConversionError)` - If the event stream is malformed
467///
468/// # Example
469///
470/// ```ignore
471/// use lex_babel::ir::events::Event;
472/// use lex_babel::common::flat_to_nested::events_to_tree;
473///
474/// let events = vec![
475///     Event::StartDocument,
476///     Event::StartParagraph,
477///     Event::Inline(InlineContent::Text("Hello".to_string())),
478///     Event::EndParagraph,
479///     Event::EndDocument,
480/// ];
481///
482/// let doc = events_to_tree(&events)?;
483/// assert_eq!(doc.children.len(), 1);
484/// ```
485pub 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    // Expect StartDocument as first event
494    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    // Process events
508    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 any remaining open headings before closing document
519                // This handles flat formats where headings may not have explicit EndHeading events
520                auto_close_all_headings(&mut stack)?;
521
522                // Pop the document from stack
523                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                    // Check for extra events
529                    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 any open headings at same or deeper level
543                // This handles flat formats (Markdown, HTML) where headings don't have explicit close markers
544                auto_close_headings_at_or_deeper(&mut stack, *level)?;
545
546                // Push new heading
547                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                // Explicit EndHeading is optional - used by nested_to_flat for export
557                // Validate that the top of stack is a heading at this level
558                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                // Content markers don't affect tree structure - they're used by serializers
577                // to create visual wrappers for indented content
578            }
579
580            Event::EndContent => {
581                // Content markers don't affect tree structure
582            }
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                // Just a marker, definition is already in description mode after EndDefinitionTerm
679            }
680
681            Event::EndDefinitionDescription => {
682                // Just a marker, no action needed
683            }
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                // TableRow is special: it's not a DocNode, so finalize_container won't work directly
762                // We need to pop it and add it to the Table parent manually
763                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                // TableCell is special:            Event::EndTableCell => {
812                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    // If we reach here, document wasn't properly closed
882    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!(&para.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); // paragraph and list
1108                    }
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), // Wrong end!
1122        ];
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, // Missing EndParagraph
1137        ];
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, // Extra after end!
1152        ];
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), // Wrong level!
1164            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        // Convert to events
1189        let events = tree_to_events(&DocNode::Document(original_doc.clone()));
1190
1191        // Convert back to tree
1192        let reconstructed = events_to_tree(&events).unwrap();
1193
1194        // Should match
1195        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}