PR #29 Allow rendering borrowed events
Merge branch 'render_ref' closes #29
This commit is contained in:
commit
8893281310
8 changed files with 605 additions and 464 deletions
|
@ -51,7 +51,9 @@ fn gen_html(c: &mut criterion::Criterion) {
|
|||
|| jotdown::Parser::new(input).collect::<Vec<_>>(),
|
||||
|p| {
|
||||
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
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
|
@ -62,6 +64,60 @@ fn gen_html(c: &mut criterion::Criterion) {
|
|||
}
|
||||
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) {
|
||||
let mut group = c.benchmark_group("full");
|
||||
for (name, input) in bench_input::INPUTS {
|
||||
|
@ -72,7 +128,7 @@ fn gen_full(c: &mut criterion::Criterion) {
|
|||
|b, &input| {
|
||||
b.iter_with_large_drop(|| {
|
||||
let mut s = String::new();
|
||||
jotdown::html::Renderer
|
||||
jotdown::html::Renderer::default()
|
||||
.push(jotdown::Parser::new(input), &mut s)
|
||||
.unwrap();
|
||||
s
|
||||
|
@ -83,4 +139,4 @@ fn gen_full(c: &mut criterion::Criterion) {
|
|||
}
|
||||
criterion_group!(full, gen_full);
|
||||
|
||||
criterion_main!(block, inline, html, full);
|
||||
criterion_main!(block, inline, html, html_borrow, html_clone, full);
|
||||
|
|
|
@ -12,7 +12,7 @@ fn block_inline() -> Option<jotdown::Event<'static>> {
|
|||
|
||||
fn full() -> String {
|
||||
let mut s = String::new();
|
||||
jotdown::html::Renderer
|
||||
jotdown::html::Renderer::default()
|
||||
.push(jotdown::Parser::new(bench_input::ALL), &mut s)
|
||||
.unwrap();
|
||||
s
|
||||
|
|
|
@ -7,6 +7,8 @@ use jotdown::Render;
|
|||
pub fn jotdown_render(djot: &str) -> String {
|
||||
let events = jotdown::Parser::new(djot);
|
||||
let mut html = String::new();
|
||||
jotdown::html::Renderer.push(events, &mut html).unwrap();
|
||||
jotdown::html::Renderer::default()
|
||||
.push(events, &mut html)
|
||||
.unwrap();
|
||||
html
|
||||
}
|
||||
|
|
405
src/html.rs
405
src/html.rs
|
@ -1,26 +1,4 @@
|
|||
//! 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::Container;
|
||||
|
@ -31,91 +9,74 @@ use crate::OrderedListNumbering::*;
|
|||
use crate::Render;
|
||||
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 {
|
||||
None,
|
||||
Html,
|
||||
Other,
|
||||
}
|
||||
|
||||
struct FilteredEvents<I> {
|
||||
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,
|
||||
pub struct Renderer {
|
||||
raw: Raw,
|
||||
img_alt_text: usize,
|
||||
list_tightness: Vec<bool>,
|
||||
encountered_footnote: bool,
|
||||
footnote_number: Option<std::num::NonZeroUsize>,
|
||||
footnote_backlink_written: bool,
|
||||
first_line: bool,
|
||||
close_para: bool,
|
||||
}
|
||||
|
||||
impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
||||
fn new(events: I, out: W) -> Self {
|
||||
impl Default for Renderer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
events: FilteredEvents { events }.peekable(),
|
||||
out,
|
||||
raw: Raw::None,
|
||||
img_alt_text: 0,
|
||||
list_tightness: Vec::new(),
|
||||
encountered_footnote: false,
|
||||
footnote_number: None,
|
||||
footnote_backlink_written: false,
|
||||
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 {
|
||||
Event::Start(c, attrs) => {
|
||||
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(..)) {
|
||||
continue;
|
||||
return Ok(());
|
||||
}
|
||||
match &c {
|
||||
Container::Blockquote => self.out.write_str("<blockquote")?,
|
||||
Container::Blockquote => out.write_str("<blockquote")?,
|
||||
Container::List { kind, tight } => {
|
||||
self.list_tightness.push(*tight);
|
||||
match kind {
|
||||
ListKind::Unordered | ListKind::Task => {
|
||||
self.out.write_str("<ul")?
|
||||
}
|
||||
ListKind::Unordered | ListKind::Task => out.write_str("<ul")?,
|
||||
ListKind::Ordered {
|
||||
numbering, start, ..
|
||||
} => {
|
||||
self.out.write_str("<ol")?;
|
||||
out.write_str("<ol")?;
|
||||
if *start > 1 {
|
||||
write!(self.out, r#" start="{}""#, start)?;
|
||||
write!(out, r#" start="{}""#, start)?;
|
||||
}
|
||||
if let Some(ty) = match numbering {
|
||||
Decimal => None,
|
||||
|
@ -124,87 +85,85 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
|||
RomanLower => Some('i'),
|
||||
RomanUpper => Some('I'),
|
||||
} {
|
||||
write!(self.out, r#" type="{}""#, ty)?;
|
||||
write!(out, r#" type="{}""#, ty)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Container::ListItem | Container::TaskListItem { .. } => {
|
||||
self.out.write_str("<li")?;
|
||||
out.write_str("<li")?;
|
||||
}
|
||||
Container::DescriptionList => self.out.write_str("<dl")?,
|
||||
Container::DescriptionDetails => self.out.write_str("<dd")?,
|
||||
Container::DescriptionList => out.write_str("<dl")?,
|
||||
Container::DescriptionDetails => out.write_str("<dd")?,
|
||||
Container::Footnote { number, .. } => {
|
||||
assert!(self.footnote_number.is_none());
|
||||
self.footnote_number = Some((*number).try_into().unwrap());
|
||||
if !self.encountered_footnote {
|
||||
self.encountered_footnote = true;
|
||||
self.out
|
||||
.write_str("<section role=\"doc-endnotes\">\n<hr>\n<ol>\n")?;
|
||||
out.write_str("<section role=\"doc-endnotes\">\n<hr>\n<ol>\n")?;
|
||||
}
|
||||
write!(self.out, "<li id=\"fn{}\">", number)?;
|
||||
self.footnote_backlink_written = false;
|
||||
continue;
|
||||
write!(out, "<li id=\"fn{}\">", number)?;
|
||||
return Ok(());
|
||||
}
|
||||
Container::Table => self.out.write_str("<table")?,
|
||||
Container::TableRow { .. } => self.out.write_str("<tr")?,
|
||||
Container::Section { .. } => self.out.write_str("<section")?,
|
||||
Container::Div { .. } => self.out.write_str("<div")?,
|
||||
Container::Table => out.write_str("<table")?,
|
||||
Container::TableRow { .. } => out.write_str("<tr")?,
|
||||
Container::Section { .. } => out.write_str("<section")?,
|
||||
Container::Div { .. } => out.write_str("<div")?,
|
||||
Container::Paragraph => {
|
||||
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::TableCell { head: false, .. } => self.out.write_str("<td")?,
|
||||
Container::TableCell { head: true, .. } => self.out.write_str("<th")?,
|
||||
Container::Caption => self.out.write_str("<caption")?,
|
||||
Container::DescriptionTerm => self.out.write_str("<dt")?,
|
||||
Container::CodeBlock { .. } => self.out.write_str("<pre")?,
|
||||
Container::Span | Container::Math { .. } => self.out.write_str("<span")?,
|
||||
Container::Heading { level, .. } => write!(out, "<h{}", level)?,
|
||||
Container::TableCell { head: false, .. } => out.write_str("<td")?,
|
||||
Container::TableCell { head: true, .. } => out.write_str("<th")?,
|
||||
Container::Caption => out.write_str("<caption")?,
|
||||
Container::DescriptionTerm => out.write_str("<dt")?,
|
||||
Container::CodeBlock { .. } => out.write_str("<pre")?,
|
||||
Container::Span | Container::Math { .. } => out.write_str("<span")?,
|
||||
Container::Link(dst, ty) => {
|
||||
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
|
||||
self.out.write_str("<a")?;
|
||||
out.write_str("<a")?;
|
||||
} else {
|
||||
self.out.write_str(r#"<a href=""#)?;
|
||||
out.write_str(r#"<a href=""#)?;
|
||||
if matches!(ty, LinkType::Email) {
|
||||
self.out.write_str("mailto:")?;
|
||||
out.write_str("mailto:")?;
|
||||
}
|
||||
self.write_attr(dst)?;
|
||||
self.out.write_char('"')?;
|
||||
write_attr(dst, &mut out)?;
|
||||
out.write_char('"')?;
|
||||
}
|
||||
}
|
||||
Container::Image(..) => {
|
||||
self.img_alt_text += 1;
|
||||
if self.img_alt_text == 1 {
|
||||
self.out.write_str("<img")?;
|
||||
out.write_str("<img")?;
|
||||
} else {
|
||||
continue;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Container::Verbatim => self.out.write_str("<code")?,
|
||||
Container::Verbatim => out.write_str("<code")?,
|
||||
Container::RawBlock { format } | Container::RawInline { format } => {
|
||||
self.raw = if format == &"html" {
|
||||
Raw::Html
|
||||
} else {
|
||||
Raw::Other
|
||||
};
|
||||
continue;
|
||||
return Ok(());
|
||||
}
|
||||
Container::Subscript => self.out.write_str("<sub")?,
|
||||
Container::Superscript => self.out.write_str("<sup")?,
|
||||
Container::Insert => self.out.write_str("<ins")?,
|
||||
Container::Delete => self.out.write_str("<del")?,
|
||||
Container::Strong => self.out.write_str("<strong")?,
|
||||
Container::Emphasis => self.out.write_str("<em")?,
|
||||
Container::Mark => self.out.write_str("<mark")?,
|
||||
Container::Subscript => out.write_str("<sub")?,
|
||||
Container::Superscript => out.write_str("<sup")?,
|
||||
Container::Insert => out.write_str("<ins")?,
|
||||
Container::Delete => out.write_str("<del")?,
|
||||
Container::Strong => out.write_str("<strong")?,
|
||||
Container::Emphasis => out.write_str("<em")?,
|
||||
Container::Mark => out.write_str("<mark")?,
|
||||
}
|
||||
|
||||
for (a, v) in attrs.iter().filter(|(a, _)| *a != "class") {
|
||||
write!(self.out, r#" {}=""#, a)?;
|
||||
v.parts().try_for_each(|part| self.write_attr(part))?;
|
||||
self.out.write_char('"')?;
|
||||
write!(out, r#" {}=""#, a)?;
|
||||
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
|
||||
out.write_char('"')?;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
if !attrs.iter().any(|(a, _)| a == "id") {
|
||||
self.out.write_str(r#" id=""#)?;
|
||||
self.write_attr(id)?;
|
||||
self.out.write_char('"')?;
|
||||
out.write_str(r#" id=""#)?;
|
||||
write_attr(id, &mut out)?;
|
||||
out.write_char('"')?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -233,7 +192,7 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
|||
| Container::TaskListItem { .. }
|
||||
)
|
||||
{
|
||||
self.out.write_str(r#" class=""#)?;
|
||||
out.write_str(r#" class=""#)?;
|
||||
let mut first_written = false;
|
||||
if let Some(cls) = match c {
|
||||
Container::List {
|
||||
|
@ -247,7 +206,7 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
|||
_ => None,
|
||||
} {
|
||||
first_written = true;
|
||||
self.out.write_str(cls)?;
|
||||
out.write_str(cls)?;
|
||||
}
|
||||
for cls in attrs
|
||||
.iter()
|
||||
|
@ -255,19 +214,20 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
|||
.map(|(_, cls)| cls)
|
||||
{
|
||||
if first_written {
|
||||
self.out.write_char(' ')?;
|
||||
out.write_char(' ')?;
|
||||
}
|
||||
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
|
||||
if let Container::Div { class: Some(cls) } = c {
|
||||
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 {
|
||||
|
@ -280,109 +240,101 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
|||
Alignment::Center => "center",
|
||||
Alignment::Right => "right",
|
||||
};
|
||||
write!(self.out, r#" style="text-align: {};">"#, a)?;
|
||||
write!(out, r#" style="text-align: {};">"#, a)?;
|
||||
}
|
||||
Container::CodeBlock { lang } => {
|
||||
if let Some(l) = lang {
|
||||
self.out.write_str(r#"><code class="language-"#)?;
|
||||
self.write_attr(l)?;
|
||||
self.out.write_str(r#"">"#)?;
|
||||
out.write_str(r#"><code class="language-"#)?;
|
||||
write_attr(l, &mut out)?;
|
||||
out.write_str(r#"">"#)?;
|
||||
} else {
|
||||
self.out.write_str("><code>")?;
|
||||
out.write_str("><code>")?;
|
||||
}
|
||||
}
|
||||
Container::Image(..) => {
|
||||
if self.img_alt_text == 1 {
|
||||
self.out.write_str(r#" alt=""#)?;
|
||||
out.write_str(r#" alt=""#)?;
|
||||
}
|
||||
}
|
||||
Container::Math { display } => {
|
||||
self.out
|
||||
.write_str(if display { r#">\["# } else { r#">\("# })?;
|
||||
out.write_str(if *display { r#">\["# } else { r#">\("# })?;
|
||||
}
|
||||
_ => self.out.write_char('>')?,
|
||||
_ => out.write_char('>')?,
|
||||
}
|
||||
}
|
||||
Event::End(c) => {
|
||||
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(..)) {
|
||||
continue;
|
||||
return Ok(());
|
||||
}
|
||||
match c {
|
||||
Container::Blockquote => self.out.write_str("</blockquote>")?,
|
||||
Container::Blockquote => out.write_str("</blockquote>")?,
|
||||
Container::List {
|
||||
kind: ListKind::Unordered | ListKind::Task,
|
||||
..
|
||||
} => {
|
||||
self.list_tightness.pop();
|
||||
self.out.write_str("</ul>")?;
|
||||
out.write_str("</ul>")?;
|
||||
}
|
||||
Container::List {
|
||||
kind: ListKind::Ordered { .. },
|
||||
..
|
||||
} => self.out.write_str("</ol>")?,
|
||||
} => out.write_str("</ol>")?,
|
||||
Container::ListItem | Container::TaskListItem { .. } => {
|
||||
self.out.write_str("</li>")?;
|
||||
out.write_str("</li>")?;
|
||||
}
|
||||
Container::DescriptionList => self.out.write_str("</dl>")?,
|
||||
Container::DescriptionDetails => self.out.write_str("</dd>")?,
|
||||
Container::DescriptionList => out.write_str("</dl>")?,
|
||||
Container::DescriptionDetails => out.write_str("</dd>")?,
|
||||
Container::Footnote { number, .. } => {
|
||||
if !self.footnote_backlink_written {
|
||||
if !close_para {
|
||||
// create a new paragraph
|
||||
out.write_str("\n<p>")?;
|
||||
}
|
||||
write!(
|
||||
self.out,
|
||||
"\n<p><a href=\"#fnref{}\" role=\"doc-backlink\">↩︎︎</a></p>",
|
||||
out,
|
||||
r##"<a href="#fnref{}" role="doc-backlink">↩︎︎</a></p>"##,
|
||||
number,
|
||||
)?;
|
||||
}
|
||||
self.out.write_str("\n</li>")?;
|
||||
out.write_str("\n</li>")?;
|
||||
self.footnote_number = None;
|
||||
}
|
||||
Container::Table => self.out.write_str("</table>")?,
|
||||
Container::TableRow { .. } => self.out.write_str("</tr>")?,
|
||||
Container::Section { .. } => self.out.write_str("</section>")?,
|
||||
Container::Div { .. } => self.out.write_str("</div>")?,
|
||||
Container::Table => out.write_str("</table>")?,
|
||||
Container::TableRow { .. } => out.write_str("</tr>")?,
|
||||
Container::Section { .. } => out.write_str("</section>")?,
|
||||
Container::Div { .. } => out.write_str("</div>")?,
|
||||
Container::Paragraph => {
|
||||
if matches!(self.list_tightness.last(), Some(true)) {
|
||||
continue;
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(num) = self.footnote_number {
|
||||
if matches!(
|
||||
self.events.peek(),
|
||||
Some(Event::End(Container::Footnote { .. }))
|
||||
) {
|
||||
write!(
|
||||
self.out,
|
||||
r##"<a href="#fnref{}" role="doc-backlink">↩︎︎</a>"##,
|
||||
num
|
||||
)?;
|
||||
self.footnote_backlink_written = true;
|
||||
if self.footnote_number.is_none() {
|
||||
out.write_str("</p>")?;
|
||||
} else {
|
||||
self.close_para = true;
|
||||
}
|
||||
}
|
||||
self.out.write_str("</p>")?;
|
||||
}
|
||||
Container::Heading { level, .. } => write!(self.out, "</h{}>", level)?,
|
||||
Container::TableCell { head: false, .. } => self.out.write_str("</td>")?,
|
||||
Container::TableCell { head: true, .. } => self.out.write_str("</th>")?,
|
||||
Container::Caption => self.out.write_str("</caption>")?,
|
||||
Container::DescriptionTerm => self.out.write_str("</dt>")?,
|
||||
Container::CodeBlock { .. } => self.out.write_str("</code></pre>")?,
|
||||
Container::Span => self.out.write_str("</span>")?,
|
||||
Container::Link(..) => self.out.write_str("</a>")?,
|
||||
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
|
||||
Container::TableCell { head: false, .. } => out.write_str("</td>")?,
|
||||
Container::TableCell { head: true, .. } => out.write_str("</th>")?,
|
||||
Container::Caption => out.write_str("</caption>")?,
|
||||
Container::DescriptionTerm => out.write_str("</dt>")?,
|
||||
Container::CodeBlock { .. } => out.write_str("</code></pre>")?,
|
||||
Container::Span => out.write_str("</span>")?,
|
||||
Container::Link(..) => out.write_str("</a>")?,
|
||||
Container::Image(src, ..) => {
|
||||
if self.img_alt_text == 1 {
|
||||
if !src.is_empty() {
|
||||
self.out.write_str(r#"" src=""#)?;
|
||||
self.write_attr(&src)?;
|
||||
out.write_str(r#"" src=""#)?;
|
||||
write_attr(src, &mut out)?;
|
||||
}
|
||||
self.out.write_str(r#"">"#)?;
|
||||
out.write_str(r#"">"#)?;
|
||||
}
|
||||
self.img_alt_text -= 1;
|
||||
}
|
||||
Container::Verbatim => self.out.write_str("</code>")?,
|
||||
Container::Verbatim => out.write_str("</code>")?,
|
||||
Container::Math { display } => {
|
||||
self.out.write_str(if display {
|
||||
out.write_str(if *display {
|
||||
r#"\]</span>"#
|
||||
} else {
|
||||
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 { .. } => {
|
||||
self.raw = Raw::None;
|
||||
}
|
||||
Container::Subscript => self.out.write_str("</sub>")?,
|
||||
Container::Superscript => self.out.write_str("</sup>")?,
|
||||
Container::Insert => self.out.write_str("</ins>")?,
|
||||
Container::Delete => self.out.write_str("</del>")?,
|
||||
Container::Strong => self.out.write_str("</strong>")?,
|
||||
Container::Emphasis => self.out.write_str("</em>")?,
|
||||
Container::Mark => self.out.write_str("</mark>")?,
|
||||
Container::Subscript => out.write_str("</sub>")?,
|
||||
Container::Superscript => out.write_str("</sup>")?,
|
||||
Container::Insert => out.write_str("</ins>")?,
|
||||
Container::Delete => out.write_str("</del>")?,
|
||||
Container::Strong => out.write_str("</strong>")?,
|
||||
Container::Emphasis => out.write_str("</em>")?,
|
||||
Container::Mark => out.write_str("</mark>")?,
|
||||
}
|
||||
}
|
||||
Event::Str(s) => match self.raw {
|
||||
Raw::None if self.img_alt_text > 0 => self.write_attr(&s)?,
|
||||
Raw::None => self.write_text(&s)?,
|
||||
Raw::Html => self.out.write_str(&s)?,
|
||||
Raw::None if self.img_alt_text > 0 => write_attr(s, &mut out)?,
|
||||
Raw::None => write_text(s, &mut out)?,
|
||||
Raw::Html => out.write_str(s)?,
|
||||
Raw::Other => {}
|
||||
},
|
||||
Event::FootnoteReference(_tag, number) => {
|
||||
if self.img_alt_text == 0 {
|
||||
write!(
|
||||
self.out,
|
||||
out,
|
||||
r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##,
|
||||
number, number, number
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Event::Symbol(sym) => write!(self.out, ":{}:", sym)?,
|
||||
Event::LeftSingleQuote => self.out.write_str("‘")?,
|
||||
Event::RightSingleQuote => self.out.write_str("’")?,
|
||||
Event::LeftDoubleQuote => self.out.write_str("“")?,
|
||||
Event::RightDoubleQuote => self.out.write_str("”")?,
|
||||
Event::Ellipsis => self.out.write_str("…")?,
|
||||
Event::EnDash => self.out.write_str("–")?,
|
||||
Event::EmDash => self.out.write_str("—")?,
|
||||
Event::NonBreakingSpace => self.out.write_str(" ")?,
|
||||
Event::Hardbreak => self.out.write_str("<br>\n")?,
|
||||
Event::Softbreak => self.out.write_char('\n')?,
|
||||
Event::Symbol(sym) => write!(out, ":{}:", sym)?,
|
||||
Event::LeftSingleQuote => out.write_str("‘")?,
|
||||
Event::RightSingleQuote => out.write_str("’")?,
|
||||
Event::LeftDoubleQuote => out.write_str("“")?,
|
||||
Event::RightDoubleQuote => out.write_str("”")?,
|
||||
Event::Ellipsis => out.write_str("…")?,
|
||||
Event::EnDash => out.write_str("–")?,
|
||||
Event::EmDash => out.write_str("—")?,
|
||||
Event::NonBreakingSpace => out.write_str(" ")?,
|
||||
Event::Hardbreak => out.write_str("<br>\n")?,
|
||||
Event::Softbreak => out.write_char('\n')?,
|
||||
Event::Escape | Event::Blankline => unreachable!("filtered out"),
|
||||
Event::ThematicBreak(attrs) => {
|
||||
self.out.write_str("\n<hr")?;
|
||||
out.write_str("\n<hr")?;
|
||||
for (a, v) in attrs.iter() {
|
||||
write!(self.out, r#" {}=""#, a)?;
|
||||
v.parts().try_for_each(|part| self.write_attr(part))?;
|
||||
self.out.write_char('"')?;
|
||||
write!(out, r#" {}=""#, a)?;
|
||||
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
|
||||
out.write_char('"')?;
|
||||
}
|
||||
self.out.write_str(">")?;
|
||||
out.write_str(">")?;
|
||||
}
|
||||
}
|
||||
self.first_line = false;
|
||||
}
|
||||
if self.encountered_footnote {
|
||||
self.out.write_str("\n</ol>\n</section>")?;
|
||||
}
|
||||
self.out.write_char('\n')?;
|
||||
|
||||
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 = "";
|
||||
while let Some(i) = s.find(|c| {
|
||||
match c {
|
||||
|
@ -461,18 +439,9 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
|||
true
|
||||
})
|
||||
}) {
|
||||
self.out.write_str(&s[..i])?;
|
||||
self.out.write_str(ent)?;
|
||||
out.write_str(&s[..i])?;
|
||||
out.write_str(ent)?;
|
||||
s = &s[i + 1..];
|
||||
}
|
||||
self.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)
|
||||
}
|
||||
out.write_str(s)
|
||||
}
|
||||
|
|
186
src/lib.rs
186
src/lib.rs
|
@ -20,7 +20,7 @@
|
|||
//! let djot_input = "hello *world*!";
|
||||
//! let events = jotdown::Parser::new(djot_input);
|
||||
//! 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");
|
||||
//! # }
|
||||
//! ```
|
||||
|
@ -41,7 +41,7 @@
|
|||
//! e => e,
|
||||
//! });
|
||||
//! 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");
|
||||
//! # }
|
||||
//! ```
|
||||
|
@ -67,52 +67,162 @@ pub use attr::{AttributeValue, AttributeValueParts, Attributes};
|
|||
|
||||
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 {
|
||||
/// Push [`Event`]s to a unicode-accepting buffer or stream.
|
||||
fn push<'s, I: Iterator<Item = Event<'s>>, W: fmt::Write>(
|
||||
&self,
|
||||
events: I,
|
||||
out: W,
|
||||
) -> fmt::Result;
|
||||
/// Render a single event.
|
||||
fn render_event<'s, W>(&mut self, e: &Event<'s>, out: W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write;
|
||||
|
||||
/// 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.
|
||||
/// [`std::io::BufWriter`].
|
||||
fn write<'s, I: Iterator<Item = Event<'s>>, W: io::Write>(
|
||||
&self,
|
||||
events: I,
|
||||
out: W,
|
||||
) -> io::Result<()> {
|
||||
struct Adapter<T: io::Write> {
|
||||
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 {
|
||||
fn write<'s, I, W>(&mut self, events: I, out: W) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = Event<'s>>,
|
||||
W: io::Write,
|
||||
{
|
||||
let mut out = WriteAdapter {
|
||||
inner: out,
|
||||
error: Ok(()),
|
||||
};
|
||||
|
||||
match self.push(events, &mut out) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => match out.error {
|
||||
Err(_) => out.error,
|
||||
_ => Err(io::Error::new(io::ErrorKind::Other, "formatter error")),
|
||||
},
|
||||
self.push(events, &mut out).map_err(|_| match out.error {
|
||||
Err(e) => e,
|
||||
_ => 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -68,11 +68,11 @@ fn run() -> Result<(), std::io::Error> {
|
|||
};
|
||||
|
||||
let parser = jotdown::Parser::new(&content);
|
||||
let html = jotdown::html::Renderer;
|
||||
let mut renderer = jotdown::html::Renderer::default();
|
||||
|
||||
match app.output {
|
||||
Some(path) => html.write(parser, File::create(path)?)?,
|
||||
None => html.write(parser, BufWriter::new(std::io::stdout()))?,
|
||||
Some(path) => renderer.write(parser, File::create(path)?)?,
|
||||
None => renderer.write(parser, BufWriter::new(std::io::stdout()))?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -19,7 +19,9 @@ pub fn html(data: &[u8]) {
|
|||
if !s.contains("=html") {
|
||||
let p = jotdown::Parser::new(s);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@ macro_rules! suite_test {
|
|||
let expected = $expected;
|
||||
let p = jotdown::Parser::new(src);
|
||||
let mut actual = String::new();
|
||||
jotdown::html::Renderer.push(p, &mut actual).unwrap();
|
||||
jotdown::html::Renderer::default()
|
||||
.push(p, &mut actual)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
actual.trim(),
|
||||
expected.trim(),
|
||||
|
|
Loading…
Reference in a new issue