diff --git a/bench/criterion/main.rs b/bench/criterion/main.rs index c2e41b9a..cde7c5d5 100644 --- a/bench/criterion/main.rs +++ b/bench/criterion/main.rs @@ -1,5 +1,6 @@ use criterion::criterion_group; use criterion::criterion_main; +use jotdown::RenderExt; fn gen_block(c: &mut criterion::Criterion) { let mut group = c.benchmark_group("block"); @@ -47,7 +48,7 @@ fn gen_html(c: &mut criterion::Criterion) { |b, &input| { b.iter_batched( || jotdown::Parser::new(input).collect::>(), - |p| jotdown::html::render_to_string(p.into_iter()), + |p| jotdown::html::Renderer::default().render_events(p.into_iter()), criterion::BatchSize::SmallInput, ); }, @@ -68,7 +69,7 @@ fn gen_html_clone(c: &mut criterion::Criterion) { |b, &input| { b.iter_batched( || jotdown::Parser::new(input).collect::>(), - |p| jotdown::html::render_to_string(p.iter().cloned()), + |p| jotdown::html::Renderer::default().render_events(p.into_iter()), criterion::BatchSize::SmallInput, ); }, @@ -85,9 +86,7 @@ fn gen_full(c: &mut criterion::Criterion) { criterion::BenchmarkId::from_parameter(name), input, |b, &input| { - b.iter_with_large_drop(|| { - jotdown::html::render_to_string(jotdown::Parser::new(input)) - }); + b.iter_with_large_drop(|| jotdown::html::render_to_string(input)); }, ); } diff --git a/examples/jotdown_wasm/src/lib.rs b/examples/jotdown_wasm/src/lib.rs index 45298c24..1018d726 100644 --- a/examples/jotdown_wasm/src/lib.rs +++ b/examples/jotdown_wasm/src/lib.rs @@ -11,7 +11,7 @@ pub fn jotdown_version() -> String { #[must_use] #[wasm_bindgen] pub fn jotdown_render(djot: &str) -> String { - jotdown::html::render_to_string(jotdown::Parser::new(djot)) + jotdown::html::render_to_string(djot) } #[must_use] diff --git a/src/html.rs b/src/html.rs index 35039c45..84a9c3e2 100644 --- a/src/html.rs +++ b/src/html.rs @@ -13,22 +13,20 @@ use crate::SpanLinkType; /// Render events into a string. /// -/// This is a convenience function for using [`Renderer::push`] with fewer imports and without an -/// intermediate variable. +/// This is a convenience function for rendering documents whole without any customizations. /// /// # Examples /// /// ``` -/// let events = jotdown::Parser::new("hello"); -/// assert_eq!(jotdown::html::render_to_string(events), "

hello

\n"); +/// assert_eq!(jotdown::html::render_to_string("hello"), "

hello

\n"); /// ``` -pub fn render_to_string<'s, I>(events: I) -> String -where - I: Iterator>, -{ - let mut s = String::new(); - Renderer::default().push(events, &mut s).unwrap(); - s +pub fn render_to_string(doc: &str) -> String { + use crate::RenderExt; + let mut r = Renderer::default(); + + r.render_document(doc).unwrap(); + + r.into_inner() } #[derive(Clone)] @@ -46,11 +44,9 @@ pub struct Indentation { /// # use jotdown::*; /// # use jotdown::html::*; /// let src = "> a\n"; - /// let events = Parser::new(src); /// - /// let mut html = String::new(); /// let renderer = Renderer::indented(Indentation::default()); - /// renderer.push(events.clone(), &mut html).unwrap(); + /// let html = renderer.render_to_string(src); /// assert_eq!( /// html, /// concat!( @@ -66,14 +62,12 @@ pub struct Indentation { /// ``` /// # use jotdown::*; /// # use jotdown::html::*; - /// # let src = "> a\n"; - /// # let events = Parser::new(src); - /// # let mut html = String::new(); + /// let src = "> a\n"; /// let renderer = Renderer::indented(Indentation { /// string: " ".to_string(), /// ..Indentation::default() /// }); - /// renderer.push(events.clone(), &mut html).unwrap(); + /// let html = renderer.render_to_string(src); /// assert_eq!( /// html, /// concat!( @@ -94,11 +88,9 @@ pub struct Indentation { /// # use jotdown::*; /// # use jotdown::html::*; /// let src = "> a\n"; - /// let events = Parser::new(src); /// - /// let mut html = String::new(); /// let renderer = Renderer::indented(Indentation::default()); - /// renderer.push(events.clone(), &mut html).unwrap(); + /// let html = renderer.render_to_string(src); /// assert_eq!( /// html, /// concat!( @@ -114,14 +106,12 @@ pub struct Indentation { /// ``` /// # use jotdown::*; /// # use jotdown::html::*; - /// # let src = "> a\n"; - /// # let events = Parser::new(src); - /// # let mut html = String::new(); + /// let src = "> a\n"; /// let renderer = Renderer::indented(Indentation { /// initial_level: 2, /// ..Indentation::default() /// }); - /// renderer.push(events.clone(), &mut html).unwrap(); + /// let html = renderer.render_to_string(src); /// assert_eq!( /// html, /// concat!( @@ -147,12 +137,21 @@ impl Default for Indentation { /// /// By default, block elements are placed on separate lines. To configure the formatting of the /// output, see the [`Renderer::minified`] and [`Renderer::indented`] constructors. -#[derive(Clone)] -pub struct Renderer { +pub struct Renderer<'s, W> { indent: Option, + writer: Writer<'s>, + output: W, } -impl Renderer { +impl<'s> Renderer<'s, String> { + fn new(indent: Option) -> Self { + Self { + writer: Writer::new(&indent), + indent, + output: String::new(), + } + } + /// Create a renderer that emits no whitespace between elements. /// /// # Examples @@ -167,16 +166,15 @@ impl Renderer { /// "\n", /// " - c\n", /// ); - /// let mut actual = String::new(); /// let renderer = Renderer::minified(); - /// renderer.push(Parser::new(src), &mut actual).unwrap(); + /// let actual = renderer.render_to_string(src); /// let expected = /// "
  • a
    • b

    • c

"; /// assert_eq!(actual, expected); /// ``` #[must_use] pub fn minified() -> Self { - Self { indent: None } + Self::new(None) } /// Create a renderer that indents lines based on their block element depth. @@ -195,9 +193,8 @@ impl Renderer { /// "\n", /// " - c\n", /// ); - /// let mut actual = String::new(); /// let renderer = Renderer::indented(Indentation::default()); - /// renderer.push(Parser::new(src), &mut actual).unwrap(); + /// let actual = renderer.render_to_string(src); /// let expected = concat!( /// "
    \n", /// "\t
  • \n", @@ -217,13 +214,30 @@ impl Renderer { /// ``` #[must_use] pub fn indented(indent: Indentation) -> Self { - Self { - indent: Some(indent), - } + Self::new(Some(indent)) + } + + pub fn render_to_string(self, input: &str) -> String { + use super::RenderExt; + let mut s = self.with_fmt_writer(String::new()); + s.render_document(input).expect("Can't fail"); + + s.into_inner() + } + + pub fn render_events_to_string(self, events: I) -> String + where + I: Iterator>, + { + use super::RenderExt; + let mut s = self.with_fmt_writer(String::new()); + s.render_events(events).expect("Can't fail"); + + s.into_inner() } } -impl Default for Renderer { +impl<'s> Default for Renderer<'s, String> { /// Place block elements on separate lines. /// /// This is the default behavior and matches the reference implementation. @@ -240,9 +254,8 @@ impl Default for Renderer { /// "\n", /// " - c\n", /// ); - /// let mut actual = String::new(); /// let renderer = Renderer::default(); - /// renderer.push(Parser::new(src), &mut actual).unwrap(); + /// let actual = renderer.render_to_string(src); /// let expected = concat!( /// "
      \n", /// "
    • \n", @@ -261,24 +274,129 @@ impl Default for Renderer { /// assert_eq!(actual, expected); /// ``` fn default() -> Self { + Renderer::new(Some(Indentation { + string: String::new(), + initial_level: 0, + })) + } +} + +impl<'s, W> Renderer<'s, W> { + pub fn into_inner(self) -> W { + self.output + } +} + +pub struct WriteAdapter(T); + +struct WriteAdapterInner { + inner: T, + error: std::io::Result<()>, +} + +impl WriteAdapterInner { + fn new(output: T) -> Self { Self { - indent: Some(Indentation { - string: String::new(), - initial_level: 0, - }), + inner: output, + error: Ok(()), } } } +impl std::fmt::Write for WriteAdapterInner { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.inner.write_all(s.as_bytes()).map_err(|e| { + self.error = Err(e); + std::fmt::Error + }) + } +} -impl Render for Renderer { - fn push<'s, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result +impl<'s, W> Renderer<'s, W> { + pub fn with_io_writer( + self, + output: NewWriter, + ) -> Renderer<'s, WriteAdapter> where - I: Iterator>, - W: std::fmt::Write, + NewWriter: std::io::Write, { - let mut w = Writer::new(&self.indent); - events.try_for_each(|e| w.render_event(e, &mut out))?; - w.render_epilogue(&mut out) + let Renderer { + indent, + writer, + output: _, + } = self; + + let output = WriteAdapter(output); + Renderer { + indent, + writer, + output, + } + } + + pub fn with_fmt_writer(self, output: NewWriter) -> Renderer<'s, NewWriter> + where + NewWriter: std::fmt::Write, + { + let Renderer { + indent, + writer, + output: _, + } = self; + + Renderer { + indent, + writer, + output, + } + } +} + +impl<'s, W> Render<'s> for Renderer<'s, W> +where + W: std::fmt::Write, +{ + type Error = std::fmt::Error; + + fn begin(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + /// Called iteratively with every single event emitted by parsing djot document + fn emit(&mut self, event: Event<'s>) -> Result<(), Self::Error> { + self.writer + .render_event(event, &mut self.output, &self.indent) + } + + /// Called at the end of rendering a djot document + fn finish(&mut self) -> Result<(), Self::Error> { + self.writer.render_epilogue(&mut self.output, &self.indent) + } +} + +impl<'s, W> Render<'s> for Renderer<'s, WriteAdapter> +where + W: std::io::Write, +{ + type Error = std::io::Error; + + fn begin(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + /// Called iteratively with every single event emitted by parsing djot document + fn emit(&mut self, event: Event<'s>) -> Result<(), Self::Error> { + let mut adapter = WriteAdapterInner::new(&mut self.output.0); + self.writer + .render_event(event, &mut adapter, &self.indent) + .map_err(|_| adapter.error.expect_err("Inner adapter always sets error")) + } + + /// Called at the end of rendering a djot document + fn finish(&mut self) -> Result<(), Self::Error> { + let mut adapter = WriteAdapterInner::new(&mut self.output.0); + self.writer + .render_epilogue(&mut adapter, &self.indent) + .map_err(|_| adapter.error.expect_err("Inner adapter always sets error")) } } @@ -294,8 +412,7 @@ impl Default for Raw { } } -struct Writer<'s, 'f> { - indent: &'f Option, +struct Writer<'s> { depth: usize, raw: Raw, img_alt_text: usize, @@ -305,15 +422,14 @@ struct Writer<'s, 'f> { footnotes: Footnotes<'s>, } -impl<'s, 'f> Writer<'s, 'f> { - fn new(indent: &'f Option) -> Self { +impl<'s> Writer<'s> { + fn new(indent: &Option) -> Self { let depth = if let Some(indent) = indent { indent.initial_level } else { 0 }; Self { - indent, depth, raw: Raw::default(), img_alt_text: 0, @@ -324,11 +440,16 @@ impl<'s, 'f> Writer<'s, 'f> { } } - fn block(&mut self, mut out: W, depth_change: isize) -> std::fmt::Result + fn block( + &mut self, + mut out: W, + indent: &Option, + depth_change: isize, + ) -> std::fmt::Result where W: std::fmt::Write, { - if self.indent.is_none() { + if indent.is_none() { return Ok(()); } @@ -340,7 +461,7 @@ impl<'s, 'f> Writer<'s, 'f> { if depth_change < 0 { self.depth = next_depth; } - self.indent(&mut out)?; + self.indent(&mut out, indent)?; if depth_change > 0 { self.depth = next_depth; } @@ -348,11 +469,11 @@ impl<'s, 'f> Writer<'s, 'f> { Ok(()) } - fn indent(&self, mut out: W) -> std::fmt::Result + fn indent(&self, mut out: W, indent: &Option) -> std::fmt::Result where W: std::fmt::Write, { - if let Some(indent) = self.indent { + if let Some(indent) = indent { if !indent.string.is_empty() { for _ in 0..self.depth { out.write_str(&indent.string)?; @@ -362,7 +483,12 @@ impl<'s, 'f> Writer<'s, 'f> { Ok(()) } - fn render_event(&mut self, e: Event<'s>, mut out: W) -> std::fmt::Result + fn render_event( + &mut self, + e: Event<'s>, + mut out: W, + indent: &Option, + ) -> std::fmt::Result where W: std::fmt::Write, { @@ -395,7 +521,7 @@ impl<'s, 'f> Writer<'s, 'f> { match e { Event::Start(c, attrs) => { if c.is_block() { - self.block(&mut out, c.is_block_container().into())?; + self.block(&mut out, indent, c.is_block_container().into())?; } if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { return Ok(()); @@ -562,7 +688,7 @@ impl<'s, 'f> Writer<'s, 'f> { } Container::TaskListItem { checked } => { out.write_char('>')?; - self.block(&mut out, 0)?; + self.block(&mut out, indent, 0)?; if checked { out.write_str(r#""#)?; } else { @@ -574,7 +700,7 @@ impl<'s, 'f> Writer<'s, 'f> { } Event::End(c) => { if c.is_block_container() { - self.block(&mut out, -1)?; + self.block(&mut out, indent, -1)?; } if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { return Ok(()); @@ -670,15 +796,15 @@ impl<'s, 'f> Writer<'s, 'f> { Event::NonBreakingSpace => out.write_str(" ")?, Event::Hardbreak => { out.write_str("
      ")?; - self.block(out, 0)?; + self.block(out, indent, 0)?; } Event::Softbreak => { out.write_char('\n')?; - self.indent(&mut out)?; + self.indent(&mut out, indent)?; } Event::Escape | Event::Blankline | Event::Attributes(..) => {} Event::ThematicBreak(attrs) => { - self.block(&mut out, 0)?; + self.block(&mut out, indent, 0)?; out.write_str(" Writer<'s, 'f> { Ok(()) } - fn render_epilogue(&mut self, mut out: W) -> std::fmt::Result + fn render_epilogue(&mut self, mut out: W, indent: &Option) -> std::fmt::Result where W: std::fmt::Write, { if self.footnotes.reference_encountered() { - self.block(&mut out, 0)?; + self.block(&mut out, indent, 0)?; out.write_str("
      ")?; - self.block(&mut out, 0)?; + self.block(&mut out, indent, 0)?; out.write_str("
      ")?; - self.block(&mut out, 0)?; + self.block(&mut out, indent, 0)?; out.write_str("
        ")?; while let Some((number, events)) = self.footnotes.next() { - self.block(&mut out, 0)?; + self.block(&mut out, indent, 0)?; write!(out, "
      1. ", number)?; let mut unclosed_para = false; @@ -718,13 +844,13 @@ impl<'s, 'f> Writer<'s, 'f> { // not a footnote, so no need to add href before para close out.write_str("

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

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

      2. ")?; } - self.block(&mut out, 0)?; + self.block(&mut out, indent, 0)?; out.write_str("
      ")?; - self.block(&mut out, 0)?; + self.block(&mut out, indent, 0)?; out.write_str("
      ")?; } - if self.indent.is_some() { + if indent.is_some() { out.write_char('\n')?; } diff --git a/src/lib.rs b/src/lib.rs index 71374f38..ecd0c9a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ //! # { //! let djot_input = "hello *world*!"; //! let events = jotdown::Parser::new(djot_input); -//! let html = jotdown::html::render_to_string(events); +//! let html = jotdown::html::Renderer::default().render_events_to_string(events); //! assert_eq!(html, "

      hello world!

      \n"); //! # } //! ``` @@ -36,7 +36,7 @@ //! } //! e => e, //! }); -//! let html = jotdown::html::render_to_string(events); +//! let html = jotdown::html::Renderer::default().render_events_to_string(events); //! assert_eq!(html, "

      a link

      \n"); //! # } //! ``` @@ -57,85 +57,45 @@ pub use attr::ParseAttributesError; type CowStr<'s> = std::borrow::Cow<'s, str>; -/// A trait for rendering [`Event`]s to an output format. +/// An implementation of rendering a djot document /// -/// 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`]): -/// -/// ``` -/// # #[cfg(feature = "html")] -/// # { -/// # use jotdown::Render; -/// # let events = std::iter::empty(); -/// let mut output = String::new(); -/// let renderer = jotdown::html::Renderer::default(); -/// renderer.push(events, &mut output); -/// # } -/// ``` -/// -/// Write to standard output with buffering ([`std::io::Stdout`] implements [`std::io::Write`]): -/// -/// ``` -/// # #[cfg(feature = "html")] -/// # { -/// # use jotdown::Render; -/// # let events = std::iter::empty(); -/// let mut out = std::io::BufWriter::new(std::io::stdout()); -/// let renderer = jotdown::html::Renderer::default(); -/// renderer.write(events, &mut out).unwrap(); -/// # } -/// ``` -pub trait Render { - /// Push owned [`Event`]s to a unicode-accepting buffer or stream. - fn push<'s, I, W>(&self, events: I, out: W) -> std::fmt::Result - where - I: Iterator>, - W: std::fmt::Write; +/// This interface will be called by the djot parser with elements +/// parsed by it. +pub trait Render<'s> { + type Error; - /// Write owned [`Event`]s to a byte sink, encoded as UTF-8. - /// - /// NOTE: This performs many small writes, so IO writes should be buffered with e.g. - /// [`std::io::BufWriter`]. - fn write<'s, I, W>(&self, events: I, out: W) -> std::io::Result<()> + /// Called at the start of rendering a djot document + fn begin(&mut self) -> Result<(), Self::Error>; + + /// Called iteratively with every single event emitted by parsing djot document + fn emit(&mut self, event: Event<'s>) -> Result<(), Self::Error>; + + /// Called at the end of rendering a djot document + fn finish(&mut self) -> Result<(), Self::Error>; +} + +pub trait RenderExt<'s>: Render<'s> { + /// Parse and render the whole document with `renderer` + fn render_document(&mut self, src: &'s str) -> Result<(), Self::Error> { + self.render_events(Parser::new(src)) + } + + /// Render document as a list of events already parsed by the [`Parser`] + fn render_events(&mut self, events: I) -> Result<(), Self::Error> where I: Iterator>, - W: std::io::Write, { - struct WriteAdapter { - inner: T, - error: std::io::Result<()>, - } + self.begin()?; - impl std::fmt::Write for WriteAdapter { - fn write_str(&mut self, s: &str) -> std::fmt::Result { - self.inner.write_all(s.as_bytes()).map_err(|e| { - self.error = Err(e); - std::fmt::Error - }) - } + for event in events { + self.emit(event)?; } - let mut out = WriteAdapter { - inner: out, - error: Ok(()), - }; - - self.push(events, &mut out).map_err(|_| match out.error { - Err(e) => e, - _ => std::io::Error::new(std::io::ErrorKind::Other, "formatter error"), - }) + self.finish() } } -// XXX why is this not a blanket implementation? -impl<'s> AsRef> for &Event<'s> { - fn as_ref(&self) -> &Event<'s> { - self - } -} +impl<'s, R> RenderExt<'s> for R where R: Render<'s> {} /// A Djot event. /// @@ -175,7 +135,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      word

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Start(Container<'s>, Attributes<'s>), /// End of a container. @@ -202,7 +162,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      str

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Str(CowStr<'s>), /// A footnote reference. @@ -234,7 +194,7 @@ pub enum Event<'s> { /// "\n", /// "\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` FootnoteReference(CowStr<'s>), /// A symbol, by default rendered literally but may be treated specially. @@ -255,7 +215,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      a :sym:

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Symbol(CowStr<'s>), /// Left single quotation mark. @@ -277,7 +237,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      ‘quote’

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` LeftSingleQuote, /// Right single quotation mark. @@ -299,7 +259,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      ’Tis Socrates’

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` RightSingleQuote, /// Left single quotation mark. @@ -322,7 +282,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      “Hello,” he said

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` LeftDoubleQuote, /// Right double quotation mark. @@ -345,7 +305,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      yes…

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Ellipsis, /// An en dash. @@ -367,7 +327,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      57–33

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` EnDash, /// An em dash. @@ -389,7 +349,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      oxen—and

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` EmDash, /// A space that must not break a line. @@ -412,7 +372,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      no break

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` NonBreakingSpace, /// A newline that may or may not break a line in the output. @@ -440,7 +400,7 @@ pub enum Event<'s> { /// "

      soft\n", /// "break

      \n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Softbreak, /// A newline that must break a line in the output. @@ -469,7 +429,7 @@ pub enum Event<'s> { /// "

      hard
      \n", /// "break

      \n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Hardbreak, /// An escape character, not visible in output. @@ -492,7 +452,7 @@ pub enum Event<'s> { /// ], /// ); /// let html = "

      *a*

      \n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Escape, /// A blank line, not visible in output. @@ -523,7 +483,7 @@ pub enum Event<'s> { /// "

      para0

      \n", /// "

      para1

      \n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Blankline, /// A thematic break, typically a horizontal rule. @@ -567,7 +527,7 @@ pub enum Event<'s> { /// "

      para1

      \n", /// "
      \n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` ThematicBreak(Attributes<'s>), /// Dangling attributes not attached to anything. @@ -613,7 +573,7 @@ pub enum Event<'s> { /// "\n", /// "

      inline

      \n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Attributes(Attributes<'s>), } @@ -656,7 +616,7 @@ pub enum Container<'s> { /// "b

      \n", /// "\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Blockquote, /// A list. @@ -708,7 +668,7 @@ pub enum Container<'s> { /// "
    • \n", /// "
    \n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` List { kind: ListKind, tight: bool }, /// An item of a list @@ -747,7 +707,7 @@ pub enum Container<'s> { /// "
  • \n", /// "
\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` ListItem, /// An item of a task list, either checked or unchecked. @@ -790,7 +750,7 @@ pub enum Container<'s> { /// "\n", /// "\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` TaskListItem { checked: bool }, /// A description list. @@ -845,7 +805,7 @@ pub enum Container<'s> { /// "\n", /// "\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` DescriptionList, /// Details describing a term within a description list. @@ -891,7 +851,7 @@ pub enum Container<'s> { /// "\n", /// "\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Footnote { label: CowStr<'s> }, /// A table element. @@ -983,7 +943,7 @@ pub enum Container<'s> { /// "\n", /// "\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Table, /// A row element of a table. @@ -1052,7 +1012,7 @@ pub enum Container<'s> { /// "\n", /// "\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Section { id: CowStr<'s> }, /// A block-level divider element. @@ -1085,7 +1045,7 @@ pub enum Container<'s> { /// "

this is a note

\n", /// "\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Div { class: CowStr<'s> }, /// A paragraph. @@ -1127,7 +1087,7 @@ pub enum Container<'s> { /// "

heading

\n", /// "\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Heading { level: u16, @@ -1182,7 +1142,7 @@ pub enum Container<'s> { /// "\n", /// "\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Caption, /// A term within a description list. @@ -1207,7 +1167,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` LinkDefinition { label: CowStr<'s> }, /// A block with raw markup for a specific output format. @@ -1234,7 +1194,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "x\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` RawBlock { format: CowStr<'s> }, /// A block with code in a specific language. @@ -1264,7 +1224,7 @@ pub enum Container<'s> { /// "
<tag>x</tag>\n",
     ///     "
\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` CodeBlock { language: CowStr<'s> }, /// An inline divider element. @@ -1304,7 +1264,7 @@ pub enum Container<'s> { /// "

word\n", /// "two words

\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Span, /// An inline link, the first field is either a destination URL or an unresolved tag. @@ -1356,7 +1316,7 @@ pub enum Container<'s> { /// "

https://example.com\n", /// "me@example.com

\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` /// /// Anchor text and the URL can be specified inline: @@ -1385,7 +1345,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

anchor

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` /// /// Alternatively, the URL can be retrieved from a link definition using hard brackets, if it @@ -1443,7 +1403,7 @@ pub enum Container<'s> { /// "

a\n", /// "b

\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Link(CowStr<'s>, LinkType), /// An inline image, the first field is either a destination URL or an unresolved tag. @@ -1472,7 +1432,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

\"alt

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Image(CowStr<'s>, SpanLinkType), /// An inline verbatim string. @@ -1495,7 +1455,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

inline verbatim

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Verbatim, /// An inline or display math element. @@ -1536,7 +1496,7 @@ pub enum Container<'s> { /// "

inline \\(a\\cdot{}b\\) or\n", /// "display \\[\\frac{a}{b}\\]

\n", /// ); - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Math { display: bool }, /// Inline raw markup for a specific output format. @@ -1560,7 +1520,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

a

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` RawInline { format: CowStr<'s> }, /// A subscripted element. @@ -1582,7 +1542,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

SUB

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Subscript, /// A superscripted element. @@ -1604,7 +1564,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

SUP

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Superscript, /// An inserted inline element. @@ -1626,7 +1586,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

INS

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Insert, /// A deleted inline element. @@ -1648,7 +1608,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

DEL

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Delete, /// An inline element emphasized with a bold typeface. @@ -1670,7 +1630,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

STRONG

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Strong, /// An emphasized inline element. @@ -1692,7 +1652,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

EM

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Emphasis, /// A highlighted inline element. @@ -1714,7 +1674,7 @@ pub enum Container<'s> { /// ], /// ); /// let html = "

MARK

\n"; - /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// assert_eq!(&html::Renderer::default().render_events_to_string(events.into_iter()), html); /// ``` Mark, } diff --git a/src/main.rs b/src/main.rs index 3d382ff7..056dde18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use jotdown::RenderExt as _; + #[derive(Default)] struct App { input: Option, @@ -79,7 +81,6 @@ fn parse_args() -> App { } fn run() -> Result<(), std::io::Error> { - use jotdown::Render; use std::io::Read; let app = parse_args(); @@ -93,7 +94,6 @@ fn run() -> Result<(), std::io::Error> { } }; - let parser = jotdown::Parser::new(&content); let renderer = if app.minified { jotdown::html::Renderer::minified() } else { @@ -104,8 +104,12 @@ fn run() -> Result<(), std::io::Error> { }; match app.output { - Some(path) => renderer.write(parser, std::fs::File::create(path)?)?, - None => renderer.write(parser, std::io::BufWriter::new(std::io::stdout()))?, + Some(path) => renderer + .with_io_writer(&mut std::fs::File::create(path)?) + .render_document(&content)?, + None => renderer + .with_io_writer(std::io::BufWriter::new(std::io::stdout())) + .render_document(&content)?, } Ok(()) diff --git a/tests/afl/src/lib.rs b/tests/afl/src/lib.rs index 591f46c5..03218ac0 100644 --- a/tests/afl/src/lib.rs +++ b/tests/afl/src/lib.rs @@ -1,9 +1,8 @@ -use jotdown::Render; - use html5ever::tendril; use html5ever::tendril::TendrilSink; use html5ever::tokenizer; use html5ever::tree_builder; +use jotdown::RenderExt; /// Perform sanity checks on events. pub fn parse(data: &[u8]) { @@ -63,7 +62,8 @@ pub fn html(data: &[u8]) { let p = jotdown::Parser::new(s); let mut html = "\n".to_string(); jotdown::html::Renderer::default() - .push(p, &mut html) + .with_fmt_writer(&mut html) + .render_events(p) .unwrap(); validate_html(&html); } diff --git a/tests/filters.rs b/tests/filters.rs new file mode 100644 index 00000000..003afd3a --- /dev/null +++ b/tests/filters.rs @@ -0,0 +1,46 @@ +use jotdown::{Container, Event, Render, RenderExt as _}; +use std::borrow::Cow; + +struct RickrollRenderer(R); + +impl<'s, R> Render<'s> for RickrollRenderer +where + R: Render<'s>, +{ + type Error = R::Error; + + fn begin(&mut self) -> Result<(), Self::Error> { + self.0.begin() + } + + fn emit(&mut self, event: Event<'s>) -> Result<(), Self::Error> { + match event { + Event::Start(Container::Link(_link, t), attrs) => self.0.emit(Event::Start( + Container::Link( + Cow::Owned("https://www.youtube.com/watch?v=E4WlUXrJgy4".to_owned()), + t, + ), + attrs, + )), + _ => self.0.emit(event), + } + } + + fn finish(&mut self) -> Result<(), Self::Error> { + self.0.finish() + } +} + +#[test] +fn rickroll_me() { + use jotdown::RenderOutputExt; + let src = "[interesting link](https://example.com)"; + let out = RickrollRenderer(jotdown::html::Renderer::minified()) + .render_into_document(src) + .unwrap(); + + assert_eq!( + r.0.into_inner(), + "

interesting link

" + ); +} diff --git a/tests/html-ref/lib.rs b/tests/html-ref/lib.rs index fabada5f..3feae2c8 100644 --- a/tests/html-ref/lib.rs +++ b/tests/html-ref/lib.rs @@ -4,14 +4,9 @@ mod r#ref; #[macro_export] macro_rules! compare { ($src:expr, $expected:expr) => { - use jotdown::Render; let src = $src; let expected = std::fs::read_to_string($expected).expect("read failed"); - let p = jotdown::Parser::new(src); - let mut actual = String::new(); - jotdown::html::Renderer::default() - .push(p, &mut actual) - .unwrap(); + let actual = jotdown::html::Renderer::default().render_to_string(src); assert_eq!(actual, expected, "\n{}", { use std::io::Write; let mut child = std::process::Command::new("diff") diff --git a/tests/html-ut/lib.rs b/tests/html-ut/lib.rs index d616f539..3cd981fa 100644 --- a/tests/html-ut/lib.rs +++ b/tests/html-ut/lib.rs @@ -6,8 +6,7 @@ macro_rules! compare { ($src:expr, $expected:expr) => { let src = $src; let expected = $expected; - let p = jotdown::Parser::new(src); - let actual = jotdown::html::render_to_string(p); + let actual = jotdown::html::render_to_string(src); assert_eq!( actual.trim(), expected.trim(), diff --git a/tests/html.rs b/tests/html.rs index e808fb2e..8d886a3b 100644 --- a/tests/html.rs +++ b/tests/html.rs @@ -1,15 +1,14 @@ use jotdown::html::Indentation; -use jotdown::Render; +use jotdown::RenderExt; macro_rules! test_html { ($src:expr, $expected:expr $(,$indent:expr)? $(,)?) => { #[allow(unused)] let mut renderer = jotdown::html::Renderer::minified(); $(renderer = jotdown::html::Renderer::indented($indent);)? - let mut actual = String::new(); renderer - .push(jotdown::Parser::new($src), &mut actual) - .unwrap(); + .render_document($src).expect("Can't fail"); + let actual = renderer.into_inner(); assert_eq!(actual, $expected); }; ($src:expr, $expected:expr, $(,)?) => { diff --git a/tests/lib.rs b/tests/lib.rs index 3f06414a..e86ca523 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,3 +1,4 @@ mod attr; +mod filters; mod html; mod parse_events;