Skip to content

Commit

Permalink
Merge pull request #870 from stepantubanov/write-to-fmt
Browse files Browse the repository at this point in the history
Change `write_to_html` to allow `fmt::Write`
  • Loading branch information
Martin1887 committed Apr 17, 2024
2 parents 2540d40 + cefabf3 commit 7a1e53c
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 34 deletions.
66 changes: 50 additions & 16 deletions pulldown-cmark-escape/src/lib.rs
Expand Up @@ -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]
Expand All @@ -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<W>(pub W);
pub struct IoWriter<W>(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<W> StrWrite for WriteWrapper<W>
impl<W> StrWrite for IoWriter<W>
where
W: Write,
{
type Error = io::Error;

#[inline]
fn write_str(&mut self, s: &str) -> io::Result<()> {
self.0.write_all(s.as_bytes())
Expand All @@ -71,37 +74,64 @@ where
}
}

/// This wrapper exists because we can't have both a blanket implementation
/// 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)]
pub struct FmtWriter<W>(pub W);

impl<W> StrWrite for FmtWriter<W>
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)
}
}

impl<W> 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<W>(mut w: W, s: &str) -> io::Result<()>
pub fn escape_href<W>(mut w: W, s: &str) -> Result<(), W::Error>
where
W: StrWrite,
{
Expand Down Expand Up @@ -171,7 +201,7 @@ static HTML_ESCAPES: [&str; 6] = ["", "&amp;", "&lt;", "&gt;", "&quot;", "&#39;"
/// // This is not okay.
/// //let not_ok = format!("<a title={value}>test</a>");
/// ````
pub fn escape_html<W: StrWrite>(w: W, s: &str) -> io::Result<()> {
pub fn escape_html<W: StrWrite>(w: W, s: &str) -> Result<(), W::Error> {
#[cfg(all(target_arch = "x86_64", feature = "simd"))]
{
simd::escape_html(w, s, &HTML_ESCAPE_TABLE)
Expand Down Expand Up @@ -200,7 +230,7 @@ pub fn escape_html<W: StrWrite>(w: W, s: &str) -> io::Result<()> {
/// It should always be correct, but will produce larger output.
///
/// </div>
pub fn escape_html_body_text<W: StrWrite>(w: W, s: &str) -> io::Result<()> {
pub fn escape_html_body_text<W: StrWrite>(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)
Expand All @@ -211,7 +241,11 @@ pub fn escape_html_body_text<W: StrWrite>(w: W, s: &str) -> io::Result<()> {
}
}

fn escape_html_scalar<W: StrWrite>(mut w: W, s: &str, table: &'static [u8; 256]) -> io::Result<()> {
fn escape_html_scalar<W: StrWrite>(
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;
Expand Down
2 changes: 1 addition & 1 deletion pulldown-cmark/examples/event-filter.rs
Expand Up @@ -23,5 +23,5 @@ 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_io(&mut handle, parser).unwrap();
}
4 changes: 2 additions & 2 deletions pulldown-cmark/examples/footnote-rewrite.rs
Expand Up @@ -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_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.
Expand Down Expand Up @@ -88,7 +88,7 @@ fn main() {
handle
.write_all(b"<hr><ol class=\"footnotes-list\">\n")
.unwrap();
html::write_html(
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.
Expand Down
63 changes: 49 additions & 14 deletions pulldown-cmark/src/html.rs
Expand Up @@ -21,13 +21,12 @@
//! 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,
escape_href, escape_html, escape_html_body_text, FmtWriter, IoWriter, StrWrite,
};

enum TableState {
Expand Down Expand Up @@ -73,14 +72,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() {
Expand All @@ -89,7 +89,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) => {
Expand Down Expand Up @@ -156,7 +156,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 => {
Expand Down Expand Up @@ -368,7 +368,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 => {
Expand Down Expand Up @@ -439,7 +439,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 {
Expand Down Expand Up @@ -514,11 +514,11 @@ pub fn push_html<'a, I>(s: &mut String, iter: I)
where
I: Iterator<Item = Event<'a>>,
{
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
Expand All @@ -541,7 +541,7 @@ where
/// let mut bytes = Vec::new();
/// let parser = Parser::new(markdown_str);
///
/// html::write_html(Cursor::new(&mut bytes), parser);
/// html::write_html_io(Cursor::new(&mut bytes), parser);
///
/// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"<h1>hello</h1>
/// <ul>
Expand All @@ -550,10 +550,45 @@ where
/// </ul>
/// "#);
/// ```
pub fn write_html<'a, I, W>(writer: W, iter: I) -> io::Result<()>
pub fn write_html_io<'a, I, W>(writer: W, iter: I) -> std::io::Result<()>
where
I: Iterator<Item = Event<'a>>,
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#"<h1>hello</h1>
/// <ul>
/// <li>alpha</li>
/// <li>beta</li>
/// </ul>
/// "#);
/// ```
pub fn write_html_fmt<'a, I, W>(writer: W, iter: I) -> std::fmt::Result
where
I: Iterator<Item = Event<'a>>,
W: Write,
W: std::fmt::Write,
{
HtmlWriter::new(iter, WriteWrapper(writer)).run()
HtmlWriter::new(iter, FmtWriter(writer)).run()
}
2 changes: 1 addition & 1 deletion pulldown-cmark/src/main.rs
Expand Up @@ -187,5 +187,5 @@ pub fn pulldown_cmark(input: &str, opts: Options, broken_links: &mut Vec<BrokenL
);
let stdio = io::stdout();
let buffer = std::io::BufWriter::with_capacity(1024 * 1024, stdio.lock());
let _ = html::write_html(buffer, &mut p);
let _ = html::write_html_io(buffer, &mut p);
}

0 comments on commit 7a1e53c

Please sign in to comment.