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