#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
use crate::{Event, EventSource, Result};
pub struct SkipEmptyBlocks<S: EventSource> {
inner: S,
buffered: Option<Event>,
pending: Option<Event>,
}
impl<S: EventSource> SkipEmptyBlocks<S> {
#[inline]
pub fn new(inner: S) -> Self {
Self {
inner,
buffered: None,
pending: None,
}
}
}
impl<S: EventSource> EventSource for SkipEmptyBlocks<S> {
#[inline]
fn next_event(&mut self) -> Result<Option<Event>> {
loop {
if let Some(pending) = self.pending.take() {
return Ok(Some(pending));
}
if let Some(buffered) = self.buffered.take() {
match self.inner.next_event()? {
Some(next) if is_matching_end(&buffered, &next) => {
continue;
}
Some(next) if is_skippable_start(&next) => {
self.buffered = Some(next);
return Ok(Some(buffered));
}
Some(next) => {
self.pending = Some(next);
return Ok(Some(buffered));
}
None => {
return Ok(Some(buffered));
}
}
}
match self.inner.next_event()? {
Some(event) if is_skippable_start(&event) => {
self.buffered = Some(event);
}
other => return Ok(other),
}
}
}
}
fn is_skippable_start(event: &Event) -> bool {
matches!(
event,
Event::StartHeading { .. } | Event::StartBlockQuote { .. } | Event::StartParagraph { .. }
)
}
fn is_matching_end(start: &Event, end: &Event) -> bool {
matches!(
(start, end),
(Event::StartHeading { .. }, Event::EndHeading)
| (Event::StartBlockQuote { .. }, Event::EndBlockQuote)
| (Event::StartParagraph { .. }, Event::EndParagraph)
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Event, EventSource, Result};
struct Replay {
events: alloc::vec::IntoIter<Event>,
error_at_end: bool,
}
impl Replay {
fn new(events: alloc::vec::Vec<Event>) -> Self {
Self {
events: events.into_iter(),
error_at_end: false,
}
}
fn with_terminal_error(events: alloc::vec::Vec<Event>) -> Self {
Self {
events: events.into_iter(),
error_at_end: true,
}
}
}
impl EventSource for Replay {
fn next_event(&mut self) -> Result<Option<Event>> {
if let Some(e) = self.events.next() {
Ok(Some(e))
} else if self.error_at_end {
self.error_at_end = false;
Err(crate::Error::Other {
message: "simulated".into(),
})
} else {
Ok(None)
}
}
}
fn drain<S: EventSource>(mut src: S) -> alloc::vec::Vec<Event> {
let mut out = alloc::vec::Vec::new();
while let Some(e) = src.next_event().expect("unexpected error") {
out.push(e);
}
out
}
#[test]
fn empty_heading_is_dropped() {
let replay = Replay::new(alloc::vec![
Event::StartHeading { id: None, level: 1 },
Event::EndHeading,
]);
assert_eq!(drain(SkipEmptyBlocks::new(replay)), alloc::vec![]);
}
#[test]
fn empty_block_quote_is_dropped() {
let replay = Replay::new(alloc::vec![
Event::StartBlockQuote { id: None },
Event::EndBlockQuote,
]);
assert_eq!(drain(SkipEmptyBlocks::new(replay)), alloc::vec![]);
}
#[test]
fn empty_paragraph_is_dropped() {
let replay = Replay::new(alloc::vec![
Event::StartParagraph {
alignment: None,
id: None
},
Event::EndParagraph,
]);
assert_eq!(drain(SkipEmptyBlocks::new(replay)), alloc::vec![]);
}
#[test]
fn non_empty_heading_is_preserved() {
let input = alloc::vec![
Event::StartHeading { id: None, level: 2 },
Event::Text {
content: "h".into()
},
Event::EndHeading,
];
let replay = Replay::new(input.clone());
assert_eq!(drain(SkipEmptyBlocks::new(replay)), input);
}
#[test]
fn non_empty_paragraph_is_preserved() {
let input = alloc::vec![
Event::StartParagraph {
alignment: None,
id: None
},
Event::Text {
content: "p".into()
},
Event::EndParagraph,
];
let replay = Replay::new(input.clone());
assert_eq!(drain(SkipEmptyBlocks::new(replay)), input);
}
#[test]
fn non_empty_block_quote_is_preserved() {
let input = alloc::vec![
Event::StartBlockQuote { id: None },
Event::StartParagraph {
alignment: None,
id: None
},
Event::Text {
content: "q".into()
},
Event::EndParagraph,
Event::EndBlockQuote,
];
let replay = Replay::new(input.clone());
assert_eq!(drain(SkipEmptyBlocks::new(replay)), input);
}
#[test]
fn consecutive_empty_headings_all_dropped() {
let replay = Replay::new(alloc::vec![
Event::StartHeading { id: None, level: 1 },
Event::EndHeading,
Event::StartHeading { id: None, level: 2 },
Event::EndHeading,
Event::StartHeading { id: None, level: 3 },
Event::EndHeading,
]);
assert_eq!(drain(SkipEmptyBlocks::new(replay)), alloc::vec![]);
}
#[test]
fn empty_then_nonempty_heading() {
let replay = Replay::new(alloc::vec![
Event::StartHeading { id: None, level: 1 },
Event::EndHeading,
Event::StartHeading { id: None, level: 2 },
Event::Text {
content: "x".into()
},
Event::EndHeading,
]);
assert_eq!(
drain(SkipEmptyBlocks::new(replay)),
alloc::vec![
Event::StartHeading { id: None, level: 2 },
Event::Text {
content: "x".into()
},
Event::EndHeading,
]
);
}
#[test]
fn empty_heading_with_id_is_still_dropped() {
let replay = Replay::new(alloc::vec![
Event::StartHeading {
id: Some("anchor".into()),
level: 1
},
Event::EndHeading,
]);
assert_eq!(drain(SkipEmptyBlocks::new(replay)), alloc::vec![]);
}
#[test]
fn empty_paragraph_with_alignment_is_still_dropped() {
let replay = Replay::new(alloc::vec![
Event::StartParagraph {
alignment: Some(crate::TextAlignment::Center),
id: None
},
Event::EndParagraph,
]);
assert_eq!(drain(SkipEmptyBlocks::new(replay)), alloc::vec![]);
}
#[test]
fn nested_empty_blockquote_containing_empty_paragraph_preserves_outer() {
let replay = Replay::new(alloc::vec![
Event::StartBlockQuote { id: None },
Event::StartParagraph {
alignment: None,
id: None
},
Event::EndParagraph,
Event::EndBlockQuote,
]);
assert_eq!(
drain(SkipEmptyBlocks::new(replay)),
alloc::vec![Event::StartBlockQuote { id: None }, Event::EndBlockQuote,]
);
}
#[test]
fn non_skippable_kinds_pass_through_unchanged() {
let input = alloc::vec![Event::StartTable { id: None }, Event::EndTable,];
let replay = Replay::new(input.clone());
assert_eq!(drain(SkipEmptyBlocks::new(replay)), input);
}
#[test]
fn empty_document_passes_through() {
let input = alloc::vec![
Event::StartDocument {
id: None,
language: None,
metadata: None
},
Event::EndDocument,
];
let replay = Replay::new(input.clone());
assert_eq!(drain(SkipEmptyBlocks::new(replay)), input);
}
#[test]
fn error_from_inner_propagates_when_no_buffer() {
let replay = Replay::with_terminal_error(alloc::vec![]);
let mut filter = SkipEmptyBlocks::new(replay);
assert!(filter.next_event().is_err());
}
#[test]
fn error_while_start_buffered_surfaces_immediately() {
let replay =
Replay::with_terminal_error(alloc::vec![Event::StartHeading { id: None, level: 1 },]);
let mut filter = SkipEmptyBlocks::new(replay);
assert!(filter.next_event().is_err());
assert_eq!(filter.next_event().unwrap(), None);
}
#[test]
fn send_sync_compile_assertion() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<SkipEmptyBlocks<Replay>>();
}
#[test]
fn many_consecutive_empty_blocks_do_not_blow_stack() {
const N: usize = 100_000;
let mut events = alloc::vec::Vec::with_capacity(N * 2);
for _ in 0..N {
events.push(Event::StartHeading { id: None, level: 1 });
events.push(Event::EndHeading);
}
let replay = Replay::new(events);
assert_eq!(drain(SkipEmptyBlocks::new(replay)), alloc::vec![]);
}
}