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