From 081a4d2ca1ad8ff6a1387bad8d8f1fed2a7bf76e Mon Sep 17 00:00:00 2001 From: Stepan Tubanov Date: Mon, 25 Mar 2024 06:33:49 +0400 Subject: [PATCH 1/3] Change `write_to_html` to allow `fmt::Write` --- pulldown-cmark-escape/src/lib.rs | 66 ++++++++++++++++----- pulldown-cmark/examples/event-filter.rs | 23 +++---- pulldown-cmark/examples/footnote-rewrite.rs | 6 +- pulldown-cmark/src/html.rs | 28 ++++----- pulldown-cmark/src/lib.rs | 1 + pulldown-cmark/src/main.rs | 19 +++--- 6 files changed, 89 insertions(+), 54 deletions(-) diff --git a/pulldown-cmark-escape/src/lib.rs b/pulldown-cmark-escape/src/lib.rs index 4fc2d51a..d7881659 100644 --- a/pulldown-cmark-escape/src/lib.rs +++ b/pulldown-cmark-escape/src/lib.rs @@ -21,8 +21,8 @@ //! Utility functions for HTML escaping. Only useful when building your own //! HTML renderer. -use std::fmt::{Arguments, Write as FmtWrite}; -use std::io::{self, ErrorKind, Write}; +use std::fmt::{self, Arguments}; +use std::io::{self, Write}; use std::str::from_utf8; #[rustfmt::skip] @@ -46,20 +46,23 @@ static SINGLE_QUOTE_ESCAPE: &str = "'"; /// `W: StrWrite`. Since we need the latter a lot, we choose to wrap /// `Write` types. #[derive(Debug)] -pub struct WriteWrapper(pub W); +pub struct IoWriter(pub W); /// Trait that allows writing string slices. This is basically an extension /// of `std::io::Write` in order to include `String`. pub trait StrWrite { - fn write_str(&mut self, s: &str) -> io::Result<()>; + type Error; - fn write_fmt(&mut self, args: Arguments) -> io::Result<()>; + fn write_str(&mut self, s: &str) -> Result<(), Self::Error>; + fn write_fmt(&mut self, args: Arguments) -> Result<(), Self::Error>; } -impl StrWrite for WriteWrapper +impl StrWrite for IoWriter where W: Write, { + type Error = io::Error; + #[inline] fn write_str(&mut self, s: &str) -> io::Result<()> { self.0.write_all(s.as_bytes()) @@ -71,17 +74,42 @@ where } } +/// This wrapper exists because we can't have both a blanket implementation +/// for all types implementing `Write` and types of the for `&mut W` where +/// `W: StrWrite`. Since we need the latter a lot, we choose to wrap +/// `Write` types. +#[derive(Debug)] +pub struct FmtWriter(pub W); + +impl StrWrite for FmtWriter +where + W: fmt::Write, +{ + type Error = fmt::Error; + + #[inline] + fn write_str(&mut self, s: &str) -> fmt::Result { + self.0.write_str(s) + } + + #[inline] + fn write_fmt(&mut self, args: Arguments) -> fmt::Result { + self.0.write_fmt(args) + } +} + impl StrWrite for String { + type Error = fmt::Error; + #[inline] - fn write_str(&mut self, s: &str) -> io::Result<()> { + fn write_str(&mut self, s: &str) -> fmt::Result { self.push_str(s); Ok(()) } #[inline] - fn write_fmt(&mut self, args: Arguments) -> io::Result<()> { - // FIXME: translate fmt error to io error? - FmtWrite::write_fmt(self, args).map_err(|_| ErrorKind::Other.into()) + fn write_fmt(&mut self, args: Arguments) -> fmt::Result { + fmt::Write::write_fmt(self, args) } } @@ -89,19 +117,21 @@ impl StrWrite for &'_ mut W where W: StrWrite, { + type Error = W::Error; + #[inline] - fn write_str(&mut self, s: &str) -> io::Result<()> { + fn write_str(&mut self, s: &str) -> Result<(), Self::Error> { (**self).write_str(s) } #[inline] - fn write_fmt(&mut self, args: Arguments) -> io::Result<()> { + fn write_fmt(&mut self, args: Arguments) -> Result<(), Self::Error> { (**self).write_fmt(args) } } /// Writes an href to the buffer, escaping href unsafe bytes. -pub fn escape_href(mut w: W, s: &str) -> io::Result<()> +pub fn escape_href(mut w: W, s: &str) -> Result<(), W::Error> where W: StrWrite, { @@ -171,7 +201,7 @@ static HTML_ESCAPES: [&str; 6] = ["", "&", "<", ">", """, "'" /// // This is not okay. /// //let not_ok = format!("test"); /// ```` -pub fn escape_html(w: W, s: &str) -> io::Result<()> { +pub fn escape_html(w: W, s: &str) -> Result<(), W::Error> { #[cfg(all(target_arch = "x86_64", feature = "simd"))] { simd::escape_html(w, s, &HTML_ESCAPE_TABLE) @@ -200,7 +230,7 @@ pub fn escape_html(w: W, s: &str) -> io::Result<()> { /// It should always be correct, but will produce larger output. /// /// -pub fn escape_html_body_text(w: W, s: &str) -> io::Result<()> { +pub fn escape_html_body_text(w: W, s: &str) -> Result<(), W::Error> { #[cfg(all(target_arch = "x86_64", feature = "simd"))] { simd::escape_html(w, s, &HTML_BODY_TEXT_ESCAPE_TABLE) @@ -211,7 +241,11 @@ pub fn escape_html_body_text(w: W, s: &str) -> io::Result<()> { } } -fn escape_html_scalar(mut w: W, s: &str, table: &'static [u8; 256]) -> io::Result<()> { +fn escape_html_scalar( + mut w: W, + s: &str, + table: &'static [u8; 256], +) -> Result<(), W::Error> { let bytes = s.as_bytes(); let mut mark = 0; let mut i = 0; diff --git a/pulldown-cmark/examples/event-filter.rs b/pulldown-cmark/examples/event-filter.rs index a67e1210..f69101d0 100644 --- a/pulldown-cmark/examples/event-filter.rs +++ b/pulldown-cmark/examples/event-filter.rs @@ -1,6 +1,6 @@ use std::io::Write as _; -use pulldown_cmark::{html, Event, Options, Parser, Tag, TagEnd}; +use pulldown_cmark::{html, Event, IoWriter, Options, Parser, Tag, TagEnd}; fn main() { let markdown_input: &str = "This is Peter on ![holiday in Greece](pearl_beach.jpg)."; @@ -8,20 +8,21 @@ fn main() { // Set up parser. We can treat is as any other iterator. We replace Peter by John // and image by its alt text. - let parser = Parser::new_ext(markdown_input, Options::empty()) - .map(|event| match event { - Event::Text(text) => Event::Text(text.replace("Peter", "John").into()), - _ => event, - }) - .filter(|event| match event { - Event::Start(Tag::Image { .. }) | Event::End(TagEnd::Image) => false, - _ => true, - }); + let parser = + Parser::new_ext(markdown_input, Options::empty()) + .map(|event| match event { + Event::Text(text) => Event::Text(text.replace("Peter", "John").into()), + _ => event, + }) + .filter(|event| match event { + Event::Start(Tag::Image { .. }) | Event::End(TagEnd::Image) => false, + _ => true, + }); // Write to anything implementing the `Write` trait. This could also be a file // or network socket. let stdout = std::io::stdout(); let mut handle = stdout.lock(); handle.write_all(b"\nHTML output:\n").unwrap(); - html::write_html(&mut handle, parser).unwrap(); + html::write_html(IoWriter(&mut handle), parser).unwrap(); } diff --git a/pulldown-cmark/examples/footnote-rewrite.rs b/pulldown-cmark/examples/footnote-rewrite.rs index 8d9e1642..9f7ed091 100644 --- a/pulldown-cmark/examples/footnote-rewrite.rs +++ b/pulldown-cmark/examples/footnote-rewrite.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt::Write as _; use std::io::Write as _; -use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag, TagEnd}; +use pulldown_cmark::{html, CowStr, Event, IoWriter, Options, Parser, Tag, TagEnd}; /// This example shows how to do footnotes as bottom-notes, in the style of GitHub. fn main() { @@ -53,7 +53,7 @@ fn main() { let stdout = std::io::stdout(); let mut handle = stdout.lock(); handle.write_all(b"\nHTML output:\n").unwrap(); - html::write_html(&mut handle, parser).unwrap(); + html::write_html(IoWriter(&mut handle), parser).unwrap(); // To make the footnotes look right, we need to sort them by their appearance order, not by // the in-tree order of their actual definitions. Unused items are omitted entirely. @@ -89,7 +89,7 @@ fn main() { .write_all(b"
    \n") .unwrap(); html::write_html( - &mut handle, + IoWriter(&mut handle), footnotes.into_iter().flat_map(|fl| { // To write backrefs, the name needs kept until the end of the footnote definition. let mut name = CowStr::from(""); diff --git a/pulldown-cmark/src/html.rs b/pulldown-cmark/src/html.rs index a180c344..bf345b41 100644 --- a/pulldown-cmark/src/html.rs +++ b/pulldown-cmark/src/html.rs @@ -21,14 +21,11 @@ //! HTML renderer that takes an iterator of events as input. use std::collections::HashMap; -use std::io::{self, Write}; use crate::strings::CowStr; use crate::Event::*; use crate::{Alignment, BlockQuoteKind, CodeBlockKind, Event, LinkType, Tag, TagEnd}; -use pulldown_cmark_escape::{ - escape_href, escape_html, escape_html_body_text, StrWrite, WriteWrapper, -}; +use pulldown_cmark_escape::{escape_href, escape_html, escape_html_body_text, StrWrite}; enum TableState { Head, @@ -73,14 +70,15 @@ where } /// Writes a new line. - fn write_newline(&mut self) -> io::Result<()> { + #[inline] + fn write_newline(&mut self) -> Result<(), W::Error> { self.end_newline = true; self.writer.write_str("\n") } /// Writes a buffer, and tracks whether or not a newline was written. #[inline] - fn write(&mut self, s: &str) -> io::Result<()> { + fn write(&mut self, s: &str) -> Result<(), W::Error> { self.writer.write_str(s)?; if !s.is_empty() { @@ -89,7 +87,7 @@ where Ok(()) } - fn run(mut self) -> io::Result<()> { + fn run(mut self) -> Result<(), W::Error> { while let Some(event) = self.iter.next() { match event { Start(tag) => { @@ -156,7 +154,7 @@ where } /// Writes the start of an HTML tag. - fn start_tag(&mut self, tag: Tag<'a>) -> io::Result<()> { + fn start_tag(&mut self, tag: Tag<'a>) -> Result<(), W::Error> { match tag { Tag::HtmlBlock => Ok(()), Tag::Paragraph => { @@ -368,7 +366,7 @@ where } } - fn end_tag(&mut self, tag: TagEnd) -> io::Result<()> { + fn end_tag(&mut self, tag: TagEnd) -> Result<(), W::Error> { match tag { TagEnd::HtmlBlock => {} TagEnd::Paragraph => { @@ -439,7 +437,7 @@ where } // run raw text, consuming end tag - fn raw_text(&mut self) -> io::Result<()> { + fn raw_text(&mut self) -> Result<(), W::Error> { let mut nest = 0; while let Some(event) = self.iter.next() { match event { @@ -528,7 +526,7 @@ where /// # Examples /// /// ``` -/// use pulldown_cmark::{html, Parser}; +/// use pulldown_cmark::{html, Parser, IoWriter}; /// use std::io::Cursor; /// /// let markdown_str = r#" @@ -541,7 +539,7 @@ where /// let mut bytes = Vec::new(); /// let parser = Parser::new(markdown_str); /// -/// html::write_html(Cursor::new(&mut bytes), parser); +/// html::write_html(IoWriter(Cursor::new(&mut bytes)), parser); /// /// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"

    hello

    ///
      @@ -550,10 +548,10 @@ where ///
    /// "#); /// ``` -pub fn write_html<'a, I, W>(writer: W, iter: I) -> io::Result<()> +pub fn write_html<'a, I, W>(writer: W, iter: I) -> Result<(), W::Error> where I: Iterator>, - W: Write, + W: StrWrite, { - HtmlWriter::new(iter, WriteWrapper(writer)).run() + HtmlWriter::new(iter, writer).run() } diff --git a/pulldown-cmark/src/lib.rs b/pulldown-cmark/src/lib.rs index 3c16b0e5..6198ada6 100644 --- a/pulldown-cmark/src/lib.rs +++ b/pulldown-cmark/src/lib.rs @@ -101,6 +101,7 @@ pub use crate::parse::{ }; pub use crate::strings::{CowStr, InlineStr}; pub use crate::utils::*; +pub use pulldown_cmark_escape::{FmtWriter, IoWriter, StrWrite}; /// Codeblock kind. #[derive(Clone, Debug, PartialEq)] diff --git a/pulldown-cmark/src/main.rs b/pulldown-cmark/src/main.rs index 8ff59d14..517b43b4 100644 --- a/pulldown-cmark/src/main.rs +++ b/pulldown-cmark/src/main.rs @@ -22,7 +22,7 @@ #![forbid(unsafe_code)] -use pulldown_cmark::{html, BrokenLink, Options, Parser}; +use pulldown_cmark::{html, BrokenLink, IoWriter, Options, Parser}; use std::env; use std::fs::File; @@ -94,13 +94,14 @@ pub fn main() -> std::io::Result<()> { "fail if input file has broken links", ); - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - eprintln!("{}\n{}", f, opts.usage(&brief(&args[0]))); - std::process::exit(1); - } - }; + let matches = + match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => { + eprintln!("{}\n{}", f, opts.usage(&brief(&args[0]))); + std::process::exit(1); + } + }; if matches.opt_present("help") { println!("{}", opts.usage(&brief(&args[0]))); return Ok(()); @@ -187,5 +188,5 @@ pub fn pulldown_cmark(input: &str, opts: Options, broken_links: &mut Vec Date: Tue, 16 Apr 2024 13:49:19 -0700 Subject: [PATCH 2/3] Update pulldown-cmark-escape/src/lib.rs Co-authored-by: Roope Salmi --- pulldown-cmark-escape/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulldown-cmark-escape/src/lib.rs b/pulldown-cmark-escape/src/lib.rs index d7881659..cc14a2f1 100644 --- a/pulldown-cmark-escape/src/lib.rs +++ b/pulldown-cmark-escape/src/lib.rs @@ -75,7 +75,7 @@ where } /// This wrapper exists because we can't have both a blanket implementation -/// for all types implementing `Write` and types of the for `&mut W` where +/// for all types implementing `io::Write` and types of the form `&mut W` where /// `W: StrWrite`. Since we need the latter a lot, we choose to wrap /// `Write` types. #[derive(Debug)] From cefabf394fa1f741a25d315929fc772c2e6eaae4 Mon Sep 17 00:00:00 2001 From: Stepan Tubanov Date: Wed, 17 Apr 2024 01:12:49 +0400 Subject: [PATCH 3/3] do not reexport writer wrappers --- pulldown-cmark/examples/event-filter.rs | 23 +++++---- pulldown-cmark/examples/footnote-rewrite.rs | 8 ++-- pulldown-cmark/src/html.rs | 53 +++++++++++++++++---- pulldown-cmark/src/lib.rs | 1 - pulldown-cmark/src/main.rs | 19 ++++---- 5 files changed, 69 insertions(+), 35 deletions(-) diff --git a/pulldown-cmark/examples/event-filter.rs b/pulldown-cmark/examples/event-filter.rs index f69101d0..c2e25576 100644 --- a/pulldown-cmark/examples/event-filter.rs +++ b/pulldown-cmark/examples/event-filter.rs @@ -1,6 +1,6 @@ use std::io::Write as _; -use pulldown_cmark::{html, Event, IoWriter, Options, Parser, Tag, TagEnd}; +use pulldown_cmark::{html, Event, Options, Parser, Tag, TagEnd}; fn main() { let markdown_input: &str = "This is Peter on ![holiday in Greece](pearl_beach.jpg)."; @@ -8,21 +8,20 @@ fn main() { // Set up parser. We can treat is as any other iterator. We replace Peter by John // and image by its alt text. - let parser = - Parser::new_ext(markdown_input, Options::empty()) - .map(|event| match event { - Event::Text(text) => Event::Text(text.replace("Peter", "John").into()), - _ => event, - }) - .filter(|event| match event { - Event::Start(Tag::Image { .. }) | Event::End(TagEnd::Image) => false, - _ => true, - }); + let parser = Parser::new_ext(markdown_input, Options::empty()) + .map(|event| match event { + Event::Text(text) => Event::Text(text.replace("Peter", "John").into()), + _ => event, + }) + .filter(|event| match event { + Event::Start(Tag::Image { .. }) | Event::End(TagEnd::Image) => false, + _ => true, + }); // Write to anything implementing the `Write` trait. This could also be a file // or network socket. let stdout = std::io::stdout(); let mut handle = stdout.lock(); handle.write_all(b"\nHTML output:\n").unwrap(); - html::write_html(IoWriter(&mut handle), parser).unwrap(); + html::write_html_io(&mut handle, parser).unwrap(); } diff --git a/pulldown-cmark/examples/footnote-rewrite.rs b/pulldown-cmark/examples/footnote-rewrite.rs index 9f7ed091..a550c3e3 100644 --- a/pulldown-cmark/examples/footnote-rewrite.rs +++ b/pulldown-cmark/examples/footnote-rewrite.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt::Write as _; use std::io::Write as _; -use pulldown_cmark::{html, CowStr, Event, IoWriter, Options, Parser, Tag, TagEnd}; +use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag, TagEnd}; /// This example shows how to do footnotes as bottom-notes, in the style of GitHub. fn main() { @@ -53,7 +53,7 @@ fn main() { let stdout = std::io::stdout(); let mut handle = stdout.lock(); handle.write_all(b"\nHTML output:\n").unwrap(); - html::write_html(IoWriter(&mut handle), parser).unwrap(); + html::write_html_io(&mut handle, parser).unwrap(); // To make the footnotes look right, we need to sort them by their appearance order, not by // the in-tree order of their actual definitions. Unused items are omitted entirely. @@ -88,8 +88,8 @@ fn main() { handle .write_all(b"
      \n") .unwrap(); - html::write_html( - IoWriter(&mut handle), + html::write_html_io( + &mut handle, footnotes.into_iter().flat_map(|fl| { // To write backrefs, the name needs kept until the end of the footnote definition. let mut name = CowStr::from(""); diff --git a/pulldown-cmark/src/html.rs b/pulldown-cmark/src/html.rs index bf345b41..9819674a 100644 --- a/pulldown-cmark/src/html.rs +++ b/pulldown-cmark/src/html.rs @@ -25,7 +25,9 @@ use std::collections::HashMap; use crate::strings::CowStr; use crate::Event::*; use crate::{Alignment, BlockQuoteKind, CodeBlockKind, Event, LinkType, Tag, TagEnd}; -use pulldown_cmark_escape::{escape_href, escape_html, escape_html_body_text, StrWrite}; +use pulldown_cmark_escape::{ + escape_href, escape_html, escape_html_body_text, FmtWriter, IoWriter, StrWrite, +}; enum TableState { Head, @@ -512,11 +514,11 @@ pub fn push_html<'a, I>(s: &mut String, iter: I) where I: Iterator>, { - HtmlWriter::new(iter, s).run().unwrap(); + write_html_fmt(s, iter).unwrap() } /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and -/// write it out to a writable stream. +/// write it out to an I/O stream. /// /// **Note**: using this function with an unbuffered writer like a file or socket /// will result in poor performance. Wrap these in a @@ -526,7 +528,7 @@ where /// # Examples /// /// ``` -/// use pulldown_cmark::{html, Parser, IoWriter}; +/// use pulldown_cmark::{html, Parser}; /// use std::io::Cursor; /// /// let markdown_str = r#" @@ -539,7 +541,7 @@ where /// let mut bytes = Vec::new(); /// let parser = Parser::new(markdown_str); /// -/// html::write_html(IoWriter(Cursor::new(&mut bytes)), parser); +/// html::write_html_io(Cursor::new(&mut bytes), parser); /// /// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"

      hello

      ///
        @@ -548,10 +550,45 @@ where ///
      /// "#); /// ``` -pub fn write_html<'a, I, W>(writer: W, iter: I) -> Result<(), W::Error> +pub fn write_html_io<'a, I, W>(writer: W, iter: I) -> std::io::Result<()> where I: Iterator>, - W: StrWrite, + W: std::io::Write, +{ + HtmlWriter::new(iter, IoWriter(writer)).run() +} + +/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and +/// write it into Unicode-accepting buffer or stream. +/// +/// # Examples +/// +/// ``` +/// use pulldown_cmark::{html, Parser}; +/// +/// let markdown_str = r#" +/// hello +/// ===== +/// +/// * alpha +/// * beta +/// "#; +/// let mut buf = String::new(); +/// let parser = Parser::new(markdown_str); +/// +/// html::write_html_fmt(&mut buf, parser); +/// +/// assert_eq!(buf, r#"

      hello

      +///
        +///
      • alpha
      • +///
      • beta
      • +///
      +/// "#); +/// ``` +pub fn write_html_fmt<'a, I, W>(writer: W, iter: I) -> std::fmt::Result +where + I: Iterator>, + W: std::fmt::Write, { - HtmlWriter::new(iter, writer).run() + HtmlWriter::new(iter, FmtWriter(writer)).run() } diff --git a/pulldown-cmark/src/lib.rs b/pulldown-cmark/src/lib.rs index 6198ada6..3c16b0e5 100644 --- a/pulldown-cmark/src/lib.rs +++ b/pulldown-cmark/src/lib.rs @@ -101,7 +101,6 @@ pub use crate::parse::{ }; pub use crate::strings::{CowStr, InlineStr}; pub use crate::utils::*; -pub use pulldown_cmark_escape::{FmtWriter, IoWriter, StrWrite}; /// Codeblock kind. #[derive(Clone, Debug, PartialEq)] diff --git a/pulldown-cmark/src/main.rs b/pulldown-cmark/src/main.rs index 517b43b4..c0c26289 100644 --- a/pulldown-cmark/src/main.rs +++ b/pulldown-cmark/src/main.rs @@ -22,7 +22,7 @@ #![forbid(unsafe_code)] -use pulldown_cmark::{html, BrokenLink, IoWriter, Options, Parser}; +use pulldown_cmark::{html, BrokenLink, Options, Parser}; use std::env; use std::fs::File; @@ -94,14 +94,13 @@ pub fn main() -> std::io::Result<()> { "fail if input file has broken links", ); - let matches = - match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - eprintln!("{}\n{}", f, opts.usage(&brief(&args[0]))); - std::process::exit(1); - } - }; + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => { + eprintln!("{}\n{}", f, opts.usage(&brief(&args[0]))); + std::process::exit(1); + } + }; if matches.opt_present("help") { println!("{}", opts.usage(&brief(&args[0]))); return Ok(()); @@ -188,5 +187,5 @@ pub fn pulldown_cmark(input: &str, opts: Options, broken_links: &mut Vec