Skip to main content

docspec_core/
stack.rs

1//! Stack tracking for block-level containers in the event stream.
2//!
3//! This module provides [`StackTrackingSink`], a wrapper around any [`EventSink`] that
4//! maintains a stack of open block-level containers. This enables normalization logic
5//! and well-formedness validation.
6
7use alloc::vec::Vec;
8
9use crate::{Error, Event, EventSink, Result};
10
11/// Declarative macro that generates three block-kind lookup functions from a single table.
12///
13/// Expands to:
14/// - `block_kind_for_start(event: &Event) -> Option<BlockKind>`
15/// - `block_kind_for_end(event: &Event) -> Option<BlockKind>`
16/// - `end_event_for(kind: BlockKind) -> Event`
17///
18/// Each function is marked `#[inline]` and `#[must_use]`.
19macro_rules! block_kinds {
20    ( $( $kind:ident => ( $start:ident, $end:ident ) ),+ $(,)? ) => {
21        /// Returns the [`BlockKind`] for a start event, or `None` if the event is not a block start.
22        #[inline]
23        #[must_use]
24        pub fn block_kind_for_start(event: &Event) -> Option<BlockKind> {
25            match event {
26                $( Event::$start { .. } => Some(BlockKind::$kind), )+
27                _ => None,
28            }
29        }
30
31        /// Returns the [`BlockKind`] for an end event, or `None` if the event is not a block end.
32        #[inline]
33        #[must_use]
34        pub fn block_kind_for_end(event: &Event) -> Option<BlockKind> {
35            match event {
36                $( Event::$end => Some(BlockKind::$kind), )+
37                _ => None,
38            }
39        }
40
41        /// Returns the end event for a given [`BlockKind`].
42        #[inline]
43        #[must_use]
44        pub fn end_event_for(kind: BlockKind) -> Event {
45            match kind {
46                $( BlockKind::$kind => Event::$end, )+
47            }
48        }
49    };
50}
51
52/// Identifies the kind of block-level container in a document event stream.
53///
54/// Each variant corresponds to a Start/End event pair. The stack tracker uses this
55/// to maintain the nesting structure as events flow through.
56#[derive(Clone, Copy, PartialEq, Eq, Debug)]
57pub enum BlockKind {
58    /// A block quote container.
59    Blockquote,
60    /// A table caption container.
61    Caption,
62    /// A definition detail (description) container.
63    DefinitionDetail,
64    /// A definition list container.
65    DefinitionList,
66    /// A definition term container.
67    DefinitionTerm,
68    /// The document root container.
69    Document,
70    /// A footnote definition container.
71    Footnote,
72    /// A heading container.
73    Heading,
74    /// A hyperlink container.
75    Link,
76    /// An ordered (numbered) list item container.
77    OrderedListItem,
78    /// A paragraph container.
79    Paragraph,
80    /// A preformatted (code) block container.
81    Preformatted,
82    /// A table container.
83    Table,
84    /// A table data cell container.
85    TableCell,
86    /// A table header cell container.
87    TableHeader,
88    /// A table row container.
89    TableRow,
90    /// An unordered (bulleted) list item container.
91    UnorderedListItem,
92}
93
94/// A wrapper around any [`EventSink`] that tracks the nesting stack of open block-level containers
95/// and performs event-stream normalization.
96///
97/// This sink maintains a stack of [`BlockKind`] values representing currently open containers.
98/// Use the query methods to inspect the current nesting state.
99///
100/// # Normalization Behavior
101///
102/// - **Auto-insert**: If a [`Text`](Event::Text) event arrives outside any content-bearing block,
103///   a [`StartParagraph`](Event::StartParagraph) is automatically inserted before it.
104/// - **Auto-close**: On [`EndDocument`](Event::EndDocument), all remaining open blocks are closed
105///   in reverse order. When an End event targets a block deeper in the stack, intervening blocks
106///   are auto-closed first. Auto-inserted paragraphs are closed before new block-level Start events.
107/// - **Validation**: An End event that does not match any open block on the stack returns
108///   [`Error::InvalidSequence`](crate::Error::InvalidSequence).
109pub struct StackTrackingSink<S: EventSink> {
110    document_finished: bool,
111    sink: S,
112    stack: Vec<BlockKind>,
113}
114
115impl<S: EventSink> StackTrackingSink<S> {
116    /// Handles an `EndDocument` event: drains all remaining open blocks in reverse order,
117    /// emits their close events, sets `document_finished`, then forwards `EndDocument`.
118    fn handle_end_document(&mut self) -> Result<()> {
119        if !self.stack.contains(&BlockKind::Document) {
120            return Err(Error::InvalidSequence {
121                expected: "open Document".to_string(),
122                found: "EndDocument".to_string(),
123                message: "EndDocument received without StartDocument".to_string(),
124            });
125        }
126        while let Some(kind) = self.stack.pop() {
127            if kind != BlockKind::Document {
128                self.sink.handle_event(end_event_for(kind))?;
129            }
130        }
131        self.document_finished = true;
132        self.sink.handle_event(Event::EndDocument)
133    }
134
135    /// Handles any intermediate End event (not `EndDocument`): validates the stack,
136    /// auto-closes intervening blocks, pops the target frame, and forwards the event.
137    fn handle_end_event(&mut self, event: Event) -> Result<()> {
138        let Some(target_kind) = block_kind_for_end(&event) else {
139            return Err(Error::InvalidSequence {
140                expected: "valid End event".to_string(),
141                found: format!("{event:?}"),
142                message: "handle_end_event called with non-End event".to_string(),
143            });
144        };
145        if self.stack.is_empty() {
146            return Err(Error::InvalidSequence {
147                expected: "open block".to_string(),
148                found: format!("{target_kind:?}"),
149                message: "received End event with empty stack".to_string(),
150            });
151        }
152        if self.stack.contains(&target_kind) {
153            while self.stack.last() != Some(&target_kind) {
154                if let Some(popped_kind) = self.stack.pop() {
155                    self.sink.handle_event(end_event_for(popped_kind))?;
156                }
157            }
158            self.stack.pop();
159            return self.sink.handle_event(event);
160        }
161        Err(Error::InvalidSequence {
162            expected: self
163                .stack
164                .last()
165                .map_or("empty".to_string(), |k| format!("{k:?}")),
166            found: format!("{target_kind:?}"),
167            message: format!("End event for {target_kind:?} does not match any open block"),
168        })
169    }
170
171    /// Handles `ThematicBreak` and `Text` events (all non-block, non-End events).
172    ///
173    /// `ThematicBreak`: auto-closes an open `Paragraph` if present.
174    /// `Text`: auto-inserts a `StartParagraph` when no content-bearing block is open.
175    /// All other leaf events are forwarded directly.
176    fn handle_other_event(&mut self, event: Event) -> Result<()> {
177        if matches!(event, Event::ThematicBreak { .. })
178            && self.stack.last() == Some(&BlockKind::Paragraph)
179        {
180            self.stack.pop();
181            self.sink.handle_event(Event::EndParagraph)?;
182        }
183
184        if matches!(event, Event::Text { .. }) && !self.has_open_content() {
185            let para = Event::StartParagraph {
186                alignment: None,
187                id: None,
188            };
189            self.stack.push(BlockKind::Paragraph);
190            self.sink.handle_event(para)?;
191        }
192
193        self.sink.handle_event(event)
194    }
195
196    /// Handles any Start event: validates Document/Link nesting constraints,
197    /// auto-closes an open Paragraph when needed, pushes the new block, and forwards the event.
198    fn handle_start_event(&mut self, kind: BlockKind, event: Event) -> Result<()> {
199        if kind == BlockKind::Document {
200            if self.stack.contains(&BlockKind::Document) {
201                return Err(Error::InvalidSequence {
202                    expected: "single Document".to_string(),
203                    found: "StartDocument".to_string(),
204                    message: "StartDocument received while Document already open".to_string(),
205                });
206            }
207            if self.document_finished {
208                return Err(Error::InvalidSequence {
209                    expected: "end of stream".to_string(),
210                    found: "StartDocument".to_string(),
211                    message: "StartDocument received after document already finished".to_string(),
212                });
213            }
214        }
215        // Links cannot nest per EVENTS.md
216        if kind == BlockKind::Link && self.stack.contains(&BlockKind::Link) {
217            return Err(Error::InvalidSequence {
218                expected: "no nested links".to_string(),
219                found: "StartLink".to_string(),
220                message: "StartLink received while another link is already open".to_string(),
221            });
222        }
223        if kind != BlockKind::Link && self.stack.last() == Some(&BlockKind::Paragraph) {
224            self.stack.pop();
225            self.sink.handle_event(Event::EndParagraph)?;
226        }
227        self.stack.push(kind);
228        self.sink.handle_event(event)
229    }
230
231    /// Returns `true` if the stack contains any content-bearing block.
232    ///
233    /// Content-bearing blocks are: [`BlockKind::Heading`], [`BlockKind::Paragraph`],
234    /// [`BlockKind::Preformatted`], [`BlockKind::Link`], [`BlockKind::DefinitionTerm`].
235    ///
236    /// Note: [`BlockKind::Blockquote`] is NOT content-bearing because block quotes contain
237    /// block elements (paragraphs, headings, etc.), not inline text directly. Text inside
238    /// a block quote without an explicit paragraph triggers auto-paragraph insertion.
239    ///
240    /// Note: [`BlockKind::OrderedListItem`], [`BlockKind::UnorderedListItem`], [`BlockKind::TableCell`], [`BlockKind::TableHeader`],
241    /// and [`BlockKind::DefinitionDetail`] are NOT content-bearing despite being able to
242    /// contain inline content per `EVENTS.md`. This is because downstream writers like
243    /// `BlockNoteWriter` rely on auto-paragraph insertion for these container types.
244    #[inline]
245    pub fn has_open_content(&self) -> bool {
246        self.stack.iter().any(|kind| {
247            matches!(
248                kind,
249                BlockKind::Heading
250                    | BlockKind::Paragraph
251                    | BlockKind::Preformatted
252                    | BlockKind::Link
253                    | BlockKind::DefinitionTerm
254            )
255        })
256    }
257
258    /// Returns `true` if the given block kind is anywhere in the current nesting stack.
259    #[inline]
260    pub fn is_inside(&self, kind: BlockKind) -> bool {
261        self.stack.contains(&kind)
262    }
263
264    /// Creates a new stack-tracking wrapper around the given sink.
265    #[inline]
266    pub fn new(sink: S) -> Self {
267        Self {
268            document_finished: false,
269            sink,
270            stack: Vec::new(),
271        }
272    }
273
274    /// Returns a reference to the inner sink.
275    #[cfg(test)]
276    fn sink(&self) -> &S {
277        &self.sink
278    }
279
280    /// Returns a slice of the current nesting stack.
281    ///
282    /// The first element is the outermost container (typically [`BlockKind::Document`]),
283    /// and the last element is the innermost currently open container.
284    #[inline]
285    pub fn stack(&self) -> &[BlockKind] {
286        &self.stack
287    }
288
289    /// Returns a mutable reference to the stack.
290    #[cfg(test)]
291    fn stack_mut(&mut self) -> &mut Vec<BlockKind> {
292        &mut self.stack
293    }
294}
295
296impl<S: EventSink> EventSink for StackTrackingSink<S> {
297    #[inline]
298    fn finish(self) -> Result<()> {
299        self.sink.finish()
300    }
301
302    #[inline]
303    fn handle_event(&mut self, event: Event) -> Result<()> {
304        // Reject all events after document has finished (except StartDocument gets a
305        // clearer error message from handle_start_event)
306        if self.document_finished && !matches!(event, Event::StartDocument { .. }) {
307            return Err(Error::InvalidSequence {
308                expected: "end of stream".to_string(),
309                found: format!("{event:?}"),
310                message: "event received after document already finished".to_string(),
311            });
312        }
313
314        if let Some(kind) = block_kind_for_start(&event) {
315            return self.handle_start_event(kind, event);
316        }
317
318        if matches!(event, Event::EndDocument) {
319            return self.handle_end_document();
320        }
321
322        if block_kind_for_end(&event).is_some() {
323            return self.handle_end_event(event);
324        }
325
326        self.handle_other_event(event)
327    }
328}
329
330block_kinds! {
331    Blockquote        => (StartBlockQuote,        EndBlockQuote),
332    Caption           => (StartCaption,           EndCaption),
333    DefinitionDetail  => (StartDefinitionDetail,  EndDefinitionDetail),
334    DefinitionList    => (StartDefinitionList,    EndDefinitionList),
335    DefinitionTerm    => (StartDefinitionTerm,    EndDefinitionTerm),
336    Document          => (StartDocument,          EndDocument),
337    Footnote          => (StartFootnote,          EndFootnote),
338    Heading           => (StartHeading,           EndHeading),
339    Link              => (StartLink,              EndLink),
340    OrderedListItem   => (StartOrderedListItem,   EndOrderedListItem),
341    Paragraph         => (StartParagraph,         EndParagraph),
342    Preformatted      => (StartPreformatted,      EndPreformatted),
343    Table             => (StartTable,             EndTable),
344    TableCell         => (StartTableCell,         EndTableCell),
345    TableHeader       => (StartTableHeader,       EndTableHeader),
346    TableRow          => (StartTableRow,          EndTableRow),
347    UnorderedListItem => (StartUnorderedListItem, EndUnorderedListItem),
348}
349
350#[cfg(test)]
351mod tests {
352    use alloc::vec::Vec;
353
354    use super::*;
355    use crate::TextStyle;
356
357    struct MockSink {
358        events: Vec<Event>,
359    }
360
361    impl MockSink {
362        fn new() -> Self {
363            Self { events: Vec::new() }
364        }
365    }
366
367    impl EventSink for MockSink {
368        fn finish(self) -> Result<()> {
369            Ok(())
370        }
371
372        fn handle_event(&mut self, event: Event) -> Result<()> {
373            self.events.push(event);
374            Ok(())
375        }
376    }
377
378    fn send(sink: &mut StackTrackingSink<MockSink>, event: Event) {
379        let result = sink.handle_event(event);
380        assert!(result.is_ok());
381    }
382
383    #[test]
384    fn has_open_content_with_blockquote_returns_false() {
385        let mock = MockSink::new();
386        let mut sink = StackTrackingSink::new(mock);
387        sink.stack_mut().push(BlockKind::Document);
388        sink.stack_mut().push(BlockKind::Blockquote);
389        // Blockquote is NOT content-bearing - it contains block elements, not inline text
390        assert!(!sink.has_open_content());
391    }
392
393    #[test]
394    fn has_open_content_with_heading() {
395        let mock = MockSink::new();
396        let mut sink = StackTrackingSink::new(mock);
397        sink.stack_mut().push(BlockKind::Document);
398        sink.stack_mut().push(BlockKind::Heading);
399        assert!(sink.has_open_content());
400    }
401
402    #[test]
403    fn has_open_content_with_paragraph() {
404        let mock = MockSink::new();
405        let mut sink = StackTrackingSink::new(mock);
406        sink.stack_mut().push(BlockKind::Document);
407        sink.stack_mut().push(BlockKind::Paragraph);
408        assert!(sink.has_open_content());
409    }
410
411    #[test]
412    fn has_open_content_with_preformatted() {
413        let mock = MockSink::new();
414        let mut sink = StackTrackingSink::new(mock);
415        sink.stack_mut().push(BlockKind::Document);
416        sink.stack_mut().push(BlockKind::Preformatted);
417        assert!(sink.has_open_content());
418    }
419
420    #[test]
421    fn has_open_content_without_content_blocks() {
422        let mock = MockSink::new();
423        let mut sink = StackTrackingSink::new(mock);
424        sink.stack_mut().push(BlockKind::Document);
425        sink.stack_mut().push(BlockKind::Table);
426        sink.stack_mut().push(BlockKind::TableRow);
427        assert!(!sink.has_open_content());
428    }
429
430    #[test]
431    fn is_inside_finds_nested_kind() {
432        let mock = MockSink::new();
433        let mut sink = StackTrackingSink::new(mock);
434        sink.stack_mut().push(BlockKind::Document);
435        sink.stack_mut().push(BlockKind::Blockquote);
436        sink.stack_mut().push(BlockKind::Paragraph);
437        assert!(sink.is_inside(BlockKind::Blockquote));
438    }
439
440    #[test]
441    fn is_inside_returns_false_for_missing_kind() {
442        let mock = MockSink::new();
443        let mut sink = StackTrackingSink::new(mock);
444        sink.stack_mut().push(BlockKind::Document);
445        sink.stack_mut().push(BlockKind::Paragraph);
446        assert!(!sink.is_inside(BlockKind::Blockquote));
447    }
448
449    #[test]
450    fn stack_returns_current_stack() {
451        let mock = MockSink::new();
452        let mut sink = StackTrackingSink::new(mock);
453        sink.stack_mut().push(BlockKind::Document);
454        sink.stack_mut().push(BlockKind::Paragraph);
455        let stack = sink.stack();
456        assert_eq!(stack.len(), 2);
457        assert_eq!(stack.first(), Some(&BlockKind::Document));
458        assert_eq!(stack.get(1), Some(&BlockKind::Paragraph));
459    }
460
461    #[test]
462    fn passthrough_forwards_all_events() {
463        let mock = MockSink::new();
464        let mut sink = StackTrackingSink::new(mock);
465
466        send(
467            &mut sink,
468            Event::StartDocument {
469                id: None,
470                language: None,
471                metadata: None,
472            },
473        );
474        send(
475            &mut sink,
476            Event::StartParagraph {
477                alignment: None,
478                id: None,
479            },
480        );
481        send(
482            &mut sink,
483            Event::Text {
484                content: "hello".to_string(),
485                style: TextStyle::default(),
486            },
487        );
488        send(&mut sink, Event::EndParagraph);
489        send(&mut sink, Event::EndDocument);
490
491        assert_eq!(sink.sink().events.len(), 5);
492        assert!(matches!(
493            sink.sink().events.first(),
494            Some(Event::StartDocument { .. })
495        ));
496        assert!(matches!(
497            sink.sink().events.get(1),
498            Some(Event::StartParagraph { .. })
499        ));
500        assert!(matches!(
501            sink.sink().events.get(2),
502            Some(Event::Text { .. })
503        ));
504        assert!(matches!(
505            sink.sink().events.get(3),
506            Some(Event::EndParagraph)
507        ));
508        assert!(matches!(
509            sink.sink().events.get(4),
510            Some(Event::EndDocument)
511        ));
512        assert!(sink.stack().is_empty());
513    }
514
515    #[test]
516    fn orphan_text_gets_paragraph() {
517        let mock = MockSink::new();
518        let mut sink = StackTrackingSink::new(mock);
519
520        send(
521            &mut sink,
522            Event::StartDocument {
523                id: None,
524                language: None,
525                metadata: None,
526            },
527        );
528        send(
529            &mut sink,
530            Event::Text {
531                content: "hello".to_string(),
532                style: TextStyle::default(),
533            },
534        );
535        send(&mut sink, Event::EndDocument);
536
537        assert_eq!(sink.sink().events.len(), 5);
538        assert!(matches!(
539            sink.sink().events.first(),
540            Some(Event::StartDocument { .. })
541        ));
542        assert_eq!(
543            sink.sink().events.get(1),
544            Some(&Event::StartParagraph {
545                alignment: None,
546                id: None
547            })
548        );
549        assert!(matches!(
550            sink.sink().events.get(2),
551            Some(Event::Text { .. })
552        ));
553        assert_eq!(sink.sink().events.get(3), Some(&Event::EndParagraph));
554        assert!(matches!(
555            sink.sink().events.get(4),
556            Some(Event::EndDocument)
557        ));
558    }
559
560    #[test]
561    fn orphan_text_inside_table_cell_gets_paragraph() {
562        let mock = MockSink::new();
563        let mut sink = StackTrackingSink::new(mock);
564
565        send(
566            &mut sink,
567            Event::StartDocument {
568                id: None,
569                language: None,
570                metadata: None,
571            },
572        );
573        send(&mut sink, Event::StartTable { id: None });
574        send(&mut sink, Event::StartTableRow { id: None });
575        send(
576            &mut sink,
577            Event::StartTableCell {
578                colspan: None,
579                id: None,
580                rowspan: None,
581            },
582        );
583        send(
584            &mut sink,
585            Event::Text {
586                content: "cell".to_string(),
587                style: TextStyle::default(),
588            },
589        );
590        send(&mut sink, Event::EndTableCell);
591        send(&mut sink, Event::EndTableRow);
592        send(&mut sink, Event::EndTable);
593        send(&mut sink, Event::EndDocument);
594
595        assert_eq!(sink.sink().events.len(), 11);
596        assert_eq!(
597            sink.sink().events.get(4),
598            Some(&Event::StartParagraph {
599                alignment: None,
600                id: None
601            })
602        );
603        assert_eq!(sink.sink().events.get(6), Some(&Event::EndParagraph));
604    }
605
606    #[test]
607    fn orphan_text_inside_blockquote_gets_paragraph() {
608        let mock = MockSink::new();
609        let mut sink = StackTrackingSink::new(mock);
610
611        send(
612            &mut sink,
613            Event::StartDocument {
614                id: None,
615                language: None,
616                metadata: None,
617            },
618        );
619        send(&mut sink, Event::StartBlockQuote { id: None });
620        send(
621            &mut sink,
622            Event::Text {
623                content: "quoted".to_string(),
624                style: TextStyle::default(),
625            },
626        );
627        send(&mut sink, Event::EndBlockQuote);
628        send(&mut sink, Event::EndDocument);
629
630        // Should auto-insert paragraph around orphan text inside blockquote
631        assert_eq!(sink.sink().events.len(), 7);
632        assert!(matches!(
633            sink.sink().events.first(),
634            Some(Event::StartDocument { .. })
635        ));
636        assert!(matches!(
637            sink.sink().events.get(1),
638            Some(Event::StartBlockQuote { .. })
639        ));
640        assert_eq!(
641            sink.sink().events.get(2),
642            Some(&Event::StartParagraph {
643                alignment: None,
644                id: None
645            })
646        );
647        assert!(matches!(
648            sink.sink().events.get(3),
649            Some(Event::Text { .. })
650        ));
651        assert_eq!(sink.sink().events.get(4), Some(&Event::EndParagraph));
652        assert!(matches!(
653            sink.sink().events.get(5),
654            Some(Event::EndBlockQuote)
655        ));
656        assert!(matches!(
657            sink.sink().events.get(6),
658            Some(Event::EndDocument)
659        ));
660    }
661
662    #[test]
663    fn text_inside_paragraph_no_extra_insert() {
664        let mock = MockSink::new();
665        let mut sink = StackTrackingSink::new(mock);
666
667        send(
668            &mut sink,
669            Event::StartDocument {
670                id: None,
671                language: None,
672                metadata: None,
673            },
674        );
675        send(
676            &mut sink,
677            Event::StartParagraph {
678                alignment: None,
679                id: None,
680            },
681        );
682        send(
683            &mut sink,
684            Event::Text {
685                content: "hello".to_string(),
686                style: TextStyle::default(),
687            },
688        );
689        send(
690            &mut sink,
691            Event::Text {
692                content: "world".to_string(),
693                style: TextStyle::default(),
694            },
695        );
696        send(&mut sink, Event::EndParagraph);
697        send(&mut sink, Event::EndDocument);
698
699        assert_eq!(sink.sink().events.len(), 6);
700    }
701
702    #[test]
703    fn auto_close_paragraph_on_end_table() {
704        let mock = MockSink::new();
705        let mut sink = StackTrackingSink::new(mock);
706
707        send(
708            &mut sink,
709            Event::StartDocument {
710                id: None,
711                language: None,
712                metadata: None,
713            },
714        );
715        send(&mut sink, Event::StartTable { id: None });
716        send(&mut sink, Event::StartTableRow { id: None });
717        send(
718            &mut sink,
719            Event::StartTableCell {
720                colspan: None,
721                id: None,
722                rowspan: None,
723            },
724        );
725        send(
726            &mut sink,
727            Event::StartParagraph {
728                alignment: None,
729                id: None,
730            },
731        );
732        send(
733            &mut sink,
734            Event::Text {
735                content: "cell".to_string(),
736                style: TextStyle::default(),
737            },
738        );
739        send(&mut sink, Event::EndTable);
740
741        assert_eq!(sink.sink().events.len(), 10);
742        assert_eq!(sink.sink().events.get(6), Some(&Event::EndParagraph));
743        assert_eq!(sink.sink().events.get(7), Some(&Event::EndTableCell));
744        assert_eq!(sink.sink().events.get(8), Some(&Event::EndTableRow));
745        assert_eq!(sink.sink().events.get(9), Some(&Event::EndTable));
746    }
747
748    #[test]
749    fn auto_close_on_end_blockquote() {
750        let mock = MockSink::new();
751        let mut sink = StackTrackingSink::new(mock);
752
753        send(
754            &mut sink,
755            Event::StartDocument {
756                id: None,
757                language: None,
758                metadata: None,
759            },
760        );
761        send(&mut sink, Event::StartBlockQuote { id: None });
762        send(
763            &mut sink,
764            Event::StartParagraph {
765                alignment: None,
766                id: None,
767            },
768        );
769        send(
770            &mut sink,
771            Event::Text {
772                content: "quote".to_string(),
773                style: TextStyle::default(),
774            },
775        );
776        send(&mut sink, Event::EndBlockQuote);
777
778        assert_eq!(sink.sink().events.len(), 6);
779        assert_eq!(sink.sink().events.get(4), Some(&Event::EndParagraph));
780        assert_eq!(sink.sink().events.get(5), Some(&Event::EndBlockQuote));
781    }
782
783    #[test]
784    fn start_block_closes_open_paragraph() {
785        let mock = MockSink::new();
786        let mut sink = StackTrackingSink::new(mock);
787
788        send(
789            &mut sink,
790            Event::StartDocument {
791                id: None,
792                language: None,
793                metadata: None,
794            },
795        );
796        send(
797            &mut sink,
798            Event::StartParagraph {
799                alignment: None,
800                id: None,
801            },
802        );
803        send(&mut sink, Event::StartHeading { id: None, level: 1 });
804
805        assert_eq!(sink.sink().events.get(2), Some(&Event::EndParagraph));
806        assert!(matches!(
807            sink.sink().events.get(3),
808            Some(Event::StartHeading { .. })
809        ));
810    }
811
812    #[test]
813    fn start_document_while_document_open_returns_error() {
814        let mock = MockSink::new();
815        let mut sink = StackTrackingSink::new(mock);
816
817        send(
818            &mut sink,
819            Event::StartDocument {
820                id: None,
821                language: None,
822                metadata: None,
823            },
824        );
825        let result = sink.handle_event(Event::StartDocument {
826            id: None,
827            language: None,
828            metadata: None,
829        });
830
831        assert!(matches!(result, Err(Error::InvalidSequence { .. })));
832    }
833
834    #[test]
835    fn thematic_break_closes_open_paragraph() {
836        let mock = MockSink::new();
837        let mut sink = StackTrackingSink::new(mock);
838
839        send(
840            &mut sink,
841            Event::StartDocument {
842                id: None,
843                language: None,
844                metadata: None,
845            },
846        );
847        send(
848            &mut sink,
849            Event::StartParagraph {
850                alignment: None,
851                id: None,
852            },
853        );
854        send(&mut sink, Event::ThematicBreak { id: None });
855
856        assert_eq!(sink.sink().events.get(2), Some(&Event::EndParagraph));
857        assert!(matches!(
858            sink.sink().events.get(3),
859            Some(Event::ThematicBreak { .. })
860        ));
861    }
862
863    #[test]
864    fn end_document_closes_all() {
865        let mock = MockSink::new();
866        let mut sink = StackTrackingSink::new(mock);
867
868        send(
869            &mut sink,
870            Event::StartDocument {
871                id: None,
872                language: None,
873                metadata: None,
874            },
875        );
876        send(&mut sink, Event::StartBlockQuote { id: None });
877        send(
878            &mut sink,
879            Event::StartParagraph {
880                alignment: None,
881                id: None,
882            },
883        );
884        send(&mut sink, Event::EndDocument);
885
886        assert_eq!(sink.sink().events.len(), 6);
887        assert_eq!(sink.sink().events.get(3), Some(&Event::EndParagraph));
888        assert_eq!(sink.sink().events.get(4), Some(&Event::EndBlockQuote));
889        assert_eq!(sink.sink().events.get(5), Some(&Event::EndDocument));
890        assert!(sink.stack().is_empty());
891    }
892
893    #[test]
894    fn end_event_for_all_kinds() {
895        assert_eq!(end_event_for(BlockKind::Blockquote), Event::EndBlockQuote);
896        assert_eq!(end_event_for(BlockKind::Caption), Event::EndCaption);
897        assert_eq!(
898            end_event_for(BlockKind::DefinitionDetail),
899            Event::EndDefinitionDetail
900        );
901        assert_eq!(
902            end_event_for(BlockKind::DefinitionList),
903            Event::EndDefinitionList
904        );
905        assert_eq!(
906            end_event_for(BlockKind::DefinitionTerm),
907            Event::EndDefinitionTerm
908        );
909        assert_eq!(end_event_for(BlockKind::Document), Event::EndDocument);
910        assert_eq!(end_event_for(BlockKind::Footnote), Event::EndFootnote);
911        assert_eq!(end_event_for(BlockKind::Heading), Event::EndHeading);
912        assert_eq!(end_event_for(BlockKind::Link), Event::EndLink);
913        assert_eq!(
914            end_event_for(BlockKind::OrderedListItem),
915            Event::EndOrderedListItem
916        );
917        assert_eq!(
918            end_event_for(BlockKind::UnorderedListItem),
919            Event::EndUnorderedListItem
920        );
921        assert_eq!(end_event_for(BlockKind::Paragraph), Event::EndParagraph);
922        assert_eq!(
923            end_event_for(BlockKind::Preformatted),
924            Event::EndPreformatted
925        );
926        assert_eq!(end_event_for(BlockKind::Table), Event::EndTable);
927        assert_eq!(end_event_for(BlockKind::TableCell), Event::EndTableCell);
928        assert_eq!(end_event_for(BlockKind::TableHeader), Event::EndTableHeader);
929        assert_eq!(end_event_for(BlockKind::TableRow), Event::EndTableRow);
930    }
931}