PR #29 Allow rendering borrowed events

Merge branch 'render_ref'

closes #29
This commit is contained in:
Noah Hellman 2023-03-25 18:43:51 +01:00
commit 8893281310
8 changed files with 605 additions and 464 deletions

View file

@ -51,7 +51,9 @@ fn gen_html(c: &mut criterion::Criterion) {
|| jotdown::Parser::new(input).collect::<Vec<_>>(), || jotdown::Parser::new(input).collect::<Vec<_>>(),
|p| { |p| {
let mut s = String::new(); 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 s
}, },
criterion::BatchSize::SmallInput, criterion::BatchSize::SmallInput,
@ -62,6 +64,60 @@ fn gen_html(c: &mut criterion::Criterion) {
} }
criterion_group!(html, gen_html); 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::<Vec<_>>(),
|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::<Vec<_>>(),
|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) { fn gen_full(c: &mut criterion::Criterion) {
let mut group = c.benchmark_group("full"); let mut group = c.benchmark_group("full");
for (name, input) in bench_input::INPUTS { for (name, input) in bench_input::INPUTS {
@ -72,7 +128,7 @@ fn gen_full(c: &mut criterion::Criterion) {
|b, &input| { |b, &input| {
b.iter_with_large_drop(|| { b.iter_with_large_drop(|| {
let mut s = String::new(); let mut s = String::new();
jotdown::html::Renderer jotdown::html::Renderer::default()
.push(jotdown::Parser::new(input), &mut s) .push(jotdown::Parser::new(input), &mut s)
.unwrap(); .unwrap();
s s
@ -83,4 +139,4 @@ fn gen_full(c: &mut criterion::Criterion) {
} }
criterion_group!(full, gen_full); criterion_group!(full, gen_full);
criterion_main!(block, inline, html, full); criterion_main!(block, inline, html, html_borrow, html_clone, full);

View file

@ -12,7 +12,7 @@ fn block_inline() -> Option<jotdown::Event<'static>> {
fn full() -> String { fn full() -> String {
let mut s = String::new(); let mut s = String::new();
jotdown::html::Renderer jotdown::html::Renderer::default()
.push(jotdown::Parser::new(bench_input::ALL), &mut s) .push(jotdown::Parser::new(bench_input::ALL), &mut s)
.unwrap(); .unwrap();
s s

View file

@ -7,6 +7,8 @@ use jotdown::Render;
pub fn jotdown_render(djot: &str) -> String { pub fn jotdown_render(djot: &str) -> String {
let events = jotdown::Parser::new(djot); let events = jotdown::Parser::new(djot);
let mut html = String::new(); let mut html = String::new();
jotdown::html::Renderer.push(events, &mut html).unwrap(); jotdown::html::Renderer::default()
.push(events, &mut html)
.unwrap();
html html
} }

View file

@ -1,26 +1,4 @@
//! An HTML renderer that takes an iterator of [`Event`]s and emits HTML. //! 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::Alignment;
use crate::Container; use crate::Container;
@ -31,91 +9,74 @@ use crate::OrderedListNumbering::*;
use crate::Render; use crate::Render;
use crate::SpanLinkType; use crate::SpanLinkType;
pub struct Renderer;
impl Render for Renderer {
fn push<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write>(
&self,
events: I,
out: W,
) -> std::fmt::Result {
Writer::new(events, out).write()
}
}
enum Raw { enum Raw {
None, None,
Html, Html,
Other, Other,
} }
struct FilteredEvents<I> { pub struct Renderer {
events: I,
}
impl<'s, I: Iterator<Item = Event<'s>>> Iterator for FilteredEvents<I> {
type Item = Event<'s>;
fn next(&mut self) -> Option<Self::Item> {
let mut ev = self.events.next();
while matches!(ev, Some(Event::Blankline | Event::Escape)) {
ev = self.events.next();
}
ev
}
}
struct Writer<'s, I: Iterator<Item = Event<'s>>, W> {
events: std::iter::Peekable<FilteredEvents<I>>,
out: W,
raw: Raw, raw: Raw,
img_alt_text: usize, img_alt_text: usize,
list_tightness: Vec<bool>, list_tightness: Vec<bool>,
encountered_footnote: bool, encountered_footnote: bool,
footnote_number: Option<std::num::NonZeroUsize>, footnote_number: Option<std::num::NonZeroUsize>,
footnote_backlink_written: bool,
first_line: bool, first_line: bool,
close_para: bool,
} }
impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> { impl Default for Renderer {
fn new(events: I, out: W) -> Self { fn default() -> Self {
Self { Self {
events: FilteredEvents { events }.peekable(),
out,
raw: Raw::None, raw: Raw::None,
img_alt_text: 0, img_alt_text: 0,
list_tightness: Vec::new(), list_tightness: Vec::new(),
encountered_footnote: false, encountered_footnote: false,
footnote_number: None, footnote_number: None,
footnote_backlink_written: false,
first_line: true, first_line: true,
close_para: false,
}
}
}
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,
{
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("</p>")?;
} }
} }
fn write(&mut self) -> std::fmt::Result {
while let Some(e) = self.events.next() {
match e { match e {
Event::Start(c, attrs) => { Event::Start(c, attrs) => {
if c.is_block() && !self.first_line { 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(..)) { if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
continue; return Ok(());
} }
match &c { match &c {
Container::Blockquote => self.out.write_str("<blockquote")?, Container::Blockquote => out.write_str("<blockquote")?,
Container::List { kind, tight } => { Container::List { kind, tight } => {
self.list_tightness.push(*tight); self.list_tightness.push(*tight);
match kind { match kind {
ListKind::Unordered | ListKind::Task => { ListKind::Unordered | ListKind::Task => out.write_str("<ul")?,
self.out.write_str("<ul")?
}
ListKind::Ordered { ListKind::Ordered {
numbering, start, .. numbering, start, ..
} => { } => {
self.out.write_str("<ol")?; out.write_str("<ol")?;
if *start > 1 { if *start > 1 {
write!(self.out, r#" start="{}""#, start)?; write!(out, r#" start="{}""#, start)?;
} }
if let Some(ty) = match numbering { if let Some(ty) = match numbering {
Decimal => None, Decimal => None,
@ -124,87 +85,85 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
RomanLower => Some('i'), RomanLower => Some('i'),
RomanUpper => Some('I'), RomanUpper => Some('I'),
} { } {
write!(self.out, r#" type="{}""#, ty)?; write!(out, r#" type="{}""#, ty)?;
} }
} }
} }
} }
Container::ListItem | Container::TaskListItem { .. } => { Container::ListItem | Container::TaskListItem { .. } => {
self.out.write_str("<li")?; out.write_str("<li")?;
} }
Container::DescriptionList => self.out.write_str("<dl")?, Container::DescriptionList => out.write_str("<dl")?,
Container::DescriptionDetails => self.out.write_str("<dd")?, Container::DescriptionDetails => out.write_str("<dd")?,
Container::Footnote { number, .. } => { Container::Footnote { number, .. } => {
assert!(self.footnote_number.is_none()); assert!(self.footnote_number.is_none());
self.footnote_number = Some((*number).try_into().unwrap()); self.footnote_number = Some((*number).try_into().unwrap());
if !self.encountered_footnote { if !self.encountered_footnote {
self.encountered_footnote = true; self.encountered_footnote = true;
self.out out.write_str("<section role=\"doc-endnotes\">\n<hr>\n<ol>\n")?;
.write_str("<section role=\"doc-endnotes\">\n<hr>\n<ol>\n")?;
} }
write!(self.out, "<li id=\"fn{}\">", number)?; write!(out, "<li id=\"fn{}\">", number)?;
self.footnote_backlink_written = false; return Ok(());
continue;
} }
Container::Table => self.out.write_str("<table")?, Container::Table => out.write_str("<table")?,
Container::TableRow { .. } => self.out.write_str("<tr")?, Container::TableRow { .. } => out.write_str("<tr")?,
Container::Section { .. } => self.out.write_str("<section")?, Container::Section { .. } => out.write_str("<section")?,
Container::Div { .. } => self.out.write_str("<div")?, Container::Div { .. } => out.write_str("<div")?,
Container::Paragraph => { Container::Paragraph => {
if matches!(self.list_tightness.last(), Some(true)) { if matches!(self.list_tightness.last(), Some(true)) {
continue; return Ok(());
} }
self.out.write_str("<p")?; out.write_str("<p")?;
} }
Container::Heading { level, .. } => write!(self.out, "<h{}", level)?, Container::Heading { level, .. } => write!(out, "<h{}", level)?,
Container::TableCell { head: false, .. } => self.out.write_str("<td")?, Container::TableCell { head: false, .. } => out.write_str("<td")?,
Container::TableCell { head: true, .. } => self.out.write_str("<th")?, Container::TableCell { head: true, .. } => out.write_str("<th")?,
Container::Caption => self.out.write_str("<caption")?, Container::Caption => out.write_str("<caption")?,
Container::DescriptionTerm => self.out.write_str("<dt")?, Container::DescriptionTerm => out.write_str("<dt")?,
Container::CodeBlock { .. } => self.out.write_str("<pre")?, Container::CodeBlock { .. } => out.write_str("<pre")?,
Container::Span | Container::Math { .. } => self.out.write_str("<span")?, Container::Span | Container::Math { .. } => out.write_str("<span")?,
Container::Link(dst, ty) => { Container::Link(dst, ty) => {
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) { if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
self.out.write_str("<a")?; out.write_str("<a")?;
} else { } else {
self.out.write_str(r#"<a href=""#)?; out.write_str(r#"<a href=""#)?;
if matches!(ty, LinkType::Email) { if matches!(ty, LinkType::Email) {
self.out.write_str("mailto:")?; out.write_str("mailto:")?;
} }
self.write_attr(dst)?; write_attr(dst, &mut out)?;
self.out.write_char('"')?; out.write_char('"')?;
} }
} }
Container::Image(..) => { Container::Image(..) => {
self.img_alt_text += 1; self.img_alt_text += 1;
if self.img_alt_text == 1 { if self.img_alt_text == 1 {
self.out.write_str("<img")?; out.write_str("<img")?;
} else { } else {
continue; return Ok(());
} }
} }
Container::Verbatim => self.out.write_str("<code")?, Container::Verbatim => out.write_str("<code")?,
Container::RawBlock { format } | Container::RawInline { format } => { Container::RawBlock { format } | Container::RawInline { format } => {
self.raw = if format == &"html" { self.raw = if format == &"html" {
Raw::Html Raw::Html
} else { } else {
Raw::Other Raw::Other
}; };
continue; return Ok(());
} }
Container::Subscript => self.out.write_str("<sub")?, Container::Subscript => out.write_str("<sub")?,
Container::Superscript => self.out.write_str("<sup")?, Container::Superscript => out.write_str("<sup")?,
Container::Insert => self.out.write_str("<ins")?, Container::Insert => out.write_str("<ins")?,
Container::Delete => self.out.write_str("<del")?, Container::Delete => out.write_str("<del")?,
Container::Strong => self.out.write_str("<strong")?, Container::Strong => out.write_str("<strong")?,
Container::Emphasis => self.out.write_str("<em")?, Container::Emphasis => out.write_str("<em")?,
Container::Mark => self.out.write_str("<mark")?, Container::Mark => out.write_str("<mark")?,
} }
for (a, v) in attrs.iter().filter(|(a, _)| *a != "class") { for (a, v) in attrs.iter().filter(|(a, _)| *a != "class") {
write!(self.out, r#" {}=""#, a)?; write!(out, r#" {}=""#, a)?;
v.parts().try_for_each(|part| self.write_attr(part))?; v.parts().try_for_each(|part| write_attr(part, &mut out))?;
self.out.write_char('"')?; out.write_char('"')?;
} }
if let Container::Heading { if let Container::Heading {
@ -215,9 +174,9 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
| Container::Section { id } = &c | Container::Section { id } = &c
{ {
if !attrs.iter().any(|(a, _)| a == "id") { if !attrs.iter().any(|(a, _)| a == "id") {
self.out.write_str(r#" id=""#)?; out.write_str(r#" id=""#)?;
self.write_attr(id)?; write_attr(id, &mut out)?;
self.out.write_char('"')?; out.write_char('"')?;
} }
} }
@ -233,7 +192,7 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
| Container::TaskListItem { .. } | Container::TaskListItem { .. }
) )
{ {
self.out.write_str(r#" class=""#)?; out.write_str(r#" class=""#)?;
let mut first_written = false; let mut first_written = false;
if let Some(cls) = match c { if let Some(cls) = match c {
Container::List { Container::List {
@ -247,7 +206,7 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
_ => None, _ => None,
} { } {
first_written = true; first_written = true;
self.out.write_str(cls)?; out.write_str(cls)?;
} }
for cls in attrs for cls in attrs
.iter() .iter()
@ -255,19 +214,20 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
.map(|(_, cls)| cls) .map(|(_, cls)| cls)
{ {
if first_written { if first_written {
self.out.write_char(' ')?; out.write_char(' ')?;
} }
first_written = true; 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 // div class goes after classes from attrs
if let Container::Div { class: Some(cls) } = c { if let Container::Div { class: Some(cls) } = c {
if first_written { 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 { match c {
@ -280,109 +240,101 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
Alignment::Center => "center", Alignment::Center => "center",
Alignment::Right => "right", Alignment::Right => "right",
}; };
write!(self.out, r#" style="text-align: {};">"#, a)?; write!(out, r#" style="text-align: {};">"#, a)?;
} }
Container::CodeBlock { lang } => { Container::CodeBlock { lang } => {
if let Some(l) = lang { if let Some(l) = lang {
self.out.write_str(r#"><code class="language-"#)?; out.write_str(r#"><code class="language-"#)?;
self.write_attr(l)?; write_attr(l, &mut out)?;
self.out.write_str(r#"">"#)?; out.write_str(r#"">"#)?;
} else { } else {
self.out.write_str("><code>")?; out.write_str("><code>")?;
} }
} }
Container::Image(..) => { Container::Image(..) => {
if self.img_alt_text == 1 { if self.img_alt_text == 1 {
self.out.write_str(r#" alt=""#)?; out.write_str(r#" alt=""#)?;
} }
} }
Container::Math { display } => { Container::Math { display } => {
self.out out.write_str(if *display { r#">\["# } else { r#">\("# })?;
.write_str(if display { r#">\["# } else { r#">\("# })?;
} }
_ => self.out.write_char('>')?, _ => out.write_char('>')?,
} }
} }
Event::End(c) => { Event::End(c) => {
if c.is_block_container() && !matches!(c, Container::Footnote { .. }) { 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(..)) { if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
continue; return Ok(());
} }
match c { match c {
Container::Blockquote => self.out.write_str("</blockquote>")?, Container::Blockquote => out.write_str("</blockquote>")?,
Container::List { Container::List {
kind: ListKind::Unordered | ListKind::Task, kind: ListKind::Unordered | ListKind::Task,
.. ..
} => { } => {
self.list_tightness.pop(); self.list_tightness.pop();
self.out.write_str("</ul>")?; out.write_str("</ul>")?;
} }
Container::List { Container::List {
kind: ListKind::Ordered { .. }, kind: ListKind::Ordered { .. },
.. ..
} => self.out.write_str("</ol>")?, } => out.write_str("</ol>")?,
Container::ListItem | Container::TaskListItem { .. } => { Container::ListItem | Container::TaskListItem { .. } => {
self.out.write_str("</li>")?; out.write_str("</li>")?;
} }
Container::DescriptionList => self.out.write_str("</dl>")?, Container::DescriptionList => out.write_str("</dl>")?,
Container::DescriptionDetails => self.out.write_str("</dd>")?, Container::DescriptionDetails => out.write_str("</dd>")?,
Container::Footnote { number, .. } => { Container::Footnote { number, .. } => {
if !self.footnote_backlink_written { if !close_para {
// create a new paragraph
out.write_str("\n<p>")?;
}
write!( write!(
self.out, out,
"\n<p><a href=\"#fnref{}\" role=\"doc-backlink\">↩︎︎</a></p>", r##"<a href="#fnref{}" role="doc-backlink">↩︎︎</a></p>"##,
number, number,
)?; )?;
} out.write_str("\n</li>")?;
self.out.write_str("\n</li>")?;
self.footnote_number = None; self.footnote_number = None;
} }
Container::Table => self.out.write_str("</table>")?, Container::Table => out.write_str("</table>")?,
Container::TableRow { .. } => self.out.write_str("</tr>")?, Container::TableRow { .. } => out.write_str("</tr>")?,
Container::Section { .. } => self.out.write_str("</section>")?, Container::Section { .. } => out.write_str("</section>")?,
Container::Div { .. } => self.out.write_str("</div>")?, Container::Div { .. } => out.write_str("</div>")?,
Container::Paragraph => { Container::Paragraph => {
if matches!(self.list_tightness.last(), Some(true)) { if matches!(self.list_tightness.last(), Some(true)) {
continue; return Ok(());
} }
if let Some(num) = self.footnote_number { if self.footnote_number.is_none() {
if matches!( out.write_str("</p>")?;
self.events.peek(), } else {
Some(Event::End(Container::Footnote { .. })) self.close_para = true;
) {
write!(
self.out,
r##"<a href="#fnref{}" role="doc-backlink">↩︎︎</a>"##,
num
)?;
self.footnote_backlink_written = true;
} }
} }
self.out.write_str("</p>")?; Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
} Container::TableCell { head: false, .. } => out.write_str("</td>")?,
Container::Heading { level, .. } => write!(self.out, "</h{}>", level)?, Container::TableCell { head: true, .. } => out.write_str("</th>")?,
Container::TableCell { head: false, .. } => self.out.write_str("</td>")?, Container::Caption => out.write_str("</caption>")?,
Container::TableCell { head: true, .. } => self.out.write_str("</th>")?, Container::DescriptionTerm => out.write_str("</dt>")?,
Container::Caption => self.out.write_str("</caption>")?, Container::CodeBlock { .. } => out.write_str("</code></pre>")?,
Container::DescriptionTerm => self.out.write_str("</dt>")?, Container::Span => out.write_str("</span>")?,
Container::CodeBlock { .. } => self.out.write_str("</code></pre>")?, Container::Link(..) => out.write_str("</a>")?,
Container::Span => self.out.write_str("</span>")?,
Container::Link(..) => self.out.write_str("</a>")?,
Container::Image(src, ..) => { Container::Image(src, ..) => {
if self.img_alt_text == 1 { if self.img_alt_text == 1 {
if !src.is_empty() { if !src.is_empty() {
self.out.write_str(r#"" src=""#)?; out.write_str(r#"" src=""#)?;
self.write_attr(&src)?; write_attr(src, &mut out)?;
} }
self.out.write_str(r#"">"#)?; out.write_str(r#"">"#)?;
} }
self.img_alt_text -= 1; self.img_alt_text -= 1;
} }
Container::Verbatim => self.out.write_str("</code>")?, Container::Verbatim => out.write_str("</code>")?,
Container::Math { display } => { Container::Math { display } => {
self.out.write_str(if display { out.write_str(if *display {
r#"\]</span>"# r#"\]</span>"#
} else { } else {
r#"\)</span>"# r#"\)</span>"#
@ -391,62 +343,88 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
Container::RawBlock { .. } | Container::RawInline { .. } => { Container::RawBlock { .. } | Container::RawInline { .. } => {
self.raw = Raw::None; self.raw = Raw::None;
} }
Container::Subscript => self.out.write_str("</sub>")?, Container::Subscript => out.write_str("</sub>")?,
Container::Superscript => self.out.write_str("</sup>")?, Container::Superscript => out.write_str("</sup>")?,
Container::Insert => self.out.write_str("</ins>")?, Container::Insert => out.write_str("</ins>")?,
Container::Delete => self.out.write_str("</del>")?, Container::Delete => out.write_str("</del>")?,
Container::Strong => self.out.write_str("</strong>")?, Container::Strong => out.write_str("</strong>")?,
Container::Emphasis => self.out.write_str("</em>")?, Container::Emphasis => out.write_str("</em>")?,
Container::Mark => self.out.write_str("</mark>")?, Container::Mark => out.write_str("</mark>")?,
} }
} }
Event::Str(s) => match self.raw { Event::Str(s) => match self.raw {
Raw::None if self.img_alt_text > 0 => self.write_attr(&s)?, Raw::None if self.img_alt_text > 0 => write_attr(s, &mut out)?,
Raw::None => self.write_text(&s)?, Raw::None => write_text(s, &mut out)?,
Raw::Html => self.out.write_str(&s)?, Raw::Html => out.write_str(s)?,
Raw::Other => {} Raw::Other => {}
}, },
Event::FootnoteReference(_tag, number) => { Event::FootnoteReference(_tag, number) => {
if self.img_alt_text == 0 { if self.img_alt_text == 0 {
write!( write!(
self.out, out,
r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##, r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##,
number, number, number number, number, number
)?; )?;
} }
} }
Event::Symbol(sym) => write!(self.out, ":{}:", sym)?, Event::Symbol(sym) => write!(out, ":{}:", sym)?,
Event::LeftSingleQuote => self.out.write_str("&lsquo;")?, Event::LeftSingleQuote => out.write_str("&lsquo;")?,
Event::RightSingleQuote => self.out.write_str("&rsquo;")?, Event::RightSingleQuote => out.write_str("&rsquo;")?,
Event::LeftDoubleQuote => self.out.write_str("&ldquo;")?, Event::LeftDoubleQuote => out.write_str("&ldquo;")?,
Event::RightDoubleQuote => self.out.write_str("&rdquo;")?, Event::RightDoubleQuote => out.write_str("&rdquo;")?,
Event::Ellipsis => self.out.write_str("&hellip;")?, Event::Ellipsis => out.write_str("&hellip;")?,
Event::EnDash => self.out.write_str("&ndash;")?, Event::EnDash => out.write_str("&ndash;")?,
Event::EmDash => self.out.write_str("&mdash;")?, Event::EmDash => out.write_str("&mdash;")?,
Event::NonBreakingSpace => self.out.write_str("&nbsp;")?, Event::NonBreakingSpace => out.write_str("&nbsp;")?,
Event::Hardbreak => self.out.write_str("<br>\n")?, Event::Hardbreak => out.write_str("<br>\n")?,
Event::Softbreak => self.out.write_char('\n')?, Event::Softbreak => out.write_char('\n')?,
Event::Escape | Event::Blankline => unreachable!("filtered out"), Event::Escape | Event::Blankline => unreachable!("filtered out"),
Event::ThematicBreak(attrs) => { Event::ThematicBreak(attrs) => {
self.out.write_str("\n<hr")?; out.write_str("\n<hr")?;
for (a, v) in attrs.iter() { for (a, v) in attrs.iter() {
write!(self.out, r#" {}=""#, a)?; write!(out, r#" {}=""#, a)?;
v.parts().try_for_each(|part| self.write_attr(part))?; v.parts().try_for_each(|part| write_attr(part, &mut out))?;
self.out.write_char('"')?; out.write_char('"')?;
} }
self.out.write_str(">")?; out.write_str(">")?;
} }
} }
self.first_line = false; self.first_line = false;
}
if self.encountered_footnote {
self.out.write_str("\n</ol>\n</section>")?;
}
self.out.write_char('\n')?;
Ok(()) Ok(())
} }
fn write_escape(&mut self, mut s: &str, escape_quotes: bool) -> std::fmt::Result { fn render_epilogue<W>(&mut self, mut out: W) -> std::fmt::Result
where
W: std::fmt::Write,
{
if self.encountered_footnote {
out.write_str("\n</ol>\n</section>")?;
}
out.write_char('\n')?;
Ok(())
}
}
fn write_text<W>(s: &str, out: W) -> std::fmt::Result
where
W: std::fmt::Write,
{
write_escape(s, false, out)
}
fn write_attr<W>(s: &str, out: W) -> std::fmt::Result
where
W: std::fmt::Write,
{
write_escape(s, true, out)
}
fn write_escape<W>(mut s: &str, escape_quotes: bool, mut out: W) -> std::fmt::Result
where
W: std::fmt::Write,
{
let mut ent = ""; let mut ent = "";
while let Some(i) = s.find(|c| { while let Some(i) = s.find(|c| {
match c { match c {
@ -461,18 +439,9 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
true true
}) })
}) { }) {
self.out.write_str(&s[..i])?; out.write_str(&s[..i])?;
self.out.write_str(ent)?; out.write_str(ent)?;
s = &s[i + 1..]; s = &s[i + 1..];
} }
self.out.write_str(s) out.write_str(s)
}
fn write_text(&mut self, s: &str) -> std::fmt::Result {
self.write_escape(s, false)
}
fn write_attr(&mut self, s: &str) -> std::fmt::Result {
self.write_escape(s, true)
}
} }

View file

@ -20,7 +20,7 @@
//! let djot_input = "hello *world*!"; //! let djot_input = "hello *world*!";
//! let events = jotdown::Parser::new(djot_input); //! let events = jotdown::Parser::new(djot_input);
//! let mut html = String::new(); //! let mut html = String::new();
//! jotdown::html::Renderer.push(events, &mut html); //! jotdown::html::Renderer::default().push(events, &mut html);
//! assert_eq!(html, "<p>hello <strong>world</strong>!</p>\n"); //! assert_eq!(html, "<p>hello <strong>world</strong>!</p>\n");
//! # } //! # }
//! ``` //! ```
@ -41,7 +41,7 @@
//! e => e, //! e => e,
//! }); //! });
//! let mut html = String::new(); //! let mut html = String::new();
//! jotdown::html::Renderer.push(events, &mut html); //! jotdown::html::Renderer::default().push(events, &mut html);
//! assert_eq!(html, "<p>a <a href=\"https://example.net\">link</a></p>\n"); //! assert_eq!(html, "<p>a <a href=\"https://example.net\">link</a></p>\n");
//! # } //! # }
//! ``` //! ```
@ -67,52 +67,162 @@ pub use attr::{AttributeValue, AttributeValueParts, Attributes};
type CowStr<'s> = std::borrow::Cow<'s, str>; 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.
///
/// 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
/// well.
///
/// # Examples
///
/// Push to a [`String`] (implements [`std::fmt::Write`]):
///
/// ```
/// # use jotdown::Render;
/// # let events = std::iter::empty();
/// let mut output = String::new();
/// 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`]):
///
/// ```
/// # use jotdown::Render;
/// # let events = std::iter::empty();
/// let mut out = std::io::BufWriter::new(std::io::stdout());
/// let mut renderer = jotdown::html::Renderer::default();
/// renderer.write(events, &mut out).unwrap();
/// ```
pub trait Render { pub trait Render {
/// Push [`Event`]s to a unicode-accepting buffer or stream. /// Render a single event.
fn push<'s, I: Iterator<Item = Event<'s>>, W: fmt::Write>( fn render_event<'s, W>(&mut self, e: &Event<'s>, out: W) -> std::fmt::Result
&self, where
events: I, W: std::fmt::Write;
out: W,
) -> fmt::Result;
/// Write [`Event`]s to a byte sink, encoded as UTF-8. /// 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<W>(&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<W>(&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<Item = Event<'s>>,
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 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. /// NOTE: This performs many small writes, so IO writes should be buffered with e.g.
/// [`std::io::BufWriter`]. /// [`std::io::BufWriter`].
fn write<'s, I: Iterator<Item = Event<'s>>, W: io::Write>( fn write<'s, I, W>(&mut self, events: I, out: W) -> io::Result<()>
&self, where
events: I, I: Iterator<Item = Event<'s>>,
out: W, W: io::Write,
) -> io::Result<()> { {
struct Adapter<T: io::Write> { let mut out = WriteAdapter {
inner: T,
error: io::Result<()>,
}
impl<T: io::Write> fmt::Write for Adapter<T> {
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)
}
}
}
}
let mut out = Adapter {
inner: out, inner: out,
error: Ok(()), error: Ok(()),
}; };
match self.push(events, &mut out) { self.push(events, &mut out).map_err(|_| match out.error {
Ok(()) => Ok(()), Err(e) => e,
Err(_) => match out.error { _ => io::Error::new(io::ErrorKind::Other, "formatter error"),
Err(_) => out.error, })
_ => Err(io::Error::new(io::ErrorKind::Other, "formatter error")),
},
} }
/// 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<Event<'s>>,
I: Iterator<Item = E>,
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<Event<'s>>,
I: Iterator<Item = E>,
W: io::Write,
{
let mut out = WriteAdapter {
inner: out,
error: Ok(()),
};
self.push_borrowed(events, &mut out)
.map_err(|_| match out.error {
Err(e) => e,
_ => io::Error::new(io::ErrorKind::Other, "formatter error"),
})
}
}
struct WriteAdapter<T: io::Write> {
inner: T,
error: io::Result<()>,
}
impl<T: io::Write> fmt::Write for WriteAdapter<T> {
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<Event<'s>> for &Event<'s> {
fn as_ref(&self) -> &Event<'s> {
self
} }
} }

View file

@ -68,11 +68,11 @@ fn run() -> Result<(), std::io::Error> {
}; };
let parser = jotdown::Parser::new(&content); let parser = jotdown::Parser::new(&content);
let html = jotdown::html::Renderer; let mut renderer = jotdown::html::Renderer::default();
match app.output { match app.output {
Some(path) => html.write(parser, File::create(path)?)?, Some(path) => renderer.write(parser, File::create(path)?)?,
None => html.write(parser, BufWriter::new(std::io::stdout()))?, None => renderer.write(parser, BufWriter::new(std::io::stdout()))?,
} }
Ok(()) Ok(())

View file

@ -19,7 +19,9 @@ pub fn html(data: &[u8]) {
if !s.contains("=html") { if !s.contains("=html") {
let p = jotdown::Parser::new(s); let p = jotdown::Parser::new(s);
let mut html = "<!DOCTYPE html>\n".to_string(); let mut html = "<!DOCTYPE html>\n".to_string();
jotdown::html::Renderer.push(p, &mut html).unwrap(); jotdown::html::Renderer::default()
.push(p, &mut html)
.unwrap();
validate_html(&html); validate_html(&html);
} }
} }

View file

@ -14,7 +14,9 @@ macro_rules! suite_test {
let expected = $expected; let expected = $expected;
let p = jotdown::Parser::new(src); let p = jotdown::Parser::new(src);
let mut actual = String::new(); let mut actual = String::new();
jotdown::html::Renderer.push(p, &mut actual).unwrap(); jotdown::html::Renderer::default()
.push(p, &mut actual)
.unwrap();
assert_eq!( assert_eq!(
actual.trim(), actual.trim(),
expected.trim(), expected.trim(),