From f0a74361af8215eee9cfcb8ed08ca13e81cafcfc Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 19 Mar 2023 13:32:06 +0100 Subject: [PATCH 01/52] lib: don't prepend mailto to url in autolink Event better to provide the original url, the event is already tagged as email also avoids a string allocation --- src/html.rs | 3 +++ src/lib.rs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/html.rs b/src/html.rs index 1a1d9e08..d9a3c04e 100644 --- a/src/html.rs +++ b/src/html.rs @@ -166,6 +166,9 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { self.out.write_str(" Parser<'s> { } inline::Container::Autolink => { let url = self.inlines.src(inline.span); - let (url, ty) = if url.contains('@') { - (format!("mailto:{}", url).into(), LinkType::Email) + let ty = if url.contains('@') { + LinkType::Email } else { - (url, LinkType::AutoLink) + LinkType::AutoLink }; Container::Link(url, ty) } From 3b12052bed32d088946927add4c87c861bb2f6cf Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 19 Mar 2023 13:12:16 +0100 Subject: [PATCH 02/52] lib: Add SpanLinkTag::Unresolved variant keep the tag for unresolved links, and allow distinguishing between `[tag][tag with empty url]` and `[tag][non-existent tag]`. closes #26 --- src/html.rs | 6 ++++-- src/lib.rs | 48 +++++++++++++++++++++++++++++++++++++++--------- tests/bench/skip | 2 +- tests/suite/skip | 1 - 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/html.rs b/src/html.rs index d9a3c04e..1b825f87 100644 --- a/src/html.rs +++ b/src/html.rs @@ -25,9 +25,11 @@ use crate::Alignment; use crate::Container; use crate::Event; +use crate::LinkType; use crate::ListKind; use crate::OrderedListNumbering::*; use crate::Render; +use crate::SpanLinkType; pub struct Renderer; @@ -161,8 +163,8 @@ impl<'s, I: Iterator>, W: std::fmt::Write> Writer<'s, I, W> { Container::DescriptionTerm => self.out.write_str(" self.out.write_str(" self.out.write_str(" { - if dst.is_empty() { + Container::Link(dst, ty) => { + if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) { self.out.write_str(" { CodeBlock { lang: Option<&'s str> }, /// An inline divider element. Span, - /// An inline link with a destination URL. + /// An inline link, the first field is either a destination URL or an unresolved tag. Link(CowStr<'s>, LinkType), - /// An inline image with a source URL. Inner Str objects compose the alternative text. + /// An inline image, the first field is either a destination URL or an unresolved tag. Inner + /// Str objects compose the alternative text. Image(CowStr<'s>, SpanLinkType), /// An inline verbatim string. Verbatim, @@ -333,6 +334,8 @@ pub enum SpanLinkType { Inline, /// In the form `[text][tag]` or `[tag][]`. Reference, + /// Like reference, but the tag is unresolved. + Unresolved, } /// The type of an inline link. @@ -729,19 +732,20 @@ impl<'s> Parser<'s> { let link_def = self.pre_pass.link_definitions.get(tag.as_ref()).cloned(); - let url = if let Some((url, attrs_def)) = link_def { + let (url_or_tag, ty) = if let Some((url, attrs_def)) = link_def { attributes.union(attrs_def); - url + (url, SpanLinkType::Reference) } else { - self.pre_pass - .heading_id_by_tag(tag.as_ref()) - .map_or_else(|| "".into(), |id| format!("#{}", id).into()) + self.pre_pass.heading_id_by_tag(tag.as_ref()).map_or_else( + || (tag, SpanLinkType::Unresolved), + |id| (format!("#{}", id).into(), SpanLinkType::Reference), + ) }; if matches!(c, inline::Container::ReferenceLink) { - Container::Link(url, LinkType::Span(SpanLinkType::Reference)) + Container::Link(url_or_tag, LinkType::Span(ty)) } else { - Container::Image(url, SpanLinkType::Reference) + Container::Image(url_or_tag, ty) } } inline::Container::Autolink => { @@ -1298,6 +1302,32 @@ mod test { ); } + #[test] + fn link_reference_unresolved() { + test_parse!( + "[text][tag]", + Start(Paragraph, Attributes::new()), + Start( + Link("tag".into(), LinkType::Span(SpanLinkType::Unresolved)), + Attributes::new() + ), + Str("text".into()), + End(Link("tag".into(), LinkType::Span(SpanLinkType::Unresolved))), + End(Paragraph), + ); + test_parse!( + "![text][tag]", + Start(Paragraph, Attributes::new()), + Start( + Image("tag".into(), SpanLinkType::Unresolved), + Attributes::new() + ), + Str("text".into()), + End(Image("tag".into(), SpanLinkType::Unresolved)), + End(Paragraph), + ); + } + #[test] fn link_reference_multiline() { test_parse!( diff --git a/tests/bench/skip b/tests/bench/skip index d9afe9e2..33a6308e 100644 --- a/tests/bench/skip +++ b/tests/bench/skip @@ -1,3 +1,3 @@ block_list_flat:large list marker number -inline_links_flat:escaped attributes, empty hrefs +inline_links_flat:space before img, img attrs order inline_links_nested:empty link text diff --git a/tests/suite/skip b/tests/suite/skip index aea68c34..e9c249e5 100644 --- a/tests/suite/skip +++ b/tests/suite/skip @@ -10,7 +10,6 @@ e1f5b5e:untrimmed whitespace before linebreak 07888f3:div close within raw block 8423412:heading id conflict with existing id 00a46ed:clear inline formatting from link tags -a8e17c3:empty href c0a3dec:escape in url e66af00:url container precedence 61876cf:roman alpha ambiguity From 76c2c1d06c857305513f71e3d49c865ebf185db6 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Mon, 20 Mar 2023 17:43:09 +0100 Subject: [PATCH 03/52] make: nonzero exit when afl_quick detects crashes ci job still goes green when fuzzing fails, otherwise --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index d3173910..be90c40c 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,7 @@ afl_quick: cargo afl build --no-default-features --release --config profile.release.debug-assertions=true && \ AFL_NO_UI=1 AFL_BENCH_UNTIL_CRASH=1 \ cargo afl fuzz -i in -o out -V 60 target/release/${AFL_TARGET}) + [ -z "$$(find tests/afl/out/default/crashes -type f -name 'id:*')" ] afl_crash: set +e; \ From 1ed561b9f9e12179fd7f1bc6c318d36fd9b4ebae Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 17 Mar 2023 18:48:17 +0100 Subject: [PATCH 04/52] 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 1b825f87..c0f78649 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 524d9595baf0b2a04ff0822c8fb3037d167fbb8a Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 17 Mar 2023 19:05:56 +0100 Subject: [PATCH 05/52] 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 c0f78649..51e21d55 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 2a65ce2b983694dc8914913296ce7670ab63ec63 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 17 Mar 2023 19:13:00 +0100 Subject: [PATCH 06/52] html: rm events/out from Writer separate input/output from rendering state --- src/html.rs | 320 +++++++++++++++++++++++++++------------------------- 1 file changed, 164 insertions(+), 156 deletions(-) diff --git a/src/html.rs b/src/html.rs index 51e21d55..f78173cb 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(s: &str, out: W) -> std::fmt::Result +where + W: std::fmt::Write, +{ + write_escape(s, false, out) +} - fn write_text(&mut self, s: &str) -> std::fmt::Result { - self.write_escape(s, false) - } +fn write_attr(s: &str, out: W) -> std::fmt::Result +where + W: std::fmt::Write, +{ + write_escape(s, true, out) +} - fn write_attr(&mut self, s: &str) -> std::fmt::Result { - self.write_escape(s, true) +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 3cd1d6ded3fbd79b02a0caae76b40e4ac83563db Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 19 Mar 2023 18:35:41 +0100 Subject: [PATCH 07/52] html: extract Writer::render_{event, epilogue} --- src/html.rs | 641 +++++++++++++++++++++++++++------------------------- 1 file changed, 328 insertions(+), 313 deletions(-) diff --git a/src/html.rs b/src/html.rs index f78173cb..0e29c457 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')?; - } - 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)?; - } + 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(..)) { + 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(" { + 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")?; } - Container::Heading { level, .. } => 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("", number)?; + return Ok(()); + } + Container::Table => out.write_str(" out.write_str(" out.write_str(" out.write_str(" { + if matches!(self.list_tightness.last(), Some(true)) { + return Ok(()); } - Container::Image(..) => { - self.img_alt_text += 1; - if self.img_alt_text == 1 { - 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(" out.write_str(" { - self.raw = if format == &"html" { - Raw::Html - } else { - Raw::Other - }; - continue; + } + Container::Image(..) => { + self.img_alt_text += 1; + if self.img_alt_text == 1 { + 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(" { + 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))?; + | 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(' ')?; } - // 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)?; + 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_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=""#)?; - } + 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::Math { display } => { - out.write_str(if display { r#">\["# } else { r#">\("# })?; + } + Container::Image(..) => { + if self.img_alt_text == 1 { + out.write_str(r#" alt=""#)?; } - _ => out.write_char('>')?, } + Container::Math { display } => { + out.write_str(if *display { r#">\["# } else { r#">\("# })?; + } + _ => out.write_char('>')?, + } + } + Event::End(c) => { + if c.is_block_container() && !matches!(c, Container::Footnote { .. }) { + out.write_char('\n')?; } - 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("")?; } - if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { - continue; + Container::List { + kind: ListKind::Ordered { .. }, + .. + } => out.write_str("
    ")?, + Container::ListItem | Container::TaskListItem { .. } => { + out.write_str("
  2. ")?; } - match c { - Container::Blockquote => out.write_str("")?, - Container::List { - kind: ListKind::Unordered | ListKind::Task, - .. - } => { - self.list_tightness.pop(); - 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

    ")?; } - 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; + 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(()); } - 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; - } + 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#"">"#)?; + } + 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)?; } - self.img_alt_text -= 1; + out.write_str(r#"">"#)?; } - 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("")?, + self.img_alt_text -= 1; } - } - 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 - )?; + Container::Verbatim => out.write_str("
")?, + Container::Math { display } => { + out.write_str(if *display { + r#"\]"# + } else { + r#"\)"# + })?; } - } - 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.raw = Raw::None; } - 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 => 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; } + 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 ccdaf2b9e7cb502eec396388326e118152a45934 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 19 Mar 2023 13:54:44 +0100 Subject: [PATCH 08/52] 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 0e29c457..0014ef3f 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 2f0d2be8..eb41f121 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 f940d5c23c20e60983cd9f6ac0e79c165248affa Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 19 Mar 2023 18:44:58 +0100 Subject: [PATCH 09/52] 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 5ff477f0..28c2a4fa 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 e606d5f3..d948bb68 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 3ab7fb01..5250cf1c 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 0014ef3f..bd105c88 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 eb41f121..d8aa6f57 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 b9ea08c3..e73c081c 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 530a6aec..adbca14f 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 984b6102..4fd36afd 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 f186ca96fd0a9b5c13b9510deefe2ee2e0bc0d23 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 17 Mar 2023 19:25:24 +0100 Subject: [PATCH 10/52] lib: add Render::{push, write}_borrowed allow rendering iterators with borrowed events resolves #24 --- src/lib.rs | 97 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d8aa6f57..21999ec9 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<()>, - } + let mut out = WriteAdapter { + inner: out, + error: Ok(()), + }; - 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) - } - } - } - } + self.push(events, &mut out).map_err(|_| match out.error { + Err(e) => e, + _ => io::Error::new(io::ErrorKind::Other, "formatter error"), + }) + } - let mut out = Adapter { + /// 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(()), }; - 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_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 d28542079d852f5f487bbfa01cec497123ab2030 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Tue, 21 Mar 2023 22:31:49 +0100 Subject: [PATCH 11/52] 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 28c2a4fa..42835a53 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); From 130af1c5f6e14e329c8e10ebbfe7bfe359d1fca1 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 26 Mar 2023 16:20:00 +0200 Subject: [PATCH 12/52] jotdown_wasm: rm debug print --- examples/jotdown_wasm/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/jotdown_wasm/index.html b/examples/jotdown_wasm/index.html index 91550963..be3d9836 100644 --- a/examples/jotdown_wasm/index.html +++ b/examples/jotdown_wasm/index.html @@ -10,7 +10,6 @@ function render() { let html = jotdown_render(input.innerText); - console.log(fmt.value); if (fmt.value == "html") { output.classList.add("verbatim") output.innerText = html; From 9f977568d7a9cc687755ce5180bde2f35d4b0a5f Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 26 Mar 2023 22:59:26 +0200 Subject: [PATCH 13/52] jotdown_wasm: rm unmatched closing div --- examples/jotdown_wasm/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/jotdown_wasm/index.html b/examples/jotdown_wasm/index.html index be3d9836..df27dd45 100644 --- a/examples/jotdown_wasm/index.html +++ b/examples/jotdown_wasm/index.html @@ -28,5 +28,5 @@ setTimeout(() => { input.focus(); }, 0);
*Hello world!*
-

+  

 

From 16dbd616cdb669a02dd9eeb8ca39e7ed9292d90f Mon Sep 17 00:00:00 2001
From: Noah Hellman 
Date: Sun, 26 Mar 2023 16:33:38 +0200
Subject: [PATCH 14/52] jotdown_wasm: create html wrapper

demo.html can be included in another html file, create a html file that
includes demo.html for the demo, rather than using demo.html directly
and letting the browser add ,  etc
---
 examples/jotdown_wasm/Makefile                  | 14 ++++++++++++--
 examples/jotdown_wasm/{index.html => demo.html} |  0
 2 files changed, 12 insertions(+), 2 deletions(-)
 rename examples/jotdown_wasm/{index.html => demo.html} (100%)

diff --git a/examples/jotdown_wasm/Makefile b/examples/jotdown_wasm/Makefile
index 5c0c34c1..96ab2653 100644
--- a/examples/jotdown_wasm/Makefile
+++ b/examples/jotdown_wasm/Makefile
@@ -7,9 +7,19 @@ ${WASM}: ${SRC}
 
 wasm: ${WASM}
 
-run: ${WASM}
+index.html: Makefile demo.html
+	echo '' > $@
+	echo '' >> $@
+	echo 'Jotdown Demo' >> $@
+	echo '' >> $@
+	echo '' >> $@
+	cat demo.html >> $@
+	echo '' >> $@
+	echo '' >> $@
+
+run: ${WASM} index.html
 	python -m http.server
 
 clean:
-	rm -rf pkg
+	rm -rf pkg index.html
 	cargo clean
diff --git a/examples/jotdown_wasm/index.html b/examples/jotdown_wasm/demo.html
similarity index 100%
rename from examples/jotdown_wasm/index.html
rename to examples/jotdown_wasm/demo.html

From d6582ebde92150ee6079817ca2bec793573ee1e2 Mon Sep 17 00:00:00 2001
From: Noah Hellman 
Date: Sun, 26 Mar 2023 16:15:54 +0200
Subject: [PATCH 15/52] jotdown_wasm: use full viewport

---
 examples/jotdown_wasm/Makefile  |  2 +-
 examples/jotdown_wasm/demo.html | 15 +++++++++++----
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/examples/jotdown_wasm/Makefile b/examples/jotdown_wasm/Makefile
index 96ab2653..d2a2b92e 100644
--- a/examples/jotdown_wasm/Makefile
+++ b/examples/jotdown_wasm/Makefile
@@ -12,7 +12,7 @@ index.html: Makefile demo.html
 	echo '' >> $@
 	echo 'Jotdown Demo' >> $@
 	echo '' >> $@
-	echo '' >> $@
+	echo '' >> $@
 	cat demo.html >> $@
 	echo '' >> $@
 	echo '' >> $@
diff --git a/examples/jotdown_wasm/demo.html b/examples/jotdown_wasm/demo.html
index df27dd45..5876b284 100644
--- a/examples/jotdown_wasm/demo.html
+++ b/examples/jotdown_wasm/demo.html
@@ -1,5 +1,4 @@
-
-
+
-
*Hello world!*
-

+  
+ +
+
+
*Hello world!*
+

+  
From 2ab27810b23e36c9f3496addf6d5520e433c1b83 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 26 Mar 2023 17:05:50 +0200 Subject: [PATCH 16/52] attr: impl Debug for Attributes manually show key value pairs instead of internal structure --- src/attr.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/attr.rs b/src/attr.rs index cd6eaaa7..16fc984c 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -98,7 +98,7 @@ impl<'s> Iterator for AttributeValueParts<'s> { // Attributes are relatively rare, we choose to pay 8 bytes always and sometimes an extra // indirection instead of always 24 bytes. #[allow(clippy::box_vec)] -#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[derive(Clone, PartialEq, Eq, Default)] pub struct Attributes<'s>(Option)>>>); impl<'s> Attributes<'s> { @@ -202,6 +202,21 @@ impl<'s> FromIterator<(&'s str, &'s str)> for Attributes<'s> { } } +impl<'s> std::fmt::Debug for Attributes<'s> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{{")?; + let mut first = true; + for (k, v) in self.iter() { + if !first { + write!(f, ", ")?; + } + first = false; + write!(f, "{}=\"{}\"", k, v.raw)?; + } + write!(f, "}}") + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum State { Start, From 1ab043ebc25817d7ece74480bbbf3212392baace Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 26 Mar 2023 14:54:25 +0200 Subject: [PATCH 17/52] jotdown_wasm: add option to show events provide 1:1 representation of events to help explore how events are emitted --- examples/jotdown_wasm/demo.html | 14 ++++++++++---- examples/jotdown_wasm/src/lib.rs | 8 ++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/examples/jotdown_wasm/demo.html b/examples/jotdown_wasm/demo.html index 5876b284..60f0308c 100644 --- a/examples/jotdown_wasm/demo.html +++ b/examples/jotdown_wasm/demo.html @@ -1,6 +1,9 @@