//! 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; enum Raw { None, Html, Other, } pub struct Renderer { raw: Raw, img_alt_text: usize, list_tightness: Vec, encountered_footnote: bool, footnote_number: Option, first_line: bool, close_para: bool, } impl Default for Renderer { fn default() -> Self { Self { raw: Raw::None, img_alt_text: 0, list_tightness: Vec::new(), encountered_footnote: false, footnote_number: None, 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("

")?; } } match e { Event::Start(c, attrs) => { if c.is_block() && !self.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(" { 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, "
  1. ", 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(" 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("")?, } } 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.first_line = false; 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) }