From c12e2d94f72c87e7787b7134ac034dce7a3294ce Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 31 Oct 2025 18:18:16 +0100 Subject: [PATCH 1/8] lib: add Document event can be used by renderer to detect start and end of document --- src/block.rs | 12 ++++- src/html.rs | 2 + src/lib.rs | 109 +++++++++++++++++++++++++++++++++++++++++- tests/parse_events.rs | 6 ++- 4 files changed, 125 insertions(+), 4 deletions(-) diff --git a/src/block.rs b/src/block.rs index 7d627e7..976d765 100644 --- a/src/block.rs +++ b/src/block.rs @@ -77,6 +77,7 @@ pub enum Leaf<'s> { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Container<'s> { + Document, Blockquote, Div { class: &'s str }, List { ty: ListType, tight: bool }, @@ -223,6 +224,8 @@ impl<'s> TreeParser<'s> { #[must_use] fn parse(mut self) -> Vec> { + self.enter(Node::Container(Document), 0..0); + let mut lines = lines(self.src).collect::>(); let mut line_pos = 0; while line_pos < lines.len() { @@ -239,7 +242,10 @@ impl<'s> TreeParser<'s> { for _ in std::mem::take(&mut self.open_sections).drain(..) { self.exit(self.src.len()..self.src.len()); } + + self.exit(self.src.len()..self.src.len()); // Document debug_assert_eq!(self.open, &[]); + self.events } @@ -1317,7 +1323,11 @@ mod test { ($src:expr $(,$($event:expr),* $(,)?)?) => { let t = super::TreeParser::new($src).parse(); let actual = t.into_iter().map(|ev| (ev.kind, &$src[ev.span])).collect::>(); - let expected = &[$($($event),*,)?]; + let expected = &[ + (Enter(Container(Document)), ""), + $($($event),*,)? + (Exit(Container(Document)), ""), + ]; assert_eq!( actual, expected, diff --git a/src/html.rs b/src/html.rs index 35039c4..d145a70 100644 --- a/src/html.rs +++ b/src/html.rs @@ -401,6 +401,7 @@ impl<'s, 'f> Writer<'s, 'f> { return Ok(()); } match &c { + Container::Document => return Ok(()), Container::Blockquote => out.write_str(" { self.list_tightness.push(*tight); @@ -580,6 +581,7 @@ impl<'s, 'f> Writer<'s, 'f> { return Ok(()); } match c { + Container::Document => return Ok(()), Container::Blockquote => out.write_str("")?, Container::List { kind, .. } => { self.list_tightness.pop(); diff --git a/src/lib.rs b/src/lib.rs index 71374f3..f769057 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -161,6 +161,7 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start( /// Container::Paragraph, /// [(AttributeKind::Id, "a".into())].into_iter().collect(), @@ -172,6 +173,7 @@ pub enum Event<'s> { /// Event::Str("word".into()), /// Event::End(Container::Span), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

word

\n"; @@ -196,9 +198,11 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("str".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

str

\n"; @@ -216,11 +220,13 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("txt".into()), /// Event::FootnoteReference("nb".into()), /// Event::Str(".".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -248,10 +254,12 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("a ".into()), /// Event::Symbol("sym".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

a :sym:

\n"; @@ -269,11 +277,13 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::LeftSingleQuote, /// Event::Str("quote".into()), /// Event::RightSingleQuote, /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

‘quote’

\n"; @@ -291,11 +301,13 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::RightSingleQuote, /// Event::Str("Tis Socrates".into()), /// Event::RightSingleQuote, /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

’Tis Socrates’

\n"; @@ -313,12 +325,14 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::LeftDoubleQuote, /// Event::Str("Hello,".into()), /// Event::RightDoubleQuote, /// Event::Str(" he said".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

“Hello,” he said

\n"; @@ -338,10 +352,12 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("yes".into()), /// Event::Ellipsis, /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

yes…

\n"; @@ -359,11 +375,13 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("57".into()), /// Event::EnDash, /// Event::Str("33".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

57–33

\n"; @@ -381,11 +399,13 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("oxen".into()), /// Event::EmDash, /// Event::Str("and".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

oxen—and

\n"; @@ -403,12 +423,14 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("no".into()), /// Event::Escape, /// Event::NonBreakingSpace, /// Event::Str("break".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

no break

\n"; @@ -429,11 +451,13 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("soft".into()), /// Event::Softbreak, /// Event::Str("break".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -457,12 +481,14 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("hard".into()), /// Event::Escape, /// Event::Hardbreak, /// Event::Str("break".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -483,12 +509,14 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Escape, /// Event::Str("*a".into()), /// Event::Escape, /// Event::Str("*".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

*a*

\n"; @@ -510,6 +538,7 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("para0".into()), /// Event::End(Container::Paragraph), @@ -517,6 +546,7 @@ pub enum Event<'s> { /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("para1".into()), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -545,6 +575,7 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("para0".into()), /// Event::End(Container::Paragraph), @@ -559,6 +590,7 @@ pub enum Event<'s> { /// .into_iter() /// .collect(), /// ), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -587,6 +619,7 @@ pub enum Event<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Attributes( /// [(AttributeKind::Id, "a".into())] /// .into_iter() @@ -607,6 +640,7 @@ pub enum Event<'s> { /// .into_iter() /// .collect(), /// ), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -627,6 +661,10 @@ pub enum Event<'s> { /// - block container, may contain any block-level elements. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Container<'s> { + /// A document. + /// + /// Should appear once at the start and end of the event stream. + Document, /// A blockquote element. /// /// # Examples @@ -641,6 +679,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Blockquote, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("a".into()), @@ -648,6 +687,7 @@ pub enum Container<'s> { /// Event::Str("b".into()), /// Event::End(Container::Paragraph), /// Event::End(Container::Blockquote), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -674,6 +714,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start( /// Container::List { /// kind: ListKind::Unordered(ListBulletType::Dash), @@ -696,6 +737,7 @@ pub enum Container<'s> { /// kind: ListKind::Unordered(ListBulletType::Dash), /// tight: false /// }), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -722,6 +764,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start( /// Container::List { /// kind: ListKind::Unordered(ListBulletType::Dash), @@ -738,6 +781,7 @@ pub enum Container<'s> { /// kind: ListKind::Unordered(ListBulletType::Dash), /// tight: true, /// }), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -761,6 +805,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start( /// Container::List { /// kind: ListKind::Task(ListBulletType::Dash), @@ -780,6 +825,7 @@ pub enum Container<'s> { /// kind: ListKind::Task(ListBulletType::Dash), /// tight: true, /// }), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -811,6 +857,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::DescriptionList, Attributes::new()), /// Event::Start(Container::DescriptionTerm, Attributes::new()), /// Event::Str("orange".into()), @@ -831,6 +878,7 @@ pub enum Container<'s> { /// Event::End(Container::Paragraph), /// Event::End(Container::DescriptionDetails), /// Event::End(Container::DescriptionList), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -865,6 +913,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("txt".into()), /// Event::FootnoteReference("nb".into()), @@ -878,6 +927,7 @@ pub enum Container<'s> { /// Event::Str("actually..".into()), /// Event::End(Container::Paragraph), /// Event::End(Container::Footnote { label: "nb".into() }), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -909,6 +959,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Table, Attributes::new()), /// Event::Start( /// Container::TableRow { head: true }, @@ -969,6 +1020,7 @@ pub enum Container<'s> { /// }), /// Event::End(Container::TableRow { head: false } ), /// Event::End(Container::Table), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -1003,6 +1055,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start( /// Container::Section { id: "outer".into() }, /// Attributes::new(), @@ -1042,6 +1095,7 @@ pub enum Container<'s> { /// }), /// Event::End(Container::Section { id: "inner".into() }), /// Event::End(Container::Section { id: "outer".into() }), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -1070,6 +1124,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start( /// Container::Div { class: "note".into() }, /// Attributes::new(), @@ -1078,6 +1133,7 @@ pub enum Container<'s> { /// Event::Str("this is a note".into()), /// Event::End(Container::Paragraph), /// Event::End(Container::Div { class: "note".into() }), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -1101,6 +1157,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start( /// Container::Section { id: "heading".into() }, /// Attributes::new(), @@ -1120,6 +1177,7 @@ pub enum Container<'s> { /// id: "heading".into(), /// }), /// Event::End(Container::Section { id: "heading".into() }), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -1150,6 +1208,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Table, Attributes::new()), /// Event::Start(Container::Caption, Attributes::new()), /// Event::Str("caption".into()), @@ -1172,6 +1231,7 @@ pub enum Container<'s> { /// }), /// Event::End(Container::TableRow { head: false } ), /// Event::End(Container::Table), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -1198,12 +1258,14 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start( /// Container::LinkDefinition { label: "label".into() }, /// Attributes::new(), /// ), /// Event::Str("url".into()), /// Event::End(Container::LinkDefinition { label: "label".into() }), + /// Event::End(Container::Document), /// ], /// ); /// let html = "\n"; @@ -1225,12 +1287,14 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start( /// Container::RawBlock { format: "html".into() }, /// Attributes::new(), /// ), /// Event::Str("x".into()), /// Event::End(Container::RawBlock { format: "html".into() }), + /// Event::End(Container::Document), /// ], /// ); /// let html = "x\n"; @@ -1252,12 +1316,14 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start( /// Container::CodeBlock { language: "html".into() }, /// Attributes::new(), /// ), /// Event::Str("x\n".into()), /// Event::End(Container::CodeBlock { language: "html".into() }), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -1283,6 +1349,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start( /// Container::Span, @@ -1298,6 +1365,7 @@ pub enum Container<'s> { /// Event::Str("two words".into()), /// Event::End(Container::Span), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -1323,6 +1391,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start( /// Container::Link( @@ -1350,6 +1419,7 @@ pub enum Container<'s> { /// LinkType::Email, /// )), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -1368,6 +1438,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start( /// Container::Link( @@ -1382,6 +1453,7 @@ pub enum Container<'s> { /// LinkType::Span(SpanLinkType::Inline)), /// ), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

anchor

\n"; @@ -1403,6 +1475,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start( /// Container::Link( @@ -1437,6 +1510,7 @@ pub enum Container<'s> { /// ), /// Event::Str("url".into()), /// Event::End(Container::LinkDefinition { label: "label".into() }), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -1459,6 +1533,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start( /// Container::Image("img.png".into(), SpanLinkType::Inline), @@ -1469,6 +1544,7 @@ pub enum Container<'s> { /// Container::Image("img.png".into(), SpanLinkType::Inline), /// ), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

\"alt

\n"; @@ -1486,12 +1562,14 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("inline ".into()), /// Event::Start(Container::Verbatim, Attributes::new()), /// Event::Str("verbatim".into()), /// Event::End(Container::Verbatim), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

inline verbatim

\n"; @@ -1512,6 +1590,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Str("inline ".into()), /// Event::Start( @@ -1530,6 +1609,7 @@ pub enum Container<'s> { /// Event::Str(r"\frac{a}{b}".into()), /// Event::End(Container::Math { display: true }), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = concat!( @@ -1550,6 +1630,7 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start( /// Container::RawInline { format: "html".into() }, Attributes::new(), @@ -1557,6 +1638,7 @@ pub enum Container<'s> { /// Event::Str("a".into()), /// Event::End(Container::RawInline { format: "html".into() }), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

a

\n"; @@ -1574,11 +1656,13 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start(Container::Subscript, Attributes::new()), /// Event::Str("SUB".into()), /// Event::End(Container::Subscript), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

SUB

\n"; @@ -1596,11 +1680,13 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start(Container::Superscript, Attributes::new()), /// Event::Str("SUP".into()), /// Event::End(Container::Superscript), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

SUP

\n"; @@ -1618,11 +1704,13 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start(Container::Insert, Attributes::new()), /// Event::Str("INS".into()), /// Event::End(Container::Insert), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

INS

\n"; @@ -1640,11 +1728,13 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start(Container::Delete, Attributes::new()), /// Event::Str("DEL".into()), /// Event::End(Container::Delete), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

DEL

\n"; @@ -1662,11 +1752,13 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start(Container::Strong, Attributes::new()), /// Event::Str("STRONG".into()), /// Event::End(Container::Strong), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

STRONG

\n"; @@ -1684,11 +1776,13 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start(Container::Emphasis, Attributes::new()), /// Event::Str("EM".into()), /// Event::End(Container::Emphasis), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

EM

\n"; @@ -1706,11 +1800,13 @@ pub enum Container<'s> { /// assert_eq!( /// &events, /// &[ + /// Event::Start(Container::Document, Attributes::new()), /// Event::Start(Container::Paragraph, Attributes::new()), /// Event::Start(Container::Mark, Attributes::new()), /// Event::Str("MARK".into()), /// Event::End(Container::Mark), /// Event::End(Container::Paragraph), + /// Event::End(Container::Document), /// ], /// ); /// let html = "

MARK

\n"; @@ -1743,7 +1839,8 @@ impl Container<'_> { | Self::LinkDefinition { .. } | Self::RawBlock { .. } | Self::CodeBlock { .. } => true, - Self::Span + Self::Document + | Self::Span | Self::Link(..) | Self::Image(..) | Self::Verbatim @@ -1774,7 +1871,8 @@ impl Container<'_> { | Self::TableRow { .. } | Self::Section { .. } | Self::Div { .. } => true, - Self::Paragraph + Self::Document + | Self::Paragraph | Self::Heading { .. } | Self::TableCell { .. } | Self::Caption @@ -2282,6 +2380,7 @@ impl<'s> Parser<'s> { /// .collect::>() /// .as_slice(), /// &[ + /// ("", Start(Document, ..)), /// (">", Start(Blockquote, ..)), /// ("", Start(Paragraph, ..)), /// ("_", Start(Emphasis, ..)), @@ -2293,6 +2392,7 @@ impl<'s> Parser<'s> { /// ("](url)", End(Link { .. })), /// ("", End(Paragraph)), /// ("", End(Blockquote)), + /// ("", End(Document)), /// ], /// )); /// ``` @@ -2314,6 +2414,7 @@ impl<'s> Parser<'s> { /// .collect::>() /// .as_slice(), /// &[ + /// ("", Start(Document, ..)), /// ("\n", Blankline), /// ("{.quote}\n>", Start(Blockquote, ..)), /// ("", Start(Paragraph, ..)), @@ -2323,6 +2424,7 @@ impl<'s> Parser<'s> { /// (" world!", Str(..)), /// ("", End(Paragraph)), /// ("", End(Blockquote)), + /// ("", End(Document)), /// ], /// )); /// ``` @@ -2344,6 +2446,7 @@ impl<'s> Parser<'s> { /// .collect::>() /// .as_slice(), /// &[ + /// ("", Start(Document, ..)), /// ("\n", Blankline), /// (">", Start(Blockquote, ..)), /// ("", Start(Paragraph, ..)), @@ -2352,6 +2455,7 @@ impl<'s> Parser<'s> { /// ("](multi\n> line)", End(Link { .. })), /// ("", End(Paragraph)), /// ("", End(Blockquote)), + /// ("", End(Document)), /// ], /// )); /// ``` @@ -2567,6 +2671,7 @@ impl<'s> Parser<'s> { } } block::Node::Container(c) => match c { + block::Container::Document => Container::Document, block::Container::Blockquote => Container::Blockquote, block::Container::Div { class } => Container::Div { class: class.into(), diff --git a/tests/parse_events.rs b/tests/parse_events.rs index 7c201f7..7a77a87 100644 --- a/tests/parse_events.rs +++ b/tests/parse_events.rs @@ -22,7 +22,11 @@ macro_rules! test_parse { .into_offset_iter() .map(|(e, r)| (e, &$src[r])) .collect::>(); - let expected = &[$($($token),*,)?]; + let expected = &[ + (Start(Document, Attributes::new()), ""), + $($($token),*,)? + (End(Document), ""), + ]; assert_eq!( actual, expected, From 35cfef909e84da5dbba6bfed692d9606e3ea1c60 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Thu, 30 Oct 2025 21:03:30 +0100 Subject: [PATCH 2/8] html: mut out: W => out: &mut W avoid infinite recursion for recursive calls: error[E0275]: overflow evaluating the requirement `&mut jotdown::WriteAdapter: std::fmt::Write` | = note: required for `&mut &mut &mut &mut &mut &mut &mut &mut &mut &mut &mut &mut &mut &mut &mut &mut &mut &mut ...` to implement `std::fmt::Write` --- src/html.rs | 64 ++++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/html.rs b/src/html.rs index d145a70..f6d4521 100644 --- a/src/html.rs +++ b/src/html.rs @@ -324,7 +324,7 @@ impl<'s, 'f> Writer<'s, 'f> { } } - fn block(&mut self, mut out: W, depth_change: isize) -> std::fmt::Result + fn block(&mut self, out: &mut W, depth_change: isize) -> std::fmt::Result where W: std::fmt::Write, { @@ -340,7 +340,7 @@ impl<'s, 'f> Writer<'s, 'f> { if depth_change < 0 { self.depth = next_depth; } - self.indent(&mut out)?; + self.indent(out)?; if depth_change > 0 { self.depth = next_depth; } @@ -348,7 +348,7 @@ impl<'s, 'f> Writer<'s, 'f> { Ok(()) } - fn indent(&self, mut out: W) -> std::fmt::Result + fn indent(&self, out: &mut W) -> std::fmt::Result where W: std::fmt::Write, { @@ -362,7 +362,7 @@ impl<'s, 'f> Writer<'s, 'f> { Ok(()) } - fn render_event(&mut self, e: Event<'s>, mut out: W) -> std::fmt::Result + fn render_event(&mut self, e: Event<'s>, out: &mut W) -> std::fmt::Result where W: std::fmt::Write, { @@ -395,7 +395,7 @@ impl<'s, 'f> Writer<'s, 'f> { match e { Event::Start(c, attrs) => { if c.is_block() { - self.block(&mut out, c.is_block_container().into())?; + self.block(out, c.is_block_container().into())?; } if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { return Ok(()); @@ -457,7 +457,7 @@ impl<'s, 'f> Writer<'s, 'f> { if matches!(ty, LinkType::Email) { out.write_str("mailto:")?; } - write_attr(dst, &mut out)?; + write_attr(dst, out)?; out.write_char('"')?; } } @@ -492,11 +492,11 @@ impl<'s, 'f> Writer<'s, 'f> { let mut class_written = false; for (a, v) in attrs.unique_pairs() { write!(out, r#" {}=""#, a)?; - v.parts().try_for_each(|part| write_attr(part, &mut out))?; + v.parts().try_for_each(|part| write_attr(part, out))?; match a { "class" => { class_written = true; - write_class(&c, true, &mut out)?; + write_class(&c, true, out)?; } "id" => id_written = true, _ => {} @@ -513,7 +513,7 @@ impl<'s, 'f> Writer<'s, 'f> { { if !id_written { out.write_str(r#" id=""#)?; - write_attr(id, &mut out)?; + write_attr(id, out)?; out.write_char('"')?; } } else if (matches!(c.clone(), Container::Div { class } if !class.is_empty()) @@ -528,7 +528,7 @@ impl<'s, 'f> Writer<'s, 'f> { && !class_written { out.write_str(r#" class=""#)?; - write_class(&c, false, &mut out)?; + write_class(&c, false, out)?; out.write_char('"')?; } @@ -549,7 +549,7 @@ impl<'s, 'f> Writer<'s, 'f> { out.write_str(">")?; } else { out.write_str(r#">"#)?; } } @@ -563,7 +563,7 @@ impl<'s, 'f> Writer<'s, 'f> { } Container::TaskListItem { checked } => { out.write_char('>')?; - self.block(&mut out, 0)?; + self.block(out, 0)?; if checked { out.write_str(r#""#)?; } else { @@ -575,7 +575,7 @@ impl<'s, 'f> Writer<'s, 'f> { } Event::End(c) => { if c.is_block_container() { - self.block(&mut out, -1)?; + self.block(out, -1)?; } if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { return Ok(()); @@ -622,7 +622,7 @@ impl<'s, 'f> Writer<'s, 'f> { if self.img_alt_text == 1 { if !src.is_empty() { out.write_str(r#"" src=""#)?; - write_attr(&src, &mut out)?; + write_attr(&src, out)?; } out.write_str(r#"">"#)?; } @@ -646,8 +646,8 @@ impl<'s, 'f> Writer<'s, 'f> { } } Event::Str(s) => match self.raw { - Raw::None if self.img_alt_text > 0 => write_attr(&s, &mut out)?, - Raw::None => write_text(&s, &mut out)?, + Raw::None if self.img_alt_text > 0 => write_attr(&s, out)?, + Raw::None => write_text(&s, out)?, Raw::Html => out.write_str(&s)?, Raw::Other => {} }, @@ -676,15 +676,15 @@ impl<'s, 'f> Writer<'s, 'f> { } Event::Softbreak => { out.write_char('\n')?; - self.indent(&mut out)?; + self.indent(out)?; } Event::Escape | Event::Blankline | Event::Attributes(..) => {} Event::ThematicBreak(attrs) => { - self.block(&mut out, 0)?; + self.block(out, 0)?; out.write_str("")?; @@ -695,20 +695,20 @@ impl<'s, 'f> Writer<'s, 'f> { Ok(()) } - fn render_epilogue(&mut self, mut out: W) -> std::fmt::Result + fn render_epilogue(&mut self, out: &mut W) -> std::fmt::Result where W: std::fmt::Write, { if self.footnotes.reference_encountered() { - self.block(&mut out, 0)?; + self.block(out, 0)?; out.write_str("
")?; - self.block(&mut out, 0)?; + self.block(out, 0)?; out.write_str("
")?; - self.block(&mut out, 0)?; + self.block(out, 0)?; out.write_str("
    ")?; while let Some((number, events)) = self.footnotes.next() { - self.block(&mut out, 0)?; + self.block(out, 0)?; write!(out, "
  1. ", number)?; let mut unclosed_para = false; @@ -720,13 +720,13 @@ impl<'s, 'f> Writer<'s, 'f> { // not a footnote, so no need to add href before para close out.write_str("

    ")?; } - self.render_event(e.clone(), &mut out)?; + self.render_event(e.clone(), out)?; unclosed_para = matches!(e, Event::End(Container::Paragraph { .. })) && !matches!(self.list_tightness.last(), Some(true)); } if !unclosed_para { // create a new paragraph - self.block(&mut out, 0)?; + self.block(out, 0)?; out.write_str("

    ")?; } write!( @@ -735,13 +735,13 @@ impl<'s, 'f> Writer<'s, 'f> { number, )?; - self.block(&mut out, 0)?; + self.block(out, 0)?; out.write_str("

  2. ")?; } - self.block(&mut out, 0)?; + self.block(out, 0)?; out.write_str("
")?; - self.block(&mut out, 0)?; + self.block(out, 0)?; out.write_str("
")?; } @@ -780,21 +780,21 @@ where Ok(()) } -fn write_text(s: &str, out: W) -> std::fmt::Result +fn write_text(s: &str, out: &mut W) -> std::fmt::Result where W: std::fmt::Write, { write_escape(s, false, out) } -fn write_attr(s: &str, out: W) -> std::fmt::Result +fn write_attr(s: &str, out: &mut W) -> std::fmt::Result where W: std::fmt::Write, { write_escape(s, true, out) } -fn write_escape(mut s: &str, escape_quotes: bool, mut out: W) -> std::fmt::Result +fn write_escape(mut s: &str, escape_quotes: bool, out: &mut W) -> std::fmt::Result where W: std::fmt::Write, { From 63b3fe298bde5e54e9f624d3c212a43db603c7f4 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 31 Oct 2025 18:19:47 +0100 Subject: [PATCH 3/8] html: render epilogue on End(Document) event --- src/html.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/html.rs b/src/html.rs index f6d4521..1b0382e 100644 --- a/src/html.rs +++ b/src/html.rs @@ -277,8 +277,7 @@ impl Render for Renderer { W: std::fmt::Write, { let mut w = Writer::new(&self.indent); - events.try_for_each(|e| w.render_event(e, &mut out))?; - w.render_epilogue(&mut out) + events.try_for_each(|e| w.render_event(e, &mut out)) } } @@ -581,7 +580,7 @@ impl<'s, 'f> Writer<'s, 'f> { return Ok(()); } match c { - Container::Document => return Ok(()), + Container::Document => return self.render_epilogue(out), Container::Blockquote => out.write_str("")?, Container::List { kind, .. } => { self.list_tightness.pop(); From 421873886b2b7c077a440430c910ed24ae5ff5e4 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 2 Nov 2025 00:03:30 +0100 Subject: [PATCH 4/8] lib Render: render one event at a time push/write single event at a time instead of an iterator of all events in a document --- src/html.rs | 87 ++++++++++++++++++++++----------------------------- src/lib.rs | 50 ++++++++++++++++++++--------- src/main.rs | 6 ++-- tests/html.rs | 2 +- 4 files changed, 77 insertions(+), 68 deletions(-) diff --git a/src/html.rs b/src/html.rs index 1b0382e..3cba240 100644 --- a/src/html.rs +++ b/src/html.rs @@ -13,8 +13,8 @@ use crate::SpanLinkType; /// Render events into a string. /// -/// This is a convenience function for using [`Renderer::push`] with fewer imports and without an -/// intermediate variable. +/// This is a convenience function for using [`Renderer::push_events`] with fewer imports and +/// without an intermediate variable. /// /// # Examples /// @@ -27,7 +27,7 @@ where I: Iterator>, { let mut s = String::new(); - Renderer::default().push(events, &mut s).unwrap(); + Renderer::default().push_events(events, &mut s).unwrap(); s } @@ -49,8 +49,8 @@ pub struct Indentation { /// let events = Parser::new(src); /// /// let mut html = String::new(); - /// let renderer = Renderer::indented(Indentation::default()); - /// renderer.push(events.clone(), &mut html).unwrap(); + /// let mut renderer = Renderer::indented(Indentation::default()); + /// renderer.push_events(events.clone(), &mut html).unwrap(); /// assert_eq!( /// html, /// concat!( @@ -69,11 +69,11 @@ pub struct Indentation { /// # let src = "> a\n"; /// # let events = Parser::new(src); /// # let mut html = String::new(); - /// let renderer = Renderer::indented(Indentation { + /// let mut renderer = Renderer::indented(Indentation { /// string: " ".to_string(), /// ..Indentation::default() /// }); - /// renderer.push(events.clone(), &mut html).unwrap(); + /// renderer.push_events(events.clone(), &mut html).unwrap(); /// assert_eq!( /// html, /// concat!( @@ -97,8 +97,8 @@ pub struct Indentation { /// let events = Parser::new(src); /// /// let mut html = String::new(); - /// let renderer = Renderer::indented(Indentation::default()); - /// renderer.push(events.clone(), &mut html).unwrap(); + /// let mut renderer = Renderer::indented(Indentation::default()); + /// renderer.push_events(events.clone(), &mut html).unwrap(); /// assert_eq!( /// html, /// concat!( @@ -117,11 +117,11 @@ pub struct Indentation { /// # let src = "> a\n"; /// # let events = Parser::new(src); /// # let mut html = String::new(); - /// let renderer = Renderer::indented(Indentation { + /// let mut renderer = Renderer::indented(Indentation { /// initial_level: 2, /// ..Indentation::default() /// }); - /// renderer.push(events.clone(), &mut html).unwrap(); + /// renderer.push_events(events.clone(), &mut html).unwrap(); /// assert_eq!( /// html, /// concat!( @@ -143,16 +143,7 @@ impl Default for Indentation { } } -/// [`Render`] implementor that writes HTML output. -/// -/// By default, block elements are placed on separate lines. To configure the formatting of the -/// output, see the [`Renderer::minified`] and [`Renderer::indented`] constructors. -#[derive(Clone)] -pub struct Renderer { - indent: Option, -} - -impl Renderer { +impl<'s> Renderer<'s> { /// Create a renderer that emits no whitespace between elements. /// /// # Examples @@ -168,15 +159,15 @@ impl Renderer { /// " - c\n", /// ); /// let mut actual = String::new(); - /// let renderer = Renderer::minified(); - /// renderer.push(Parser::new(src), &mut actual).unwrap(); + /// let mut renderer = Renderer::minified(); + /// renderer.push_events(Parser::new(src), &mut actual).unwrap(); /// let expected = /// "
  • a
    • b

    • c

"; /// assert_eq!(actual, expected); /// ``` #[must_use] pub fn minified() -> Self { - Self { indent: None } + Self::new(None) } /// Create a renderer that indents lines based on their block element depth. @@ -196,8 +187,8 @@ impl Renderer { /// " - c\n", /// ); /// let mut actual = String::new(); - /// let renderer = Renderer::indented(Indentation::default()); - /// renderer.push(Parser::new(src), &mut actual).unwrap(); + /// let mut renderer = Renderer::indented(Indentation::default()); + /// renderer.push_events(Parser::new(src), &mut actual).unwrap(); /// let expected = concat!( /// "
    \n", /// "\t
  • \n", @@ -217,13 +208,11 @@ impl Renderer { /// ``` #[must_use] pub fn indented(indent: Indentation) -> Self { - Self { - indent: Some(indent), - } + Self::new(Some(indent)) } } -impl Default for Renderer { +impl<'s> Default for Renderer<'s> { /// Place block elements on separate lines. /// /// This is the default behavior and matches the reference implementation. @@ -241,8 +230,8 @@ impl Default for Renderer { /// " - c\n", /// ); /// let mut actual = String::new(); - /// let renderer = Renderer::default(); - /// renderer.push(Parser::new(src), &mut actual).unwrap(); + /// let mut renderer = Renderer::default(); + /// renderer.push_events(Parser::new(src), &mut actual).unwrap(); /// let expected = concat!( /// "
      \n", /// "
    • \n", @@ -261,23 +250,19 @@ impl Default for Renderer { /// assert_eq!(actual, expected); /// ``` fn default() -> Self { - Self { - indent: Some(Indentation { - string: String::new(), - initial_level: 0, - }), - } + Self::new(Some(Indentation { + string: String::new(), + initial_level: 0, + })) } } -impl Render for Renderer { - fn push<'s, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result +impl<'s> Render<'s> for Renderer<'s> { + fn push_event(&mut self, event: Event<'s>, mut out: W) -> std::fmt::Result where - I: Iterator>, W: std::fmt::Write, { - let mut w = Writer::new(&self.indent); - events.try_for_each(|e| w.render_event(e, &mut out)) + self.render_event(event, &mut out) } } @@ -293,8 +278,12 @@ impl Default for Raw { } } -struct Writer<'s, 'f> { - indent: &'f Option, +/// [`Render`] implementor that writes HTML output. +/// +/// By default, block elements are placed on separate lines. To configure the formatting of the +/// output, see the [`Renderer::minified`] and [`Renderer::indented`] constructors. +pub struct Renderer<'s> { + indent: Option, depth: usize, raw: Raw, img_alt_text: usize, @@ -304,9 +293,9 @@ struct Writer<'s, 'f> { footnotes: Footnotes<'s>, } -impl<'s, 'f> Writer<'s, 'f> { - fn new(indent: &'f Option) -> Self { - let depth = if let Some(indent) = indent { +impl<'s> Renderer<'s> { + fn new(indent: Option) -> Self { + let depth = if let Some(indent) = &indent { indent.initial_level } else { 0 @@ -351,7 +340,7 @@ impl<'s, 'f> Writer<'s, 'f> { where W: std::fmt::Write, { - if let Some(indent) = self.indent { + if let Some(indent) = &self.indent { if !indent.string.is_empty() { for _ in 0..self.depth { out.write_str(&indent.string)?; diff --git a/src/lib.rs b/src/lib.rs index f769057..54ec6af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,8 +71,8 @@ type CowStr<'s> = std::borrow::Cow<'s, str>; /// # use jotdown::Render; /// # let events = std::iter::empty(); /// let mut output = String::new(); -/// let renderer = jotdown::html::Renderer::default(); -/// renderer.push(events, &mut output); +/// let mut renderer = jotdown::html::Renderer::default(); +/// renderer.push_events(events, &mut output); /// # } /// ``` /// @@ -84,24 +84,22 @@ type CowStr<'s> = std::borrow::Cow<'s, str>; /// # use jotdown::Render; /// # let events = std::iter::empty(); /// let mut out = std::io::BufWriter::new(std::io::stdout()); -/// let renderer = jotdown::html::Renderer::default(); -/// renderer.write(events, &mut out).unwrap(); +/// let mut renderer = jotdown::html::Renderer::default(); +/// renderer.write_events(events, &mut out).unwrap(); /// # } /// ``` -pub trait Render { - /// Push owned [`Event`]s to a unicode-accepting buffer or stream. - fn push<'s, I, W>(&self, events: I, out: W) -> std::fmt::Result +pub trait Render<'s> { + /// Push a single owned [`Event`]s to a unicode-accepting buffer or stream. + fn push_event(&mut self, event: Event<'s>, out: W) -> std::fmt::Result where - I: Iterator>, W: std::fmt::Write; - /// Write owned [`Event`]s to a byte sink, encoded as UTF-8. + /// Write a single owned [`Event`] to a byte sink, encoded as UTF-8. /// /// NOTE: This performs many small writes, so IO writes should be buffered with e.g. /// [`std::io::BufWriter`]. - fn write<'s, I, W>(&self, events: I, out: W) -> std::io::Result<()> + fn write_event(&mut self, event: Event<'s>, out: W) -> std::io::Result<()> where - I: Iterator>, W: std::io::Write, { struct WriteAdapter { @@ -123,10 +121,32 @@ pub trait Render { error: Ok(()), }; - self.push(events, &mut out).map_err(|_| match out.error { - Err(e) => e, - _ => std::io::Error::new(std::io::ErrorKind::Other, "formatter error"), - }) + self.push_event(event, &mut out) + .map_err(|_| match out.error { + Err(e) => e, + _ => std::io::Error::new(std::io::ErrorKind::Other, "formatter error"), + }) + } + + /// Push a single owned [`Event`]s to a unicode-accepting buffer or stream. + fn push_events(&mut self, mut events: I, mut out: W) -> std::fmt::Result + where + I: Iterator>, + W: std::fmt::Write, + { + events.try_for_each(|e| self.push_event(e, &mut out)) + } + + /// Write owned [`Event`]s to a byte sink, encoded as UTF-8. + /// + /// NOTE: This performs many small writes, so IO writes should be buffered with e.g. + /// [`std::io::BufWriter`]. + fn write_events(&mut self, mut events: I, mut out: W) -> std::io::Result<()> + where + I: Iterator>, + W: std::io::Write, + { + events.try_for_each(|e| self.write_event(e, &mut out)) } } diff --git a/src/main.rs b/src/main.rs index 3d382ff..d66a641 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,7 +94,7 @@ fn run() -> Result<(), std::io::Error> { }; let parser = jotdown::Parser::new(&content); - let renderer = if app.minified { + let mut renderer = if app.minified { jotdown::html::Renderer::minified() } else { jotdown::html::Renderer::indented(jotdown::html::Indentation { @@ -104,8 +104,8 @@ fn run() -> Result<(), std::io::Error> { }; match app.output { - Some(path) => renderer.write(parser, std::fs::File::create(path)?)?, - None => renderer.write(parser, std::io::BufWriter::new(std::io::stdout()))?, + Some(path) => renderer.write_events(parser, std::fs::File::create(path)?)?, + None => renderer.write_events(parser, std::io::BufWriter::new(std::io::stdout()))?, } Ok(()) diff --git a/tests/html.rs b/tests/html.rs index e808fb2..8066d45 100644 --- a/tests/html.rs +++ b/tests/html.rs @@ -8,7 +8,7 @@ macro_rules! test_html { $(renderer = jotdown::html::Renderer::indented($indent);)? let mut actual = String::new(); renderer - .push(jotdown::Parser::new($src), &mut actual) + .push_events(jotdown::Parser::new($src), &mut actual) .unwrap(); assert_eq!(actual, $expected); }; From 9f00f9466cf21bdf22785d03289e283c018631b4 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Tue, 4 Nov 2025 00:06:02 +0100 Subject: [PATCH 5/8] lib: add Filter trait --- src/lib.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 54ec6af..c01e535 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,7 @@ type CowStr<'s> = std::borrow::Cow<'s, str>; /// renderer.write_events(events, &mut out).unwrap(); /// # } /// ``` -pub trait Render<'s> { +pub trait Render<'s>: Sized { /// Push a single owned [`Event`]s to a unicode-accepting buffer or stream. fn push_event(&mut self, event: Event<'s>, out: W) -> std::fmt::Result where @@ -148,6 +148,38 @@ pub trait Render<'s> { { events.try_for_each(|e| self.write_event(e, &mut out)) } + + fn with_filter(self, filter: F) -> FilteredRenderer { + FilteredRenderer { + filter, + renderer: self, + } + } +} + +pub trait Filter<'s> { + fn push_event

      (&mut self, event: Event<'s>, push: &mut P) -> std::fmt::Result + where + P: FnMut(Event<'s>) -> std::fmt::Result; +} + +pub struct FilteredRenderer { + filter: F, + renderer: R, +} + +impl<'s, F, R> Render<'s> for FilteredRenderer +where + F: Filter<'s>, + R: Render<'s>, +{ + fn push_event(&mut self, event: Event<'s>, mut out: W) -> std::fmt::Result + where + W: std::fmt::Write, + { + self.filter + .push_event(event, &mut |e| self.renderer.push_event(e, &mut out)) + } } // XXX why is this not a blanket implementation? From cdb2a71390ad831fec7467b3c8be3a2991c113e1 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Mon, 17 Nov 2025 17:31:37 +0100 Subject: [PATCH 6/8] fixup! lib Render: render one event at a time --- tests/afl/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/afl/src/lib.rs b/tests/afl/src/lib.rs index 591f46c..feb5b7a 100644 --- a/tests/afl/src/lib.rs +++ b/tests/afl/src/lib.rs @@ -63,7 +63,7 @@ pub fn html(data: &[u8]) { let p = jotdown::Parser::new(s); let mut html = "\n".to_string(); jotdown::html::Renderer::default() - .push(p, &mut html) + .push_events(p, &mut html) .unwrap(); validate_html(&html); } From df031626b61da7a1beaf911012e78ec351f89321 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Mon, 17 Nov 2025 17:34:09 +0100 Subject: [PATCH 7/8] afl: fix lifetime warning --- tests/afl/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/afl/src/lib.rs b/tests/afl/src/lib.rs index feb5b7a..8d2ed6c 100644 --- a/tests/afl/src/lib.rs +++ b/tests/afl/src/lib.rs @@ -137,7 +137,7 @@ impl<'a> tree_builder::TreeSink for Dom<'a> { x == y } - fn elem_name(&self, i: &usize) -> html5ever::ExpandedName { + fn elem_name(&self, i: &usize) -> html5ever::ExpandedName<'_> { self.names[i - 1].expanded() } From e46c0804b621deb30378f98c3907a4eb0c1f36bd Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Mon, 17 Nov 2025 17:44:39 +0100 Subject: [PATCH 8/8] fixup! lib Render: render one event at a time --- tests/html-ref/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/html-ref/lib.rs b/tests/html-ref/lib.rs index fabada5..e7557d2 100644 --- a/tests/html-ref/lib.rs +++ b/tests/html-ref/lib.rs @@ -10,7 +10,7 @@ macro_rules! compare { let p = jotdown::Parser::new(src); let mut actual = String::new(); jotdown::html::Renderer::default() - .push(p, &mut actual) + .push_events(p, &mut actual) .unwrap(); assert_eq!(actual, expected, "\n{}", { use std::io::Write;