From 336927faef090549b8aee562056f3c7d9569876d Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 17 Mar 2023 18:48:17 +0100 Subject: [PATCH 1/8] html: avoid peek of next event Try to make rendering of each event independent. The only case where we need to peek is when a backref link should be added to the last paragraph within a footnote. Before, when exiting a paragraph, we would peek and add the link before emitting the close tag if the next event is a the footnote end. Now, the paragraph end event skips emitting a paragraph close tag if it is within a footnote. The next event will then always close the paragraph, and if it is a footnote end, it will add the backref link before closing. --- src/html.rs | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/html.rs b/src/html.rs index 1b825f8..c0f7864 100644 --- a/src/html.rs +++ b/src/html.rs @@ -73,8 +73,8 @@ struct Writer<'s, I: Iterator>, W> { list_tightness: Vec, encountered_footnote: bool, footnote_number: Option, - footnote_backlink_written: bool, first_line: bool, + close_para: bool, } impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { @@ -87,13 +87,22 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { list_tightness: Vec::new(), encountered_footnote: false, footnote_number: None, - footnote_backlink_written: false, first_line: true, + close_para: false, } } fn write(&mut self) -> std::fmt::Result { while let Some(e) = self.events.next() { + let close_para = self.close_para; + if close_para { + self.close_para = false; + if !matches!(&e, Event::End(Container::Footnote { .. })) { + // no need to add href before para close + self.out.write_str("

")?; + } + } + match e { Event::Start(c, attrs) => { if c.is_block() && !self.first_line { @@ -143,7 +152,6 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { .write_str("
\n
\n
    \n")?; } write!(self.out, "
  1. ", number)?; - self.footnote_backlink_written = false; continue; } Container::Table => self.out.write_str(">, W: std::fmt::Write> Writer<'s, I, W> { Container::DescriptionList => self.out.write_str("")?, Container::DescriptionDetails => self.out.write_str("")?, Container::Footnote { number, .. } => { - if !self.footnote_backlink_written { - write!( - self.out, - "\n

    ↩︎︎

    ", - number, - )?; + if !close_para { + // create a new paragraph + self.out.write_str("\n

    ")?; } + write!( + self.out, + r##"↩︎︎

    "##, + number, + )?; self.out.write_str("\n
  2. ")?; self.footnote_number = None; } @@ -347,20 +357,11 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { if matches!(self.list_tightness.last(), Some(true)) { continue; } - if let Some(num) = self.footnote_number { - if matches!( - self.events.peek(), - Some(Event::End(Container::Footnote { .. })) - ) { - write!( - self.out, - r##"↩︎︎"##, - num - )?; - self.footnote_backlink_written = true; - } + if self.footnote_number.is_none() { + self.out.write_str("

    ")?; + } else { + self.close_para = true; } - self.out.write_str("

    ")?; } Container::Heading { level, .. } => write!(self.out, "", level)?, Container::TableCell { head: false, .. } => self.out.write_str("")?, From f5724fcc9c9eed7ff38ff5cdd90cd0f640931143 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 17 Mar 2023 19:05:56 +0100 Subject: [PATCH 2/8] html: rm FilteredEvents no longer useful as no peeking is needed, use simple early exit instead --- src/html.rs | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/html.rs b/src/html.rs index c0f7864..51e21d5 100644 --- a/src/html.rs +++ b/src/html.rs @@ -49,24 +49,8 @@ enum Raw { Other, } -struct FilteredEvents { - events: I, -} - -impl<'s, I: Iterator>> Iterator for FilteredEvents { - type Item = Event<'s>; - - fn next(&mut self) -> Option { - let mut ev = self.events.next(); - while matches!(ev, Some(Event::Blankline | Event::Escape)) { - ev = self.events.next(); - } - ev - } -} - struct Writer<'s, I: Iterator>, W> { - events: std::iter::Peekable>, + events: I, out: W, raw: Raw, img_alt_text: usize, @@ -80,7 +64,7 @@ struct Writer<'s, I: Iterator>, W> { impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { fn new(events: I, out: W) -> Self { Self { - events: FilteredEvents { events }.peekable(), + events, out, raw: Raw::None, img_alt_text: 0, @@ -94,6 +78,10 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { fn write(&mut self) -> std::fmt::Result { while let Some(e) = self.events.next() { + if matches!(&e, Event::Blankline | Event::Escape) { + continue; + } + let close_para = self.close_para; if close_para { self.close_para = false; From 3d1b5f2115fae0ad929a39ed4b44cc4bd8173497 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 17 Mar 2023 19:13:00 +0100 Subject: [PATCH 3/8] html: rm events/out from Writer separate input/output from rendering state --- src/html.rs | 328 +++++++++++++++++++++++++++------------------------- 1 file changed, 168 insertions(+), 160 deletions(-) diff --git a/src/html.rs b/src/html.rs index 51e21d5..f78173c 100644 --- a/src/html.rs +++ b/src/html.rs @@ -39,7 +39,7 @@ impl Render for Renderer { events: I, out: W, ) -> std::fmt::Result { - Writer::new(events, out).write() + Writer::default().write(events, out) } } @@ -49,9 +49,7 @@ enum Raw { Other, } -struct Writer<'s, I: Iterator>, W> { - events: I, - out: W, +struct Writer { raw: Raw, img_alt_text: usize, list_tightness: Vec, @@ -61,11 +59,9 @@ struct Writer<'s, I: Iterator>, W> { close_para: bool, } -impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { - fn new(events: I, out: W) -> Self { +impl Default for Writer { + fn default() -> Self { Self { - events, - out, raw: Raw::None, img_alt_text: 0, list_tightness: Vec::new(), @@ -75,9 +71,15 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { close_para: false, } } +} - fn write(&mut self) -> std::fmt::Result { - while let Some(e) = self.events.next() { +impl Writer { + fn write<'s>( + &mut self, + events: impl Iterator>, + mut out: impl std::fmt::Write, + ) -> std::fmt::Result { + for e in events { if matches!(&e, Event::Blankline | Event::Escape) { continue; } @@ -87,32 +89,30 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { self.close_para = false; if !matches!(&e, Event::End(Container::Footnote { .. })) { // no need to add href before para close - self.out.write_str("

    ")?; + out.write_str("

    ")?; } } match e { Event::Start(c, attrs) => { if c.is_block() && !self.first_line { - self.out.write_char('\n')?; + out.write_char('\n')?; } if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { continue; } match &c { - Container::Blockquote => self.out.write_str(" out.write_str(" { self.list_tightness.push(*tight); match kind { - ListKind::Unordered | ListKind::Task => { - self.out.write_str(" out.write_str(" { - self.out.write_str(" 1 { - write!(self.out, r#" start="{}""#, start)?; + write!(out, r#" start="{}""#, start)?; } if let Some(ty) = match numbering { Decimal => None, @@ -121,65 +121,64 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { RomanLower => Some('i'), RomanUpper => Some('I'), } { - write!(self.out, r#" type="{}""#, ty)?; + write!(out, r#" type="{}""#, ty)?; } } } } Container::ListItem | Container::TaskListItem { .. } => { - self.out.write_str(" self.out.write_str(" self.out.write_str(" out.write_str(" out.write_str(" { assert!(self.footnote_number.is_none()); self.footnote_number = Some((*number).try_into().unwrap()); if !self.encountered_footnote { self.encountered_footnote = true; - self.out - .write_str("
    \n
    \n
      \n")?; + out.write_str("
      \n
      \n
        \n")?; } - write!(self.out, "
      1. ", number)?; + write!(out, "
      2. ", number)?; continue; } - Container::Table => self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" { if matches!(self.list_tightness.last(), Some(true)) { continue; } - self.out.write_str(" write!(self.out, " self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" write!(out, " out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" { if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) { - self.out.write_str(" { self.img_alt_text += 1; if self.img_alt_text == 1 { - self.out.write_str(" self.out.write_str(" out.write_str(" { self.raw = if format == &"html" { Raw::Html @@ -188,19 +187,19 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { }; continue; } - Container::Subscript => self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(">, W: std::fmt::Write> Writer<'s, I, W> { | Container::Section { id } = &c { if !attrs.iter().any(|(a, _)| a == "id") { - self.out.write_str(r#" id=""#)?; - self.write_attr(id)?; - self.out.write_char('"')?; + out.write_str(r#" id=""#)?; + write_attr(id, &mut out)?; + out.write_char('"')?; } } @@ -229,7 +228,7 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { | Container::TaskListItem { .. } ) { - self.out.write_str(r#" class=""#)?; + out.write_str(r#" class=""#)?; let mut first_written = false; if let Some(cls) = match c { Container::List { @@ -243,7 +242,7 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { _ => None, } { first_written = true; - self.out.write_str(cls)?; + out.write_str(cls)?; } for cls in attrs .iter() @@ -251,19 +250,20 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { .map(|(_, cls)| cls) { if first_written { - self.out.write_char(' ')?; + out.write_char(' ')?; } first_written = true; - cls.parts().try_for_each(|part| self.write_attr(part))?; + cls.parts() + .try_for_each(|part| write_attr(part, &mut out))?; } // div class goes after classes from attrs if let Container::Div { class: Some(cls) } = c { if first_written { - self.out.write_char(' ')?; + out.write_char(' ')?; } - self.out.write_str(cls)?; + out.write_str(cls)?; } - self.out.write_char('"')?; + out.write_char('"')?; } match c { @@ -276,102 +276,101 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { Alignment::Center => "center", Alignment::Right => "right", }; - write!(self.out, r#" style="text-align: {};">"#, a)?; + write!(out, r#" style="text-align: {};">"#, a)?; } Container::CodeBlock { lang } => { if let Some(l) = lang { - self.out.write_str(r#">"#)?; + out.write_str(r#">"#)?; } else { - self.out.write_str(">")?; + out.write_str(">")?; } } Container::Image(..) => { if self.img_alt_text == 1 { - self.out.write_str(r#" alt=""#)?; + out.write_str(r#" alt=""#)?; } } Container::Math { display } => { - self.out - .write_str(if display { r#">\["# } else { r#">\("# })?; + out.write_str(if display { r#">\["# } else { r#">\("# })?; } - _ => self.out.write_char('>')?, + _ => out.write_char('>')?, } } Event::End(c) => { if c.is_block_container() && !matches!(c, Container::Footnote { .. }) { - self.out.write_char('\n')?; + out.write_char('\n')?; } if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { continue; } match c { - Container::Blockquote => self.out.write_str("")?, + Container::Blockquote => out.write_str("")?, Container::List { kind: ListKind::Unordered | ListKind::Task, .. } => { self.list_tightness.pop(); - self.out.write_str("")?; + out.write_str("")?; } Container::List { kind: ListKind::Ordered { .. }, .. - } => self.out.write_str("
      ")?, + } => out.write_str("
    ")?, Container::ListItem | Container::TaskListItem { .. } => { - self.out.write_str("")?; + out.write_str("")?; } - Container::DescriptionList => self.out.write_str("")?, - Container::DescriptionDetails => self.out.write_str("")?, + Container::DescriptionList => out.write_str("")?, + Container::DescriptionDetails => out.write_str("")?, Container::Footnote { number, .. } => { if !close_para { // create a new paragraph - self.out.write_str("\n

    ")?; + out.write_str("\n

    ")?; } write!( - self.out, + out, r##"↩︎︎

    "##, number, )?; - self.out.write_str("\n")?; + out.write_str("\n")?; self.footnote_number = None; } - Container::Table => self.out.write_str("")?, - Container::TableRow { .. } => self.out.write_str("")?, - Container::Section { .. } => self.out.write_str("
    ")?, - Container::Div { .. } => self.out.write_str("")?, + Container::Table => out.write_str("")?, + Container::TableRow { .. } => out.write_str("")?, + Container::Section { .. } => out.write_str("
")?, + Container::Div { .. } => out.write_str("")?, Container::Paragraph => { if matches!(self.list_tightness.last(), Some(true)) { continue; } if self.footnote_number.is_none() { - self.out.write_str("

")?; + out.write_str("

")?; } else { self.close_para = true; } } - Container::Heading { level, .. } => write!(self.out, "", level)?, - Container::TableCell { head: false, .. } => self.out.write_str("")?, - Container::TableCell { head: true, .. } => self.out.write_str("")?, - Container::Caption => self.out.write_str("")?, - Container::DescriptionTerm => self.out.write_str("")?, - Container::CodeBlock { .. } => self.out.write_str("
")?, - Container::Span => self.out.write_str("")?, - Container::Link(..) => self.out.write_str("")?, + Container::Heading { level, .. } => write!(out, "", level)?, + Container::TableCell { head: false, .. } => out.write_str("")?, + Container::TableCell { head: true, .. } => out.write_str("")?, + Container::Caption => out.write_str("")?, + Container::DescriptionTerm => out.write_str("")?, + Container::CodeBlock { .. } => out.write_str("
")?, + Container::Span => out.write_str("")?, + Container::Link(..) => out.write_str("")?, Container::Image(src, ..) => { if self.img_alt_text == 1 { if !src.is_empty() { - self.out.write_str(r#"" src=""#)?; - self.write_attr(&src)?; + out.write_str(r#"" src=""#)?; + write_attr(&src, &mut out)?; } - self.out.write_str(r#"">"#)?; + out.write_str(r#"">"#)?; } self.img_alt_text -= 1; } - Container::Verbatim => self.out.write_str("
")?, + Container::Verbatim => out.write_str("
")?, Container::Math { display } => { - self.out.write_str(if display { + out.write_str(if display { r#"\]"# } else { r#"\)"# @@ -380,88 +379,97 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { Container::RawBlock { .. } | Container::RawInline { .. } => { self.raw = Raw::None; } - Container::Subscript => self.out.write_str("")?, - Container::Superscript => self.out.write_str("")?, - Container::Insert => self.out.write_str("")?, - Container::Delete => self.out.write_str("")?, - Container::Strong => self.out.write_str("")?, - Container::Emphasis => self.out.write_str("")?, - Container::Mark => self.out.write_str("")?, + Container::Subscript => out.write_str("")?, + Container::Superscript => out.write_str("")?, + Container::Insert => out.write_str("")?, + Container::Delete => out.write_str("")?, + Container::Strong => out.write_str("")?, + Container::Emphasis => out.write_str("")?, + Container::Mark => out.write_str("")?, } } Event::Str(s) => match self.raw { - Raw::None if self.img_alt_text > 0 => self.write_attr(&s)?, - Raw::None => self.write_text(&s)?, - Raw::Html => self.out.write_str(&s)?, + Raw::None if self.img_alt_text > 0 => write_attr(&s, &mut out)?, + Raw::None => write_text(&s, &mut out)?, + Raw::Html => out.write_str(&s)?, Raw::Other => {} }, Event::FootnoteReference(_tag, number) => { if self.img_alt_text == 0 { write!( - self.out, + out, r##"{}"##, number, number, number )?; } } - Event::Symbol(sym) => write!(self.out, ":{}:", sym)?, - Event::LeftSingleQuote => self.out.write_str("‘")?, - Event::RightSingleQuote => self.out.write_str("’")?, - Event::LeftDoubleQuote => self.out.write_str("“")?, - Event::RightDoubleQuote => self.out.write_str("”")?, - Event::Ellipsis => self.out.write_str("…")?, - Event::EnDash => self.out.write_str("–")?, - Event::EmDash => self.out.write_str("—")?, - Event::NonBreakingSpace => self.out.write_str(" ")?, - Event::Hardbreak => self.out.write_str("
\n")?, - Event::Softbreak => self.out.write_char('\n')?, + Event::Symbol(sym) => write!(out, ":{}:", sym)?, + Event::LeftSingleQuote => out.write_str("‘")?, + Event::RightSingleQuote => out.write_str("’")?, + Event::LeftDoubleQuote => out.write_str("“")?, + Event::RightDoubleQuote => out.write_str("”")?, + Event::Ellipsis => out.write_str("…")?, + Event::EnDash => out.write_str("–")?, + Event::EmDash => out.write_str("—")?, + Event::NonBreakingSpace => out.write_str(" ")?, + Event::Hardbreak => out.write_str("
\n")?, + Event::Softbreak => out.write_char('\n')?, Event::Escape | Event::Blankline => unreachable!("filtered out"), Event::ThematicBreak(attrs) => { - self.out.write_str("\n")?; + out.write_str(">")?; } } self.first_line = false; } if self.encountered_footnote { - self.out.write_str("\n\n")?; + out.write_str("\n\n")?; } - self.out.write_char('\n')?; + out.write_char('\n')?; Ok(()) } - - fn write_escape(&mut self, mut s: &str, escape_quotes: bool) -> std::fmt::Result { - let mut ent = ""; - while let Some(i) = s.find(|c| { - match c { - '<' => Some("<"), - '>' => Some(">"), - '&' => Some("&"), - '"' if escape_quotes => Some("""), - _ => None, - } - .map_or(false, |s| { - ent = s; - true - }) - }) { - self.out.write_str(&s[..i])?; - self.out.write_str(ent)?; - s = &s[i + 1..]; - } - self.out.write_str(s) - } - - fn write_text(&mut self, s: &str) -> std::fmt::Result { - self.write_escape(s, false) - } - - fn write_attr(&mut self, s: &str) -> std::fmt::Result { - self.write_escape(s, true) - } +} + +fn write_text(s: &str, out: W) -> std::fmt::Result +where + W: std::fmt::Write, +{ + write_escape(s, false, out) +} + +fn write_attr(s: &str, out: 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 +where + W: std::fmt::Write, +{ + let mut ent = ""; + while let Some(i) = s.find(|c| { + match c { + '<' => Some("<"), + '>' => Some(">"), + '&' => Some("&"), + '"' if escape_quotes => Some("""), + _ => None, + } + .map_or(false, |s| { + ent = s; + true + }) + }) { + out.write_str(&s[..i])?; + out.write_str(ent)?; + s = &s[i + 1..]; + } + out.write_str(s) } From 8eafdf073b18e8ee991dbc13c0ee6bcda6fed2fb Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 19 Mar 2023 18:35:41 +0100 Subject: [PATCH 4/8] html: extract Writer::render_{event, epilogue} --- src/html.rs | 673 +++++++++++++++++++++++++++------------------------- 1 file changed, 344 insertions(+), 329 deletions(-) diff --git a/src/html.rs b/src/html.rs index f78173c..0e29c45 100644 --- a/src/html.rs +++ b/src/html.rs @@ -74,363 +74,378 @@ impl Default for Writer { } impl Writer { - fn write<'s>( - &mut self, - events: impl Iterator>, - mut out: impl std::fmt::Write, - ) -> std::fmt::Result { - for e in events { - if matches!(&e, Event::Blankline | Event::Escape) { - continue; - } + fn write<'s, I, W>(&mut self, mut events: I, mut out: W) -> std::fmt::Result + where + I: Iterator>, + W: std::fmt::Write, + { + events.try_for_each(|e| self.render_event(&e, &mut out))?; + self.render_epilogue(&mut out) + } - let close_para = self.close_para; - if close_para { - self.close_para = false; - if !matches!(&e, Event::End(Container::Footnote { .. })) { - // no need to add href before para close - out.write_str("

")?; + fn render_event<'s, W>(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result + where + W: std::fmt::Write, + { + if matches!(&e, Event::Blankline | Event::Escape) { + return Ok(()); + } + + let close_para = self.close_para; + if close_para { + self.close_para = false; + if !matches!(&e, Event::End(Container::Footnote { .. })) { + // no need to add href before para close + out.write_str("

")?; + } + } + + match e { + Event::Start(c, attrs) => { + if c.is_block() && !self.first_line { + out.write_char('\n')?; } - } - - match e { - Event::Start(c, attrs) => { - if c.is_block() && !self.first_line { - out.write_char('\n')?; - } - if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { - continue; - } - match &c { - Container::Blockquote => out.write_str(" { - self.list_tightness.push(*tight); - match kind { - ListKind::Unordered | ListKind::Task => out.write_str(" { - out.write_str(" 1 { - write!(out, r#" start="{}""#, start)?; - } - if let Some(ty) = match numbering { - Decimal => None, - AlphaLower => Some('a'), - AlphaUpper => Some('A'), - RomanLower => Some('i'), - RomanUpper => Some('I'), - } { - write!(out, r#" type="{}""#, ty)?; - } + if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { + return Ok(()); + } + match &c { + Container::Blockquote => out.write_str(" { + self.list_tightness.push(*tight); + match kind { + ListKind::Unordered | ListKind::Task => out.write_str(" { + out.write_str(" 1 { + write!(out, r#" start="{}""#, start)?; + } + if let Some(ty) = match numbering { + Decimal => None, + AlphaLower => Some('a'), + AlphaUpper => Some('A'), + RomanLower => Some('i'), + RomanUpper => Some('I'), + } { + write!(out, r#" type="{}""#, ty)?; } } } - Container::ListItem | Container::TaskListItem { .. } => { - out.write_str(" out.write_str(" out.write_str(" { - assert!(self.footnote_number.is_none()); - self.footnote_number = Some((*number).try_into().unwrap()); - if !self.encountered_footnote { - self.encountered_footnote = true; - out.write_str("
\n
\n
    \n")?; - } - write!(out, "
  1. ", number)?; - continue; - } - Container::Table => out.write_str(" out.write_str(" out.write_str(" out.write_str(" { - if matches!(self.list_tightness.last(), Some(true)) { - continue; - } - out.write_str(" write!(out, " out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" { - if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) { - out.write_str(" { - self.img_alt_text += 1; - if self.img_alt_text == 1 { - out.write_str(" out.write_str(" { - self.raw = if format == &"html" { - Raw::Html - } else { - Raw::Other - }; - continue; - } - Container::Subscript => out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" { + out.write_str(" out.write_str(" out.write_str(" { + assert!(self.footnote_number.is_none()); + self.footnote_number = Some((*number).try_into().unwrap()); + if !self.encountered_footnote { + self.encountered_footnote = true; + out.write_str("
    \n
    \n
      \n")?; + } + write!(out, "
    1. ", number)?; + return Ok(()); } - | Container::Section { id } = &c - { - if !attrs.iter().any(|(a, _)| a == "id") { - out.write_str(r#" id=""#)?; - write_attr(id, &mut out)?; + Container::Table => out.write_str(" out.write_str(" out.write_str(" out.write_str(" { + if matches!(self.list_tightness.last(), Some(true)) { + return Ok(()); + } + out.write_str(" write!(out, " out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" { + if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) { + out.write_str(" { + self.img_alt_text += 1; + if self.img_alt_text == 1 { + out.write_str(" out.write_str(" { + self.raw = if format == &"html" { + Raw::Html + } else { + Raw::Other + }; + return Ok(()); + } + Container::Subscript => out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" Some("task-list"), - Container::TaskListItem { checked: false } => Some("unchecked"), - Container::TaskListItem { checked: true } => Some("checked"), - Container::Math { display: false } => Some("math inline"), - Container::Math { display: true } => Some("math display"), - _ => None, - } { - first_written = true; - out.write_str(cls)?; - } - for cls in attrs - .iter() - .filter(|(a, _)| a == &"class") - .map(|(_, cls)| cls) - { - if first_written { - out.write_char(' ')?; } - first_written = true; - cls.parts() - .try_for_each(|part| write_attr(part, &mut out))?; - } - // div class goes after classes from attrs - if let Container::Div { class: Some(cls) } = c { - if first_written { - out.write_char(' ')?; - } - out.write_str(cls)?; - } - out.write_char('"')?; + | Container::TaskListItem { .. } + ) + { + out.write_str(r#" class=""#)?; + let mut first_written = false; + if let Some(cls) = match c { + Container::List { + kind: ListKind::Task, + .. + } => Some("task-list"), + Container::TaskListItem { checked: false } => Some("unchecked"), + Container::TaskListItem { checked: true } => Some("checked"), + Container::Math { display: false } => Some("math inline"), + Container::Math { display: true } => Some("math display"), + _ => None, + } { + first_written = true; + out.write_str(cls)?; } + for cls in attrs + .iter() + .filter(|(a, _)| a == &"class") + .map(|(_, cls)| cls) + { + if first_written { + out.write_char(' ')?; + } + first_written = true; + cls.parts() + .try_for_each(|part| write_attr(part, &mut out))?; + } + // div class goes after classes from attrs + if let Container::Div { class: Some(cls) } = c { + if first_written { + out.write_char(' ')?; + } + out.write_str(cls)?; + } + out.write_char('"')?; + } - match c { - Container::TableCell { alignment, .. } - if !matches!(alignment, Alignment::Unspecified) => - { - let a = match alignment { - Alignment::Unspecified => unreachable!(), - Alignment::Left => "left", - Alignment::Center => "center", - Alignment::Right => "right", - }; - write!(out, r#" style="text-align: {};">"#, a)?; - } - Container::CodeBlock { lang } => { - if let Some(l) = lang { - out.write_str(r#">"#)?; - } else { - out.write_str(">")?; - } - } - Container::Image(..) => { - if self.img_alt_text == 1 { - out.write_str(r#" alt=""#)?; - } - } - Container::Math { display } => { - out.write_str(if display { r#">\["# } else { r#">\("# })?; - } - _ => out.write_char('>')?, + match c { + Container::TableCell { alignment, .. } + if !matches!(alignment, Alignment::Unspecified) => + { + let a = match alignment { + Alignment::Unspecified => unreachable!(), + Alignment::Left => "left", + Alignment::Center => "center", + Alignment::Right => "right", + }; + write!(out, r#" style="text-align: {};">"#, a)?; } - } - Event::End(c) => { - if c.is_block_container() && !matches!(c, Container::Footnote { .. }) { - out.write_char('\n')?; + Container::CodeBlock { lang } => { + if let Some(l) = lang { + out.write_str(r#">"#)?; + } else { + out.write_str(">")?; + } } - if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { - continue; + Container::Image(..) => { + if self.img_alt_text == 1 { + out.write_str(r#" alt=""#)?; + } } - match c { - Container::Blockquote => out.write_str("")?, - Container::List { - kind: ListKind::Unordered | ListKind::Task, - .. - } => { - self.list_tightness.pop(); - out.write_str("")?; - } - Container::List { - kind: ListKind::Ordered { .. }, - .. - } => out.write_str("
    ")?, - Container::ListItem | Container::TaskListItem { .. } => { - out.write_str("
  2. ")?; - } - Container::DescriptionList => out.write_str("")?, - Container::DescriptionDetails => out.write_str("")?, - Container::Footnote { number, .. } => { - if !close_para { - // create a new paragraph - out.write_str("\n

    ")?; - } - write!( - out, - r##"↩︎︎

    "##, - number, - )?; - out.write_str("\n")?; - self.footnote_number = None; - } - Container::Table => out.write_str("")?, - Container::TableRow { .. } => out.write_str("")?, - Container::Section { .. } => out.write_str("
")?, - Container::Div { .. } => out.write_str("")?, - Container::Paragraph => { - if matches!(self.list_tightness.last(), Some(true)) { - continue; - } - if self.footnote_number.is_none() { - out.write_str("

")?; - } else { - self.close_para = true; - } - } - Container::Heading { level, .. } => write!(out, "", level)?, - Container::TableCell { head: false, .. } => out.write_str("")?, - Container::TableCell { head: true, .. } => out.write_str("")?, - Container::Caption => out.write_str("")?, - Container::DescriptionTerm => out.write_str("")?, - Container::CodeBlock { .. } => out.write_str("
")?, - Container::Span => out.write_str("")?, - Container::Link(..) => out.write_str("")?, - Container::Image(src, ..) => { - if self.img_alt_text == 1 { - if !src.is_empty() { - out.write_str(r#"" src=""#)?; - write_attr(&src, &mut out)?; - } - out.write_str(r#"">"#)?; - } - self.img_alt_text -= 1; - } - Container::Verbatim => out.write_str("
")?, - Container::Math { display } => { - out.write_str(if display { - r#"\]"# - } else { - r#"\)"# - })?; - } - Container::RawBlock { .. } | Container::RawInline { .. } => { - self.raw = Raw::None; - } - Container::Subscript => out.write_str("")?, - Container::Superscript => out.write_str("")?, - Container::Insert => out.write_str("")?, - Container::Delete => out.write_str("")?, - Container::Strong => out.write_str("")?, - Container::Emphasis => out.write_str("")?, - Container::Mark => out.write_str("")?, + Container::Math { display } => { + out.write_str(if *display { r#">\["# } else { r#">\("# })?; } - } - 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::Html => out.write_str(&s)?, - Raw::Other => {} - }, - Event::FootnoteReference(_tag, number) => { - if self.img_alt_text == 0 { - write!( - out, - r##"{}"##, - number, number, number - )?; - } - } - Event::Symbol(sym) => write!(out, ":{}:", sym)?, - Event::LeftSingleQuote => out.write_str("‘")?, - Event::RightSingleQuote => out.write_str("’")?, - Event::LeftDoubleQuote => out.write_str("“")?, - Event::RightDoubleQuote => out.write_str("”")?, - Event::Ellipsis => out.write_str("…")?, - Event::EnDash => out.write_str("–")?, - Event::EmDash => out.write_str("—")?, - Event::NonBreakingSpace => out.write_str(" ")?, - Event::Hardbreak => out.write_str("
\n")?, - Event::Softbreak => out.write_char('\n')?, - Event::Escape | Event::Blankline => unreachable!("filtered out"), - Event::ThematicBreak(attrs) => { - out.write_str("\n")?; + _ => out.write_char('>')?, } } - self.first_line = false; + Event::End(c) => { + if c.is_block_container() && !matches!(c, Container::Footnote { .. }) { + out.write_char('\n')?; + } + if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { + return Ok(()); + } + match c { + Container::Blockquote => out.write_str("")?, + Container::List { + kind: ListKind::Unordered | ListKind::Task, + .. + } => { + self.list_tightness.pop(); + out.write_str("")?; + } + Container::List { + kind: ListKind::Ordered { .. }, + .. + } => out.write_str("")?, + Container::ListItem | Container::TaskListItem { .. } => { + out.write_str("")?; + } + Container::DescriptionList => out.write_str("")?, + Container::DescriptionDetails => out.write_str("")?, + Container::Footnote { number, .. } => { + if !close_para { + // create a new paragraph + out.write_str("\n

")?; + } + write!( + out, + r##"↩︎︎

"##, + number, + )?; + out.write_str("\n")?; + self.footnote_number = None; + } + Container::Table => out.write_str("")?, + Container::TableRow { .. } => out.write_str("")?, + Container::Section { .. } => out.write_str("")?, + Container::Div { .. } => out.write_str("")?, + Container::Paragraph => { + if matches!(self.list_tightness.last(), Some(true)) { + return Ok(()); + } + if self.footnote_number.is_none() { + out.write_str("

")?; + } else { + self.close_para = true; + } + } + Container::Heading { level, .. } => write!(out, "", level)?, + Container::TableCell { head: false, .. } => out.write_str("")?, + Container::TableCell { head: true, .. } => out.write_str("")?, + Container::Caption => out.write_str("")?, + Container::DescriptionTerm => out.write_str("")?, + Container::CodeBlock { .. } => out.write_str("
")?, + Container::Span => out.write_str("")?, + Container::Link(..) => out.write_str("")?, + Container::Image(src, ..) => { + if self.img_alt_text == 1 { + if !src.is_empty() { + out.write_str(r#"" src=""#)?; + write_attr(src, &mut out)?; + } + out.write_str(r#"">"#)?; + } + self.img_alt_text -= 1; + } + Container::Verbatim => out.write_str("
")?, + Container::Math { display } => { + out.write_str(if *display { + r#"\]"# + } else { + r#"\)"# + })?; + } + Container::RawBlock { .. } | Container::RawInline { .. } => { + self.raw = Raw::None; + } + Container::Subscript => out.write_str("")?, + Container::Superscript => out.write_str("")?, + Container::Insert => out.write_str("")?, + Container::Delete => out.write_str("")?, + Container::Strong => out.write_str("")?, + Container::Emphasis => out.write_str("")?, + Container::Mark => out.write_str("")?, + } + } + 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::Html => out.write_str(s)?, + Raw::Other => {} + }, + Event::FootnoteReference(_tag, number) => { + if self.img_alt_text == 0 { + write!( + out, + r##"{}"##, + number, number, number + )?; + } + } + Event::Symbol(sym) => write!(out, ":{}:", sym)?, + Event::LeftSingleQuote => out.write_str("‘")?, + Event::RightSingleQuote => out.write_str("’")?, + Event::LeftDoubleQuote => out.write_str("“")?, + Event::RightDoubleQuote => out.write_str("”")?, + Event::Ellipsis => out.write_str("…")?, + Event::EnDash => out.write_str("–")?, + Event::EmDash => out.write_str("—")?, + Event::NonBreakingSpace => out.write_str(" ")?, + Event::Hardbreak => out.write_str("
\n")?, + Event::Softbreak => out.write_char('\n')?, + Event::Escape | Event::Blankline => unreachable!("filtered out"), + Event::ThematicBreak(attrs) => { + out.write_str("\n")?; + } } + self.first_line = false; + + Ok(()) + } + + fn render_epilogue(&mut self, mut out: W) -> std::fmt::Result + where + W: std::fmt::Write, + { if self.encountered_footnote { out.write_str("\n\n")?; } out.write_char('\n')?; + Ok(()) } } From e506fffed807e01e13586d7fa38059789a93b8b7 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 19 Mar 2023 13:54:44 +0100 Subject: [PATCH 5/8] mv push/write examples from html to Render trait They apply more to the Render trait now than the implementation in the html module --- src/html.rs | 22 ---------------------- src/lib.rs | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/html.rs b/src/html.rs index 0e29c45..0014ef3 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,26 +1,4 @@ //! An HTML renderer that takes an iterator of [`Event`]s and emits HTML. -//! -//! The HTML can be written to either a [`std::fmt::Write`] or a [`std::io::Write`] object. -//! -//! # Examples -//! -//! Push to a [`String`] (implements [`std::fmt::Write`]): -//! -//! ``` -//! # use jotdown::Render; -//! # let events = std::iter::empty(); -//! let mut html = String::new(); -//! jotdown::html::Renderer.push(events, &mut html); -//! ``` -//! -//! Write to standard output with buffering ([`std::io::Stdout`] implements [`std::io::Write`]): -//! -//! ``` -//! # use jotdown::Render; -//! # let events = std::iter::empty(); -//! let mut out = std::io::BufWriter::new(std::io::stdout()); -//! jotdown::html::Renderer.write(events, &mut out).unwrap(); -//! ``` use crate::Alignment; use crate::Container; diff --git a/src/lib.rs b/src/lib.rs index 2f0d2be..eb41f12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,29 @@ pub use attr::{AttributeValue, AttributeValueParts, Attributes}; type CowStr<'s> = std::borrow::Cow<'s, str>; +/// A trait for rendering [`Event`]s to an output format. +/// +/// The output can be written to either a [`std::fmt::Write`] or a [`std::io::Write`] object. +/// +/// # Examples +/// +/// Push to a [`String`] (implements [`std::fmt::Write`]): +/// +/// ``` +/// # use jotdown::Render; +/// # let events = std::iter::empty(); +/// let mut output = String::new(); +/// jotdown::html::Renderer.push(events, &mut output); +/// ``` +/// +/// Write to standard output with buffering ([`std::io::Stdout`] implements [`std::io::Write`]): +/// +/// ``` +/// # use jotdown::Render; +/// # let events = std::iter::empty(); +/// let mut out = std::io::BufWriter::new(std::io::stdout()); +/// jotdown::html::Renderer.write(events, &mut out).unwrap(); +/// ``` pub trait Render { /// Push [`Event`]s to a unicode-accepting buffer or stream. fn push<'s, I: Iterator>, W: fmt::Write>( From e8503e28fd775e47543d621ed7eaddcff1c8f056 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 19 Mar 2023 18:44:58 +0100 Subject: [PATCH 6/8] lib: add Render::render_{event, prologue, epilogue} derive push/write automatically from these --- bench/criterion/main.rs | 6 ++- bench/iai/main.rs | 2 +- examples/jotdown_wasm/src/lib.rs | 4 +- src/html.rs | 27 ++----------- src/lib.rs | 68 +++++++++++++++++++++++++------- src/main.rs | 6 +-- tests/afl/src/lib.rs | 4 +- tests/lib.rs | 4 +- 8 files changed, 73 insertions(+), 48 deletions(-) diff --git a/bench/criterion/main.rs b/bench/criterion/main.rs index 5ff477f..28c2a4f 100644 --- a/bench/criterion/main.rs +++ b/bench/criterion/main.rs @@ -51,7 +51,9 @@ fn gen_html(c: &mut criterion::Criterion) { || jotdown::Parser::new(input).collect::>(), |p| { let mut s = String::new(); - jotdown::html::Renderer.push(p.into_iter(), &mut s).unwrap(); + jotdown::html::Renderer::default() + .push(p.into_iter(), &mut s) + .unwrap(); s }, criterion::BatchSize::SmallInput, @@ -72,7 +74,7 @@ fn gen_full(c: &mut criterion::Criterion) { |b, &input| { b.iter_with_large_drop(|| { let mut s = String::new(); - jotdown::html::Renderer + jotdown::html::Renderer::default() .push(jotdown::Parser::new(input), &mut s) .unwrap(); s diff --git a/bench/iai/main.rs b/bench/iai/main.rs index e606d5f..d948bb6 100644 --- a/bench/iai/main.rs +++ b/bench/iai/main.rs @@ -12,7 +12,7 @@ fn block_inline() -> Option> { fn full() -> String { let mut s = String::new(); - jotdown::html::Renderer + jotdown::html::Renderer::default() .push(jotdown::Parser::new(bench_input::ALL), &mut s) .unwrap(); s diff --git a/examples/jotdown_wasm/src/lib.rs b/examples/jotdown_wasm/src/lib.rs index 3ab7fb0..5250cf1 100644 --- a/examples/jotdown_wasm/src/lib.rs +++ b/examples/jotdown_wasm/src/lib.rs @@ -7,6 +7,8 @@ use jotdown::Render; pub fn jotdown_render(djot: &str) -> String { let events = jotdown::Parser::new(djot); let mut html = String::new(); - jotdown::html::Renderer.push(events, &mut html).unwrap(); + jotdown::html::Renderer::default() + .push(events, &mut html) + .unwrap(); html } diff --git a/src/html.rs b/src/html.rs index 0014ef3..bd105c8 100644 --- a/src/html.rs +++ b/src/html.rs @@ -9,25 +9,13 @@ use crate::OrderedListNumbering::*; use crate::Render; use crate::SpanLinkType; -pub struct Renderer; - -impl Render for Renderer { - fn push<'s, I: Iterator>, W: std::fmt::Write>( - &self, - events: I, - out: W, - ) -> std::fmt::Result { - Writer::default().write(events, out) - } -} - enum Raw { None, Html, Other, } -struct Writer { +pub struct Renderer { raw: Raw, img_alt_text: usize, list_tightness: Vec, @@ -37,7 +25,7 @@ struct Writer { close_para: bool, } -impl Default for Writer { +impl Default for Renderer { fn default() -> Self { Self { raw: Raw::None, @@ -51,16 +39,7 @@ impl Default for Writer { } } -impl Writer { - fn write<'s, I, W>(&mut self, mut events: I, mut out: W) -> std::fmt::Result - where - I: Iterator>, - W: std::fmt::Write, - { - events.try_for_each(|e| self.render_event(&e, &mut out))?; - self.render_epilogue(&mut out) - } - +impl Render for Renderer { fn render_event<'s, W>(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result where W: std::fmt::Write, diff --git a/src/lib.rs b/src/lib.rs index eb41f12..d8aa6f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ //! let djot_input = "hello *world*!"; //! let events = jotdown::Parser::new(djot_input); //! let mut html = String::new(); -//! jotdown::html::Renderer.push(events, &mut html); +//! jotdown::html::Renderer::default().push(events, &mut html); //! assert_eq!(html, "

hello world!

\n"); //! # } //! ``` @@ -41,7 +41,7 @@ //! e => e, //! }); //! let mut html = String::new(); -//! jotdown::html::Renderer.push(events, &mut html); +//! jotdown::html::Renderer::default().push(events, &mut html); //! assert_eq!(html, "

a link

\n"); //! # } //! ``` @@ -71,6 +71,11 @@ type CowStr<'s> = std::borrow::Cow<'s, str>; /// /// The output can be written to either a [`std::fmt::Write`] or a [`std::io::Write`] object. /// +/// An implementor needs to at least implement the [`Render::render_event`] function that renders a +/// single event to the output. If anything needs to be rendered at the beginning or end of the +/// output, the [`Render::render_prologue`] and [`Render::render_epilogue`] can be implemented as +/// well. +/// /// # Examples /// /// Push to a [`String`] (implements [`std::fmt::Write`]): @@ -79,7 +84,8 @@ type CowStr<'s> = std::borrow::Cow<'s, str>; /// # use jotdown::Render; /// # let events = std::iter::empty(); /// let mut output = String::new(); -/// jotdown::html::Renderer.push(events, &mut output); +/// let mut renderer = jotdown::html::Renderer::default(); +/// renderer.push(events, &mut output); /// ``` /// /// Write to standard output with buffering ([`std::io::Stdout`] implements [`std::io::Write`]): @@ -88,25 +94,57 @@ 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()); -/// jotdown::html::Renderer.write(events, &mut out).unwrap(); +/// let mut renderer = jotdown::html::Renderer::default(); +/// renderer.write(events, &mut out).unwrap(); /// ``` pub trait Render { - /// Push [`Event`]s to a unicode-accepting buffer or stream. - fn push<'s, I: Iterator>, W: fmt::Write>( - &self, - events: I, - out: W, - ) -> fmt::Result; + /// Render a single event. + fn render_event<'s, W>(&mut self, e: &Event<'s>, out: W) -> std::fmt::Result + where + W: std::fmt::Write; + + /// Render something before any events have been provided. + /// + /// This does nothing by default, but an implementation may choose to prepend data at the + /// beginning of the output if needed. + fn render_prologue(&mut self, _out: W) -> std::fmt::Result + where + W: std::fmt::Write, + { + Ok(()) + } + + /// Render something after all events have been provided. + /// + /// This does nothing by default, but an implementation may choose to append extra data at the + /// end of the output if needed. + fn render_epilogue(&mut self, _out: W) -> std::fmt::Result + where + W: std::fmt::Write, + { + Ok(()) + } + + /// Push owned [`Event`]s to a unicode-accepting buffer or stream. + fn push<'s, I, W>(&mut self, mut events: I, mut out: W) -> fmt::Result + where + I: Iterator>, + W: fmt::Write, + { + self.render_prologue(&mut out)?; + events.try_for_each(|e| self.render_event(&e, &mut out))?; + self.render_epilogue(&mut out) + } /// Write [`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<'s, I: Iterator>, W: io::Write>( - &self, - events: I, - out: W, - ) -> io::Result<()> { + fn write<'s, I, W>(&mut self, events: I, out: W) -> io::Result<()> + where + I: Iterator>, + W: io::Write, + { struct Adapter { inner: T, error: io::Result<()>, diff --git a/src/main.rs b/src/main.rs index b9ea08c..e73c081 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,11 +68,11 @@ fn run() -> Result<(), std::io::Error> { }; let parser = jotdown::Parser::new(&content); - let html = jotdown::html::Renderer; + let mut renderer = jotdown::html::Renderer::default(); match app.output { - Some(path) => html.write(parser, File::create(path)?)?, - None => html.write(parser, BufWriter::new(std::io::stdout()))?, + Some(path) => renderer.write(parser, File::create(path)?)?, + None => renderer.write(parser, BufWriter::new(std::io::stdout()))?, } Ok(()) diff --git a/tests/afl/src/lib.rs b/tests/afl/src/lib.rs index 530a6ae..adbca14 100644 --- a/tests/afl/src/lib.rs +++ b/tests/afl/src/lib.rs @@ -19,7 +19,9 @@ pub fn html(data: &[u8]) { if !s.contains("=html") { let p = jotdown::Parser::new(s); let mut html = "\n".to_string(); - jotdown::html::Renderer.push(p, &mut html).unwrap(); + jotdown::html::Renderer::default() + .push(p, &mut html) + .unwrap(); validate_html(&html); } } diff --git a/tests/lib.rs b/tests/lib.rs index 984b610..4fd36af 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -14,7 +14,9 @@ macro_rules! suite_test { let expected = $expected; let p = jotdown::Parser::new(src); let mut actual = String::new(); - jotdown::html::Renderer.push(p, &mut actual).unwrap(); + jotdown::html::Renderer::default() + .push(p, &mut actual) + .unwrap(); assert_eq!( actual.trim(), expected.trim(), From 10788af24628e422a02315c10738fdc2aa6541f3 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 17 Mar 2023 19:25:24 +0100 Subject: [PATCH 7/8] lib: add Render::{push, write}_borrowed allow rendering iterators with borrowed events resolves #24 --- src/lib.rs | 101 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d8aa6f5..21999ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,9 @@ type CowStr<'s> = std::borrow::Cow<'s, str>; /// /// The output can be written to either a [`std::fmt::Write`] or a [`std::io::Write`] object. /// +/// If ownership of the [`Event`]s cannot be given to the renderer, use [`Render::push_borrowed`] +/// or [`Render::write_borrowed`]. +/// /// An implementor needs to at least implement the [`Render::render_event`] function that renders a /// single event to the output. If anything needs to be rendered at the beginning or end of the /// output, the [`Render::render_prologue`] and [`Render::render_epilogue`] can be implemented as @@ -136,7 +139,7 @@ pub trait Render { self.render_epilogue(&mut out) } - /// Write [`Event`]s to a byte sink, encoded as UTF-8. + /// 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`]. @@ -145,35 +148,81 @@ pub trait Render { I: Iterator>, W: io::Write, { - struct Adapter { - inner: T, - error: io::Result<()>, - } - - impl fmt::Write for Adapter { - fn write_str(&mut self, s: &str) -> fmt::Result { - match self.inner.write_all(s.as_bytes()) { - Ok(()) => Ok(()), - Err(e) => { - self.error = Err(e); - Err(fmt::Error) - } - } - } - } - - let mut out = Adapter { + let mut out = WriteAdapter { inner: out, error: Ok(()), }; - match self.push(events, &mut out) { - Ok(()) => Ok(()), - Err(_) => match out.error { - Err(_) => out.error, - _ => Err(io::Error::new(io::ErrorKind::Other, "formatter error")), - }, - } + self.push(events, &mut out).map_err(|_| match out.error { + Err(e) => e, + _ => io::Error::new(io::ErrorKind::Other, "formatter error"), + }) + } + + /// Push borrowed [`Event`]s to a unicode-accepting buffer or stream. + /// + /// # Examples + /// + /// Render a borrowed slice of [`Event`]s. + /// ``` + /// # use jotdown::Render; + /// # let events: &[jotdown::Event] = &[]; + /// let mut output = String::new(); + /// let mut renderer = jotdown::html::Renderer::default(); + /// renderer.push_borrowed(events.iter(), &mut output); + /// ``` + fn push_borrowed<'s, E, I, W>(&mut self, mut events: I, mut out: W) -> fmt::Result + where + E: AsRef>, + I: Iterator, + W: fmt::Write, + { + self.render_prologue(&mut out)?; + events.try_for_each(|e| self.render_event(e.as_ref(), &mut out))?; + self.render_epilogue(&mut out) + } + + /// Write borrowed [`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_borrowed<'s, E, I, W>(&mut self, events: I, out: W) -> io::Result<()> + where + E: AsRef>, + I: Iterator, + W: io::Write, + { + let mut out = WriteAdapter { + inner: out, + error: Ok(()), + }; + + self.push_borrowed(events, &mut out) + .map_err(|_| match out.error { + Err(e) => e, + _ => io::Error::new(io::ErrorKind::Other, "formatter error"), + }) + } +} + +struct WriteAdapter { + inner: T, + error: io::Result<()>, +} + +impl fmt::Write for WriteAdapter { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.inner.write_all(s.as_bytes()).map_err(|e| { + self.error = Err(e); + fmt::Error + }) + } +} + +// XXX why is this not a blanket implementation? +impl<'s> AsRef> for &Event<'s> { + fn as_ref(&self) -> &Event<'s> { + self } } From 5daa1602889d17dce6830886e4402ab4e9e135f9 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Tue, 21 Mar 2023 22:31:49 +0100 Subject: [PATCH 8/8] bench-crit: add html_borrow, html_clone allow comparing between rendering owned, borrowed or cloned events --- bench/criterion/main.rs | 56 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/bench/criterion/main.rs b/bench/criterion/main.rs index 28c2a4f..42835a5 100644 --- a/bench/criterion/main.rs +++ b/bench/criterion/main.rs @@ -64,6 +64,60 @@ fn gen_html(c: &mut criterion::Criterion) { } criterion_group!(html, gen_html); +fn gen_html_borrow(c: &mut criterion::Criterion) { + let mut group = c.benchmark_group("html_borrow"); + for (name, input) in bench_input::INPUTS { + group.throughput(criterion::Throughput::Elements( + jotdown::Parser::new(input).count() as u64, + )); + group.bench_with_input( + criterion::BenchmarkId::from_parameter(name), + input, + |b, &input| { + b.iter_batched( + || jotdown::Parser::new(input).collect::>(), + |p| { + let mut s = String::new(); + jotdown::html::Renderer::default() + .push_borrowed(p.as_slice().iter(), &mut s) + .unwrap(); + s + }, + criterion::BatchSize::SmallInput, + ); + }, + ); + } +} +criterion_group!(html_borrow, gen_html_borrow); + +fn gen_html_clone(c: &mut criterion::Criterion) { + let mut group = c.benchmark_group("html_clone"); + for (name, input) in bench_input::INPUTS { + group.throughput(criterion::Throughput::Elements( + jotdown::Parser::new(input).count() as u64, + )); + group.bench_with_input( + criterion::BenchmarkId::from_parameter(name), + input, + |b, &input| { + b.iter_batched( + || jotdown::Parser::new(input).collect::>(), + |p| { + let mut s = String::new(); + jotdown::html::Renderer::default() + .push(p.iter().cloned(), &mut s) + .unwrap(); + s + }, + criterion::BatchSize::SmallInput, + ); + }, + ); + } +} +criterion_group!(html_clone, gen_html_clone); + fn gen_full(c: &mut criterion::Criterion) { let mut group = c.benchmark_group("full"); for (name, input) in bench_input::INPUTS { @@ -85,4 +139,4 @@ fn gen_full(c: &mut criterion::Criterion) { } criterion_group!(full, gen_full); -criterion_main!(block, inline, html, full); +criterion_main!(block, inline, html, html_borrow, html_clone, full);