Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change write_to_html to allow fmt::Write #870

Merged
merged 3 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 50 additions & 16 deletions pulldown-cmark-escape/src/lib.rs
Original file line number Diff line number Diff line change
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 `Write` and types of the for `&mut W` where
notriddle marked this conversation as resolved.
Show resolved Hide resolved
/// `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
23 changes: 12 additions & 11 deletions pulldown-cmark/examples/event-filter.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
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).";
println!("Parsing the following markdown string:\n{}", markdown_input);

// 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();
}
6 changes: 3 additions & 3 deletions pulldown-cmark/examples/footnote-rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down 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(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.
Expand Down Expand Up @@ -89,7 +89,7 @@ fn main() {
.write_all(b"<hr><ol class=\"footnotes-list\">\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("");
Expand Down
28 changes: 13 additions & 15 deletions pulldown-cmark/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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#"
Expand All @@ -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#"<h1>hello</h1>
/// <ul>
Expand All @@ -550,10 +548,10 @@ where
/// </ul>
/// "#);
/// ```
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<Item = Event<'a>>,
W: Write,
W: StrWrite,
{
HtmlWriter::new(iter, WriteWrapper(writer)).run()
HtmlWriter::new(iter, writer).run()
}
1 change: 1 addition & 0 deletions pulldown-cmark/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
19 changes: 10 additions & 9 deletions pulldown-cmark/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(());
Expand Down Expand Up @@ -187,5 +188,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(IoWriter(buffer), &mut p);
}