//! An HTML renderer that takes an iterator of [`Event`]s and emits HTML.
use crate::Alignment;
use crate::Container;
use crate::Event;
use crate::LinkType;
use crate::ListKind;
use crate::OrderedListNumbering::*;
use crate::Render;
use crate::SpanLinkType;
#[derive(Default)]
pub struct Renderer {}
impl Render for Renderer {
fn push<'s, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result
where
I: Iterator- >,
W: std::fmt::Write,
{
let mut w = Writer::default();
events.try_for_each(|e| w.render_event(&e, &mut out))?;
w.render_epilogue(&mut out)
}
fn push_borrowed<'s, E, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result
where
E: AsRef>,
I: Iterator
- ,
W: std::fmt::Write,
{
let mut w = Writer::default();
events.try_for_each(|e| w.render_event(e.as_ref(), &mut out))?;
w.render_epilogue(&mut out)
}
}
enum Raw {
None,
Html,
Other,
}
impl Default for Raw {
fn default() -> Self {
Self::None
}
}
#[derive(Default)]
struct Writer {
raw: Raw,
img_alt_text: usize,
list_tightness: Vec,
encountered_footnote: bool,
footnote_number: Option,
not_first_line: bool,
close_para: bool,
ignore: bool,
}
impl Writer {
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(());
}
if matches!(&e, Event::Start(Container::LinkDefinition { .. }, ..)) {
self.ignore = true;
return Ok(());
}
if matches!(&e, Event::End(Container::LinkDefinition { .. })) {
self.ignore = false;
return Ok(());
}
if self.ignore {
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("
")?;
}
}
match e {
Event::Start(c, attrs) => {
if c.is_block() && self.not_first_line {
out.write_char('\n')?;
}
if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
return Ok(());
}
match &c {
Container::Blockquote => out.write_str(" {
self.list_tightness.push(*tight);
match kind {
ListKind::Unordered | ListKind::Task => out.write_str(" {
out.write_str(" 1 {
write!(out, r#" start="{}""#, start)?;
}
if let Some(ty) = match numbering {
Decimal => None,
AlphaLower => Some('a'),
AlphaUpper => Some('A'),
RomanLower => Some('i'),
RomanUpper => Some('I'),
} {
write!(out, r#" type="{}""#, ty)?;
}
}
}
}
Container::ListItem | Container::TaskListItem { .. } => {
out.write_str("- out.write_str("
out.write_str("- {
debug_assert!(self.footnote_number.is_none());
self.footnote_number = Some((*number).try_into().unwrap());
if !self.encountered_footnote {
self.encountered_footnote = true;
out.write_str("\n
\n\n")?;
}
write!(out, "- ", number)?;
return Ok(());
}
Container::Table => out.write_str("
out.write_str(" out.write_str(" out.write_str(" {
if matches!(self.list_tightness.last(), Some(true)) {
return Ok(());
}
out.write_str("
write!(out, " out.write_str(" out.write_str(" | out.write_str(" out.write_str("- out.write_str("
out.write_str(" {
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
out.write_str(" {
self.img_alt_text += 1;
if self.img_alt_text == 1 {
out.write_str(" out.write_str(" {
self.raw = if format == &"html" {
Raw::Html
} else {
Raw::Other
};
return Ok(());
}
Container::Subscript => out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" out.write_str(" return Ok(()),
}
for (a, v) in attrs.iter().filter(|(a, _)| *a != "class") {
write!(out, r#" {}=""#, a)?;
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
out.write_char('"')?;
}
if let Container::Heading {
id,
has_section: false,
..
}
| Container::Section { id } = &c
{
if !attrs.iter().any(|(a, _)| a == "id") {
out.write_str(r#" id=""#)?;
write_attr(id, &mut out)?;
out.write_char('"')?;
}
}
if attrs.iter().any(|(a, _)| a == "class")
|| matches!(
c,
Container::Div { class: Some(_) }
| Container::Math { .. }
| Container::List {
kind: ListKind::Task,
..
}
| Container::TaskListItem { .. }
)
{
out.write_str(r#" class=""#)?;
let mut first_written = false;
if let Some(cls) = match c {
Container::List {
kind: ListKind::Task,
..
} => Some("task-list"),
Container::TaskListItem { checked: false } => Some("unchecked"),
Container::TaskListItem { checked: true } => Some("checked"),
Container::Math { display: false } => Some("math inline"),
Container::Math { display: true } => Some("math display"),
_ => None,
} {
first_written = true;
out.write_str(cls)?;
}
for cls in attrs
.iter()
.filter(|(a, _)| a == &"class")
.map(|(_, cls)| cls)
{
if first_written {
out.write_char(' ')?;
}
first_written = true;
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 {
out.write_char(' ')?;
}
out.write_str(cls)?;
}
out.write_char('"')?;
}
match c {
Container::TableCell { alignment, .. }
if !matches!(alignment, Alignment::Unspecified) =>
{
let a = match alignment {
Alignment::Unspecified => unreachable!(),
Alignment::Left => "left",
Alignment::Center => "center",
Alignment::Right => "right",
};
write!(out, r#" style="text-align: {};">"#, a)?;
}
Container::CodeBlock { lang } => {
if let Some(l) = lang {
out.write_str(r#">"#)?;
} else {
out.write_str(">")?;
}
}
Container::Image(..) => {
if self.img_alt_text == 1 {
out.write_str(r#" alt=""#)?;
}
}
Container::Math { display } => {
out.write_str(if *display { r#">\["# } else { r#">\("# })?;
}
_ => out.write_char('>')?,
}
}
Event::End(c) => {
if c.is_block_container() && !matches!(c, Container::Footnote { .. }) {
out.write_char('\n')?;
}
if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
return Ok(());
}
match c {
Container::Blockquote => out.write_str("")?,
Container::List {
kind: ListKind::Unordered | ListKind::Task,
..
} => {
self.list_tightness.pop();
out.write_str("")?;
}
Container::List {
kind: ListKind::Ordered { .. },
..
} => out.write_str("")?,
Container::ListItem | Container::TaskListItem { .. } => {
out.write_str("")?;
}
Container::DescriptionList => out.write_str("")?,
Container::DescriptionDetails => out.write_str("")?,
Container::Footnote { number, .. } => {
if !close_para {
// create a new paragraph
out.write_str("\n")?;
}
write!(
out,
r##"↩︎︎ "##,
number,
)?;
out.write_str("\n")?;
self.footnote_number = None;
}
Container::Table => out.write_str(" |
")?,
Container::TableRow { .. } => out.write_str("")?,
Container::Section { .. } => out.write_str("
")?,
Container::Div { .. } => out.write_str("")?,
Container::Paragraph => {
if matches!(self.list_tightness.last(), Some(true)) {
return Ok(());
}
if self.footnote_number.is_none() {
out.write_str("")?;
} else {
self.close_para = true;
}
}
Container::Heading { level, .. } => write!(out, "", level)?,
Container::TableCell { head: false, .. } => out.write_str("")?,
Container::TableCell { head: true, .. } => out.write_str("")?,
Container::Caption => out.write_str("")?,
Container::DescriptionTerm => out.write_str("")?,
Container::CodeBlock { .. } => out.write_str("")?,
Container::Span => out.write_str("")?,
Container::Link(..) => out.write_str("")?,
Container::Image(src, ..) => {
if self.img_alt_text == 1 {
if !src.is_empty() {
out.write_str(r#"" src=""#)?;
write_attr(src, &mut out)?;
}
out.write_str(r#"">"#)?;
}
self.img_alt_text -= 1;
}
Container::Verbatim => out.write_str("")?,
Container::Math { display } => {
out.write_str(if *display {
r#"\]"#
} else {
r#"\)"#
})?;
}
Container::RawBlock { .. } | Container::RawInline { .. } => {
self.raw = Raw::None;
}
Container::Subscript => out.write_str("")?,
Container::Superscript => out.write_str("")?,
Container::Insert => out.write_str("")?,
Container::Delete => out.write_str("")?,
Container::Strong => out.write_str("")?,
Container::Emphasis => out.write_str("")?,
Container::Mark => out.write_str("")?,
Container::LinkDefinition { .. } => unreachable!(),
}
}
Event::Str(s) => match self.raw {
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!(
out,
r##"{}"##,
number, number, number
)?;
}
}
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("
\n")?,
Event::Softbreak => out.write_char('\n')?,
Event::Escape | Event::Blankline => unreachable!("filtered out"),
Event::ThematicBreak(attrs) => {
out.write_str("\n
")?;
}
}
self.not_first_line = true;
Ok(())
}
fn render_epilogue(&mut self, mut out: W) -> std::fmt::Result
where
W: std::fmt::Write,
{
if self.encountered_footnote {
out.write_str("\n
\n")?;
}
out.write_char('\n')?;
Ok(())
}
}
fn write_text(s: &str, out: W) -> std::fmt::Result
where
W: std::fmt::Write,
{
write_escape(s, false, out)
}
fn write_attr(s: &str, out: W) -> std::fmt::Result
where
W: std::fmt::Write,
{
write_escape(s, true, out)
}
fn write_escape(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 {
'<' => Some("<"),
'>' => Some(">"),
'&' => Some("&"),
'"' if escape_quotes => Some("""),
_ => None,
}
.map_or(false, |s| {
ent = s;
true
})
}) {
out.write_str(&s[..i])?;
out.write_str(ent)?;
s = &s[i + 1..];
}
out.write_str(s)
}