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 `Text` events (all non-block, non-End events).
173    ///
174    /// `ThematicBreak`: auto-closes an open `Paragraph` if present.
175    /// `Text`: auto-inserts 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 { .. }) && !self.has_open_content() {
186            let para = Event::StartParagraph {
187                alignment: None,
188                id: None,
189            };
190            self.stack.push(BlockKind::Paragraph);
191            self.sink.handle_event(para)?;
192        }
193
194        self.sink.handle_event(event)
195    }
196
197    /// Handles any Start event: validates Document/Link nesting constraints,
198    /// auto-closes an open Paragraph when needed, pushes the new block, and forwards the event.
199    fn handle_start_event(&mut self, kind: BlockKind, event: Event) -> Result<()> {
200        if kind == BlockKind::Document {
201            if self.stack.contains(&BlockKind::Document) {
202                return Err(Error::InvalidSequence {
203                    expected: "single Document".to_string(),
204                    found: "StartDocument".to_string(),
205                    message: "StartDocument received while Document already open".to_string(),
206                });
207            }
208            if self.document_finished {
209                return Err(Error::InvalidSequence {
210                    expected: "end of stream".to_string(),
211                    found: "StartDocument".to_string(),
212                    message: "StartDocument received after document already finished".to_string(),
213                });
214            }
215        }
216        // Links cannot nest per EVENTS.md
217        if kind == BlockKind::Link && self.stack.contains(&BlockKind::Link) {
218            return Err(Error::InvalidSequence {
219                expected: "no nested links".to_string(),
220                found: "StartLink".to_string(),
221                message: "StartLink received while another link is already open".to_string(),
222            });
223        }
224        if kind != BlockKind::Link && self.stack.last() == Some(&BlockKind::Paragraph) {
225            self.stack.pop();
226            self.sink.handle_event(Event::EndParagraph)?;
227        }
228        self.stack.push(kind);
229        self.sink.handle_event(event)
230    }
231
232    /// Returns `true` if the stack contains any content-bearing block.
233    ///
234    /// Content-bearing blocks are: [`BlockKind::Heading`], [`BlockKind::Paragraph`],
235    /// [`BlockKind::Preformatted`], [`BlockKind::Link`], [`BlockKind::DefinitionTerm`].
236    ///
237    /// Note: [`BlockKind::Blockquote`] is NOT content-bearing because block quotes contain
238    /// block elements (paragraphs, headings, etc.), not inline text directly. Text inside
239    /// a block quote without an explicit paragraph triggers auto-paragraph insertion.
240    ///
241    /// Note: [`BlockKind::OrderedListItem`], [`BlockKind::UnorderedListItem`], [`BlockKind::TableCell`], [`BlockKind::TableHeader`],
242    /// and [`BlockKind::DefinitionDetail`] are NOT content-bearing despite being able to
243    /// contain inline content per `EVENTS.md`. This is because downstream writers like
244    /// `BlockNoteWriter` rely on auto-paragraph insertion for these container types.
245    #[inline]
246    pub fn has_open_content(&self) -> bool {
247        self.stack.iter().any(|kind| {
248            matches!(
249                kind,
250                BlockKind::Heading
251                    | BlockKind::Paragraph
252                    | BlockKind::Preformatted
253                    | BlockKind::Link
254                    | BlockKind::DefinitionTerm
255            )
256        })
257    }
258
259    /// Returns `true` if the given block kind is anywhere in the current nesting stack.
260    #[inline]
261    pub fn is_inside(&self, kind: BlockKind) -> bool {
262        self.stack.contains(&kind)
263    }
264
265    /// Creates a new stack-tracking wrapper around the given sink.
266    #[inline]
267    pub fn new(sink: S) -> Self {
268        Self {
269            document_finished: false,
270            sink,
271            stack: Vec::new(),
272        }
273    }
274
275    /// Returns a reference to the inner sink.
276    #[cfg(test)]
277    fn sink(&self) -> &S {
278        &self.sink
279    }
280
281    /// Returns a slice of the current nesting stack.
282    ///
283    /// The first element is the outermost container (typically [`BlockKind::Document`]),
284    /// and the last element is the innermost currently open container.
285    #[inline]
286    pub fn stack(&self) -> &[BlockKind] {
287        &self.stack
288    }
289
290    /// Returns a mutable reference to the stack.
291    #[cfg(test)]
292    fn stack_mut(&mut self) -> &mut Vec<BlockKind> {
293        &mut self.stack
294    }
295}
296
297impl<S: EventSink> EventSink for StackTrackingSink<S> {
298    #[inline]
299    fn finish(self) -> Result<()> {
300        self.sink.finish()
301    }
302
303    #[inline]
304    fn handle_event(&mut self, event: Event) -> Result<()> {
305        // Reject all events after document has finished (except StartDocument gets a
306        // clearer error message from handle_start_event)
307        if self.document_finished && !matches!(event, Event::StartDocument { .. }) {
308            return Err(Error::InvalidSequence {
309                expected: "end of stream".to_string(),
310                found: format!("{event:?}"),
311                message: "event received after document already finished".to_string(),
312            });
313        }
314
315        if let Some(kind) = block_kind_for_start(&event) {
316            return self.handle_start_event(kind, event);
317        }
318
319        if matches!(event, Event::EndDocument) {
320            return self.handle_end_document();
321        }
322
323        if block_kind_for_end(&event).is_some() {
324            return self.handle_end_event(event);
325        }
326
327        self.handle_other_event(event)
328    }
329}
330
331block_kinds! {
332    Blockquote        => (StartBlockQuote,        EndBlockQuote),
333    Caption           => (StartCaption,           EndCaption),
334    DefinitionDetail  => (StartDefinitionDetail,  EndDefinitionDetail),
335    DefinitionList    => (StartDefinitionList,    EndDefinitionList),
336    DefinitionTerm    => (StartDefinitionTerm,    EndDefinitionTerm),
337    Document          => (StartDocument,          EndDocument),
338    Footnote          => (StartFootnote,          EndFootnote),
339    Heading           => (StartHeading,           EndHeading),
340    Link              => (StartLink,              EndLink),
341    OrderedListItem   => (StartOrderedListItem,   EndOrderedListItem),
342    Paragraph         => (StartParagraph,         EndParagraph),
343    Preformatted      => (StartPreformatted,      EndPreformatted),
344    Table             => (StartTable,             EndTable),
345    TableCell         => (StartTableCell,         EndTableCell),
346    TableHeader       => (StartTableHeader,       EndTableHeader),
347    TableRow          => (StartTableRow,          EndTableRow),
348    UnorderedListItem => (StartUnorderedListItem, EndUnorderedListItem),
349}
350
351#[cfg(test)]
352mod tests {
353    use alloc::vec::Vec;
354
355    use super::*;
356    use crate::TextStyle;
357
358    struct MockSink {
359        events: Vec<Event>,
360    }
361
362    impl MockSink {
363        fn new() -> Self {
364            Self { events: Vec::new() }
365        }
366    }
367
368    impl EventSink for MockSink {
369        fn finish(self) -> Result<()> {
370            Ok(())
371        }
372
373        fn handle_event(&mut self, event: Event) -> Result<()> {
374            self.events.push(event);
375            Ok(())
376        }
377    }
378
379    fn send(sink: &mut StackTrackingSink<MockSink>, event: Event) {
380        let result = sink.handle_event(event);
381        assert!(result.is_ok());
382    }
383
384    #[test]
385    fn has_open_content_with_blockquote_returns_false() {
386        let mock = MockSink::new();
387        let mut sink = StackTrackingSink::new(mock);
388        sink.stack_mut().push(BlockKind::Document);
389        sink.stack_mut().push(BlockKind::Blockquote);
390        // Blockquote is NOT content-bearing - it contains block elements, not inline text
391        assert!(!sink.has_open_content());
392    }
393
394    #[test]
395    fn has_open_content_with_heading() {
396        let mock = MockSink::new();
397        let mut sink = StackTrackingSink::new(mock);
398        sink.stack_mut().push(BlockKind::Document);
399        sink.stack_mut().push(BlockKind::Heading);
400        assert!(sink.has_open_content());
401    }
402
403    #[test]
404    fn has_open_content_with_paragraph() {
405        let mock = MockSink::new();
406        let mut sink = StackTrackingSink::new(mock);
407        sink.stack_mut().push(BlockKind::Document);
408        sink.stack_mut().push(BlockKind::Paragraph);
409        assert!(sink.has_open_content());
410    }
411
412    #[test]
413    fn has_open_content_with_preformatted() {
414        let mock = MockSink::new();
415        let mut sink = StackTrackingSink::new(mock);
416        sink.stack_mut().push(BlockKind::Document);
417        sink.stack_mut().push(BlockKind::Preformatted);
418        assert!(sink.has_open_content());
419    }
420
421    #[test]
422    fn has_open_content_without_content_blocks() {
423        let mock = MockSink::new();
424        let mut sink = StackTrackingSink::new(mock);
425        sink.stack_mut().push(BlockKind::Document);
426        sink.stack_mut().push(BlockKind::Table);
427        sink.stack_mut().push(BlockKind::TableRow);
428        assert!(!sink.has_open_content());
429    }
430
431    #[test]
432    fn is_inside_finds_nested_kind() {
433        let mock = MockSink::new();
434        let mut sink = StackTrackingSink::new(mock);
435        sink.stack_mut().push(BlockKind::Document);
436        sink.stack_mut().push(BlockKind::Blockquote);
437        sink.stack_mut().push(BlockKind::Paragraph);
438        assert!(sink.is_inside(BlockKind::Blockquote));
439    }
440
441    #[test]
442    fn is_inside_returns_false_for_missing_kind() {
443        let mock = MockSink::new();
444        let mut sink = StackTrackingSink::new(mock);
445        sink.stack_mut().push(BlockKind::Document);
446        sink.stack_mut().push(BlockKind::Paragraph);
447        assert!(!sink.is_inside(BlockKind::Blockquote));
448    }
449
450    #[test]
451    fn stack_returns_current_stack() {
452        let mock = MockSink::new();
453        let mut sink = StackTrackingSink::new(mock);
454        sink.stack_mut().push(BlockKind::Document);
455        sink.stack_mut().push(BlockKind::Paragraph);
456        let stack = sink.stack();
457        assert_eq!(stack.len(), 2);
458        assert_eq!(stack.first(), Some(&BlockKind::Document));
459        assert_eq!(stack.get(1), Some(&BlockKind::Paragraph));
460    }
461
462    #[test]
463    fn passthrough_forwards_all_events() {
464        let mock = MockSink::new();
465        let mut sink = StackTrackingSink::new(mock);
466
467        send(
468            &mut sink,
469            Event::StartDocument {
470                id: None,
471                language: None,
472                metadata: None,
473            },
474        );
475        send(
476            &mut sink,
477            Event::StartParagraph {
478                alignment: None,
479                id: None,
480            },
481        );
482        send(
483            &mut sink,
484            Event::Text {
485                content: "hello".to_string(),
486                style: TextStyle::default(),
487            },
488        );
489        send(&mut sink, Event::EndParagraph);
490        send(&mut sink, Event::EndDocument);
491
492        assert_eq!(sink.sink().events.len(), 5);
493        assert!(matches!(
494            sink.sink().events.first(),
495            Some(Event::StartDocument { .. })
496        ));
497        assert!(matches!(
498            sink.sink().events.get(1),
499            Some(Event::StartParagraph { .. })
500        ));
501        assert!(matches!(
502            sink.sink().events.get(2),
503            Some(Event::Text { .. })
504        ));
505        assert!(matches!(
506            sink.sink().events.get(3),
507            Some(Event::EndParagraph)
508        ));
509        assert!(matches!(
510            sink.sink().events.get(4),
511            Some(Event::EndDocument)
512        ));
513        assert!(sink.stack().is_empty());
514    }
515
516    #[test]
517    fn orphan_text_gets_paragraph() {
518        let mock = MockSink::new();
519        let mut sink = StackTrackingSink::new(mock);
520
521        send(
522            &mut sink,
523            Event::StartDocument {
524                id: None,
525                language: None,
526                metadata: None,
527            },
528        );
529        send(
530            &mut sink,
531            Event::Text {
532                content: "hello".to_string(),
533                style: TextStyle::default(),
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                style: TextStyle::default(),
589            },
590        );
591        send(&mut sink, Event::EndTableCell);
592        send(&mut sink, Event::EndTableRow);
593        send(&mut sink, Event::EndTable);
594        send(&mut sink, Event::EndDocument);
595
596        assert_eq!(sink.sink().events.len(), 11);
597        assert_eq!(
598            sink.sink().events.get(4),
599            Some(&Event::StartParagraph {
600                alignment: None,
601                id: None
602            })
603        );
604        assert_eq!(sink.sink().events.get(6), Some(&Event::EndParagraph));
605    }
606
607    #[test]
608    fn orphan_text_inside_blockquote_gets_paragraph() {
609        let mock = MockSink::new();
610        let mut sink = StackTrackingSink::new(mock);
611
612        send(
613            &mut sink,
614            Event::StartDocument {
615                id: None,
616                language: None,
617                metadata: None,
618            },
619        );
620        send(&mut sink, Event::StartBlockQuote { id: None });
621        send(
622            &mut sink,
623            Event::Text {
624                content: "quoted".to_string(),
625                style: TextStyle::default(),
626            },
627        );
628        send(&mut sink, Event::EndBlockQuote);
629        send(&mut sink, Event::EndDocument);
630
631        // Should auto-insert paragraph around orphan text inside blockquote
632        assert_eq!(sink.sink().events.len(), 7);
633        assert!(matches!(
634            sink.sink().events.first(),
635            Some(Event::StartDocument { .. })
636        ));
637        assert!(matches!(
638            sink.sink().events.get(1),
639            Some(Event::StartBlockQuote { .. })
640        ));
641        assert_eq!(
642            sink.sink().events.get(2),
643            Some(&Event::StartParagraph {
644                alignment: None,
645                id: None
646            })
647        );
648        assert!(matches!(
649            sink.sink().events.get(3),
650            Some(Event::Text { .. })
651        ));
652        assert_eq!(sink.sink().events.get(4), Some(&Event::EndParagraph));
653        assert!(matches!(
654            sink.sink().events.get(5),
655            Some(Event::EndBlockQuote)
656        ));
657        assert!(matches!(
658            sink.sink().events.get(6),
659            Some(Event::EndDocument)
660        ));
661    }
662
663    #[test]
664    fn text_inside_paragraph_no_extra_insert() {
665        let mock = MockSink::new();
666        let mut sink = StackTrackingSink::new(mock);
667
668        send(
669            &mut sink,
670            Event::StartDocument {
671                id: None,
672                language: None,
673                metadata: None,
674            },
675        );
676        send(
677            &mut sink,
678            Event::StartParagraph {
679                alignment: None,
680                id: None,
681            },
682        );
683        send(
684            &mut sink,
685            Event::Text {
686                content: "hello".to_string(),
687                style: TextStyle::default(),
688            },
689        );
690        send(
691            &mut sink,
692            Event::Text {
693                content: "world".to_string(),
694                style: TextStyle::default(),
695            },
696        );
697        send(&mut sink, Event::EndParagraph);
698        send(&mut sink, Event::EndDocument);
699
700        assert_eq!(sink.sink().events.len(), 6);
701    }
702
703    #[test]
704    fn auto_close_paragraph_on_end_table() {
705        let mock = MockSink::new();
706        let mut sink = StackTrackingSink::new(mock);
707
708        send(
709            &mut sink,
710            Event::StartDocument {
711                id: None,
712                language: None,
713                metadata: None,
714            },
715        );
716        send(&mut sink, Event::StartTable { id: None });
717        send(&mut sink, Event::StartTableRow { id: None });
718        send(
719            &mut sink,
720            Event::StartTableCell {
721                colspan: None,
722                id: None,
723                rowspan: None,
724            },
725        );
726        send(
727            &mut sink,
728            Event::StartParagraph {
729                alignment: None,
730                id: None,
731            },
732        );
733        send(
734            &mut sink,
735            Event::Text {
736                content: "cell".to_string(),
737                style: TextStyle::default(),
738            },
739        );
740        send(&mut sink, Event::EndTable);
741
742        assert_eq!(sink.sink().events.len(), 10);
743        assert_eq!(sink.sink().events.get(6), Some(&Event::EndParagraph));
744        assert_eq!(sink.sink().events.get(7), Some(&Event::EndTableCell));
745        assert_eq!(sink.sink().events.get(8), Some(&Event::EndTableRow));
746        assert_eq!(sink.sink().events.get(9), Some(&Event::EndTable));
747    }
748
749    #[test]
750    fn auto_close_on_end_blockquote() {
751        let mock = MockSink::new();
752        let mut sink = StackTrackingSink::new(mock);
753
754        send(
755            &mut sink,
756            Event::StartDocument {
757                id: None,
758                language: None,
759                metadata: None,
760            },
761        );
762        send(&mut sink, Event::StartBlockQuote { id: None });
763        send(
764            &mut sink,
765            Event::StartParagraph {
766                alignment: None,
767                id: None,
768            },
769        );
770        send(
771            &mut sink,
772            Event::Text {
773                content: "quote".to_string(),
774                style: TextStyle::default(),
775            },
776        );
777        send(&mut sink, Event::EndBlockQuote);
778
779        assert_eq!(sink.sink().events.len(), 6);
780        assert_eq!(sink.sink().events.get(4), Some(&Event::EndParagraph));
781        assert_eq!(sink.sink().events.get(5), Some(&Event::EndBlockQuote));
782    }
783
784    #[test]
785    fn start_block_closes_open_paragraph() {
786        let mock = MockSink::new();
787        let mut sink = StackTrackingSink::new(mock);
788
789        send(
790            &mut sink,
791            Event::StartDocument {
792                id: None,
793                language: None,
794                metadata: None,
795            },
796        );
797        send(
798            &mut sink,
799            Event::StartParagraph {
800                alignment: None,
801                id: None,
802            },
803        );
804        send(&mut sink, Event::StartHeading { id: None, level: 1 });
805
806        assert_eq!(sink.sink().events.get(2), Some(&Event::EndParagraph));
807        assert!(matches!(
808            sink.sink().events.get(3),
809            Some(Event::StartHeading { .. })
810        ));
811    }
812
813    #[test]
814    fn start_document_while_document_open_returns_error() {
815        let mock = MockSink::new();
816        let mut sink = StackTrackingSink::new(mock);
817
818        send(
819            &mut sink,
820            Event::StartDocument {
821                id: None,
822                language: None,
823                metadata: None,
824            },
825        );
826        let result = sink.handle_event(Event::StartDocument {
827            id: None,
828            language: None,
829            metadata: None,
830        });
831
832        assert!(matches!(result, Err(Error::InvalidSequence { .. })));
833    }
834
835    #[test]
836    fn thematic_break_closes_open_paragraph() {
837        let mock = MockSink::new();
838        let mut sink = StackTrackingSink::new(mock);
839
840        send(
841            &mut sink,
842            Event::StartDocument {
843                id: None,
844                language: None,
845                metadata: None,
846            },
847        );
848        send(
849            &mut sink,
850            Event::StartParagraph {
851                alignment: None,
852                id: None,
853            },
854        );
855        send(&mut sink, Event::ThematicBreak { id: None });
856
857        assert_eq!(sink.sink().events.get(2), Some(&Event::EndParagraph));
858        assert!(matches!(
859            sink.sink().events.get(3),
860            Some(Event::ThematicBreak { .. })
861        ));
862    }
863
864    #[test]
865    fn end_document_closes_all() {
866        let mock = MockSink::new();
867        let mut sink = StackTrackingSink::new(mock);
868
869        send(
870            &mut sink,
871            Event::StartDocument {
872                id: None,
873                language: None,
874                metadata: None,
875            },
876        );
877        send(&mut sink, Event::StartBlockQuote { id: None });
878        send(
879            &mut sink,
880            Event::StartParagraph {
881                alignment: None,
882                id: None,
883            },
884        );
885        send(&mut sink, Event::EndDocument);
886
887        assert_eq!(sink.sink().events.len(), 6);
888        assert_eq!(sink.sink().events.get(3), Some(&Event::EndParagraph));
889        assert_eq!(sink.sink().events.get(4), Some(&Event::EndBlockQuote));
890        assert_eq!(sink.sink().events.get(5), Some(&Event::EndDocument));
891        assert!(sink.stack().is_empty());
892    }
893
894    #[test]
895    fn end_event_for_all_kinds() {
896        assert_eq!(end_event_for(BlockKind::Blockquote), Event::EndBlockQuote);
897        assert_eq!(end_event_for(BlockKind::Caption), Event::EndCaption);
898        assert_eq!(
899            end_event_for(BlockKind::DefinitionDetail),
900            Event::EndDefinitionDetail
901        );
902        assert_eq!(
903            end_event_for(BlockKind::DefinitionList),
904            Event::EndDefinitionList
905        );
906        assert_eq!(
907            end_event_for(BlockKind::DefinitionTerm),
908            Event::EndDefinitionTerm
909        );
910        assert_eq!(end_event_for(BlockKind::Document), Event::EndDocument);
911        assert_eq!(end_event_for(BlockKind::Footnote), Event::EndFootnote);
912        assert_eq!(end_event_for(BlockKind::Heading), Event::EndHeading);
913        assert_eq!(end_event_for(BlockKind::Link), Event::EndLink);
914        assert_eq!(
915            end_event_for(BlockKind::OrderedListItem),
916            Event::EndOrderedListItem
917        );
918        assert_eq!(
919            end_event_for(BlockKind::UnorderedListItem),
920            Event::EndUnorderedListItem
921        );
922        assert_eq!(end_event_for(BlockKind::Paragraph), Event::EndParagraph);
923        assert_eq!(
924            end_event_for(BlockKind::Preformatted),
925            Event::EndPreformatted
926        );
927        assert_eq!(end_event_for(BlockKind::Table), Event::EndTable);
928        assert_eq!(end_event_for(BlockKind::TableCell), Event::EndTableCell);
929        assert_eq!(end_event_for(BlockKind::TableHeader), Event::EndTableHeader);
930        assert_eq!(end_event_for(BlockKind::TableRow), Event::EndTableRow);
931    }
932}