use crate::Atom; use crate::Container; use crate::Event; /// Generate HTML from parsed events and push it to a unicode-accepting buffer or stream. pub fn push<'s, I: Iterator>, W: std::fmt::Write>(out: W, events: I) { Writer::new(events, out).write().unwrap(); } /// Generate HTML from parsed events and write it 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`]. pub fn write<'s, I: Iterator>, W: std::io::Write>( mut out: W, events: I, ) -> std::io::Result<()> { struct Adapter<'a, T: ?Sized + 'a> { inner: &'a mut T, error: std::io::Result<()>, } impl std::fmt::Write for Adapter<'_, T> { fn write_str(&mut self, s: &str) -> std::fmt::Result { match self.inner.write_all(s.as_bytes()) { Ok(()) => Ok(()), Err(e) => { self.error = Err(e); Err(std::fmt::Error) } } } } let mut output = Adapter { inner: &mut out, error: Ok(()), }; Writer::new(events, &mut output) .write() .map_err(|_| output.error.unwrap_err()) } enum Raw { None, Html, Other, } struct Writer { events: std::iter::Peekable, out: W, raw: Raw, text_only: bool, encountered_footnote: bool, footnote_number: Option, footnote_backlink_written: bool, } impl<'s, I: Iterator>, W: std::fmt::Write> Writer { fn new(events: I, out: W) -> Self { Self { events: events.peekable(), out, raw: Raw::None, text_only: false, encountered_footnote: false, footnote_number: None, footnote_backlink_written: false, } } 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.out.write_char('\n')?; } if self.text_only && !matches!(c, Container::Image(..)) { continue; } match &c { Container::Blockquote => self.out.write_str(" todo!(), Container::ListItem => self.out.write_str(" self.out.write_str(" self.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; self.out .write_str("
\n
\n
    \n")?; } write!(self.out, "
  1. ", number)?; self.footnote_backlink_written = false; continue; } Container::Table => self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" write!(self.out, " self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" { if dst.is_empty() { self.out.write_str(" { self.text_only = true; self.out.write_str(" self.out.write_str(" { self.raw = if format == &"html" { Raw::Html } else { Raw::Other }; continue; } Container::Subscript => self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str("‘")?, Container::DoubleQuoted => self.out.write_str("“")?, } if matches!(c, Container::SingleQuoted | Container::DoubleQuoted) { continue; // TODO add span to allow attributes? } if attrs.iter().any(|(a, _)| a == "class") || matches!( c, Container::Div { class: Some(_) } | Container::Math { .. } ) { self.out.write_str(r#" class=""#)?; let mut classes = attrs .iter() .filter(|(a, _)| a == &"class") .map(|(_, cls)| cls); let has_attr = if let Container::Math { display } = c { self.out.write_str(if display { "math display" } else { "math inline" })?; true } else if let Some(cls) = classes.next() { self.out.write_str(cls)?; for cls in classes { self.out.write_char(' ')?; self.out.write_str(cls)?; } true } else { false }; if let Container::Div { class: Some(cls) } = c { if has_attr { self.out.write_char(' ')?; } self.out.write_str(cls)?; } self.out.write_char('"')?; } match c { Container::CodeBlock { lang } => { if let Some(l) = lang { write!(self.out, r#">"#, l)?; } else { self.out.write_str(">")?; } } Container::Image(..) => { self.out.write_str(r#" alt=""#)?; } Container::Math { display } => { self.out .write_str(if display { r#">\["# } else { r#">\("# })?; } _ => self.out.write_char('>')?, } } Event::End(c) => { if c.is_block_container() && !matches!(c, Container::Footnote { .. }) { self.out.write_char('\n')?; } if self.text_only && !matches!(c, Container::Image(..)) { continue; } match c { Container::Blockquote => self.out.write_str("")?, Container::List(..) => todo!(), Container::ListItem => self.out.write_str("
  2. ")?, Container::DescriptionList => self.out.write_str("")?, Container::DescriptionDetails => self.out.write_str("")?, Container::Footnote { number, .. } => { if !self.footnote_backlink_written { write!( self.out, "\n

    ↩︎︎

    ", number, )?; } self.out.write_str("\n")?; self.footnote_number = None; } Container::Table => self.out.write_str("")?, Container::TableRow => self.out.write_str("")?, Container::Div { .. } => self.out.write_str("")?, Container::Paragraph => { if let Some(num) = self.footnote_number { if matches!( self.events.peek(), Some(Event::End(Container::Footnote { .. })) ) { write!( self.out, r##"↩︎︎"##, num )?; self.footnote_backlink_written = true; } } self.out.write_str("

    ")?; } Container::Heading { level } => write!(self.out, "", level)?, Container::TableCell => self.out.write_str("")?, Container::DescriptionTerm => self.out.write_str("")?, Container::CodeBlock { .. } => self.out.write_str("
    ")?, Container::Span => self.out.write_str("")?, Container::Link(..) => self.out.write_str("")?, Container::Image(src, ..) => { self.text_only = false; if src.is_empty() { self.out.write_str(r#"">"#)?; } else { write!(self.out, r#"" src="{}">"#, src)?; } } Container::Verbatim => self.out.write_str("
    ")?, Container::Math { display } => { self.out.write_str(if display { r#"\]"# } else { r#"\)"# })?; } Container::RawBlock { .. } | Container::RawInline { .. } => { self.raw = Raw::None; } Container::Subscript => self.out.write_str("")?, Container::Superscript => self.out.write_str("")?, Container::Insert => self.out.write_str("")?, Container::Delete => self.out.write_str("")?, Container::Strong => self.out.write_str("")?, Container::Emphasis => self.out.write_str("")?, Container::Mark => self.out.write_str("")?, Container::SingleQuoted => self.out.write_str("’")?, Container::DoubleQuoted => self.out.write_str("”")?, } } Event::Str(s) => { let mut s: &str = s.as_ref(); match self.raw { Raw::None => { let mut ent = ""; while let Some(i) = s.chars().position(|c| { if let Some(s) = match c { '<' => Some("<"), '>' => Some(">"), '&' => Some("&"), '"' => Some("""), _ => None, } { ent = s; true } else { false } }) { self.out.write_str(&s[..i])?; self.out.write_str(ent)?; s = &s[i + 1..]; } self.out.write_str(s)?; } Raw::Html => { self.out.write_str(s)?; } Raw::Other => {} } } Event::Atom(a) => match a { Atom::FootnoteReference(_tag, number) => { write!( self.out, r##"{}"##, number, number, number )?; } Atom::Ellipsis => self.out.write_str("…")?, Atom::EnDash => self.out.write_str("–")?, Atom::EmDash => self.out.write_str("—")?, Atom::ThematicBreak => self.out.write_str("\n
    ")?, Atom::NonBreakingSpace => self.out.write_str(" ")?, Atom::Hardbreak => self.out.write_str("
    \n")?, Atom::Softbreak => self.out.write_char('\n')?, Atom::Blankline | Atom::Escape => {} }, } } if self.encountered_footnote { self.out.write_str("\n
\n
")?; } self.out.write_char('\n')?; Ok(()) } }