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 = "
\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, "", 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("")?;
}
- 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 =
/// "";
/// 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;