Skip to main content

SkipEmptyBlocks

Struct SkipEmptyBlocks 

Source
pub struct SkipEmptyBlocks<S: EventSource> { /* private fields */ }
Expand description

A streaming EventSource adapter that suppresses empty Heading, BlockQuote, and Paragraph Start/End pairs from the wrapped source.

The adapter uses a 1-event look-back (hold-1) algorithm. When it sees a candidate skippable Start* event (StartHeading, StartBlockQuote, or StartParagraph), it buffers that event and peeks the next event from the inner source. If the next event is the matching End* (i.e., the block is empty), both events are dropped and the adapter keeps draining iteratively until it finds an event to emit. If the next event is something else, the buffered Start* is emitted immediately and the “something else” is stashed as pending for the next call. Memory is O(1) — at most two Event values are held at any time. Stack is O(1) — the implementation is a single loop with no recursion, so an arbitrarily long run of empty blocks consumes constant stack regardless of input size.

§Skip Set

Exactly three Start/End pairs are matched:

  • Event::StartHeading { .. }Event::EndHeading
  • Event::StartBlockQuote { .. }Event::EndBlockQuote
  • Event::StartParagraph { .. }Event::EndParagraph

No other variants are suppressed. Empty StartTable, StartOrderedListItem, and all other containers pass through unchanged.

§Asymmetric API

docspec-cli and docspec-http apply this filter automatically by default. Library users opt in by wrapping their EventSource explicitly: SkipEmptyBlocks::new(my_reader).

§Known Limitations

No cascading: an outer container that becomes empty because its inner contents were filtered is preserved. Example: StartBlockQuote → StartParagraph → EndParagraph → EndBlockQuote produces StartBlockQuote → EndBlockQuote (the inner empty paragraph pair is dropped, but the outer block quote is preserved). A subsequent pass would be needed to suppress the outer.

Empty table cells: an empty table cell containing only StartParagraph → EndParagraph will have the inner pair dropped, leaving the cell with no child events. The cell itself is preserved (table cells are not in the skip set).

Fail-fast: if the inner source returns Err while a Start* is buffered, the error propagates immediately and the buffered Start* is dropped silently. The stream is considered terminated; no partial recovery is attempted.

§Example

use docspec_core::{Event, EventSource, Result, SkipEmptyBlocks};

struct Replay {
    events: std::vec::IntoIter<Event>,
}
impl Replay {
    fn new(events: Vec<Event>) -> Self {
        Self { events: events.into_iter() }
    }
}
impl EventSource for Replay {
    fn next_event(&mut self) -> Result<Option<Event>> {
        Ok(self.events.next())
    }
}

// An empty heading followed by a heading with text:
let inner = Replay::new(vec![
    Event::StartHeading { id: None, level: 1 },
    Event::EndHeading,
    Event::StartHeading { id: None, level: 2 },
    Event::Text { content: String::from("Hello") },
    Event::EndHeading,
]);
let mut filtered = SkipEmptyBlocks::new(inner);

// The empty H1 is dropped; the H2 with text passes through.
assert_eq!(
    filtered.next_event().unwrap(),
    Some(Event::StartHeading { id: None, level: 2 }),
);
assert_eq!(
    filtered.next_event().unwrap(),
    Some(Event::Text { content: String::from("Hello") }),
);
assert_eq!(filtered.next_event().unwrap(), Some(Event::EndHeading));
assert_eq!(filtered.next_event().unwrap(), None);

Implementations§

Source§

impl<S: EventSource> SkipEmptyBlocks<S>

Source

pub fn new(inner: S) -> Self

Wraps the given source so that empty Heading, BlockQuote, and Paragraph Start/End pairs are suppressed in the emitted stream.

Trait Implementations§

Source§

impl<S: EventSource> EventSource for SkipEmptyBlocks<S>

Source§

fn next_event(&mut self) -> Result<Option<Event>>

Returns the next event from the stream, or None if the stream has ended. Read more

Auto Trait Implementations§

§

impl<S> !RefUnwindSafe for SkipEmptyBlocks<S>

§

impl<S> !UnwindSafe for SkipEmptyBlocks<S>

§

impl<S> Freeze for SkipEmptyBlocks<S>
where S: Freeze,

§

impl<S> Send for SkipEmptyBlocks<S>
where S: Send,

§

impl<S> Sync for SkipEmptyBlocks<S>
where S: Sync,

§

impl<S> Unpin for SkipEmptyBlocks<S>
where S: Unpin,

§

impl<S> UnsafeUnpin for SkipEmptyBlocks<S>
where S: UnsafeUnpin,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.