use super::escape;
use anyhow;
use katex;
use crate::{Alignment, CowStr, Event, LinkType, Tag};
use std::collections::HashMap;
use std::io::{self, Write};
enum TableState {
Head,
Body,
}
pub struct HTMLWriter<'e, I, W> {
iter: I,
writer: W,
newline: bool,
table_state: TableState,
table_alignments: Vec<Alignment>,
table_cell_index: usize,
footnote_id: HashMap<CowStr<'e>, usize>,
math: bool,
}
impl<'e, I, W> HTMLWriter<'e, I, W>
where
I: Iterator<Item = Event<'e>>,
W: Write,
{
pub fn new(iter: I, writer: W) -> Self {
Self {
iter,
writer,
newline: false,
table_state: TableState::Head,
table_alignments: vec![],
table_cell_index: 0,
footnote_id: HashMap::new(),
math: false,
}
}
pub fn run(mut self) -> anyhow::Result<()> {
let opts = katex::Opts::builder().display_mode(true).build().unwrap();
while let Some(event) = self.iter.next() {
match event {
Event::Frontmatter(_) => (),
Event::Start(tag) => self.start(tag)?,
Event::End(tag) => self.end(tag)?,
Event::Text(text) => {
if self.math {
self.write(&katex::render_with_opts(&text, opts.clone())?)?;
} else {
escape::escape_html(&mut self.writer, &text)?;
self.newline = text.ends_with('\n');
}
}
Event::Code(code) => {
self.write("<code>")?;
escape::escape_html(&mut self.writer, &code)?;
self.write("</code>")?;
}
Event::Html(raw) => self.write(&raw)?,
Event::Math(raw) => self.write(&katex::render(&raw)?)?,
Event::SoftBreak => self.write_newline()?,
Event::HardBreak => self.write("<br />\n")?,
Event::Rule => {
if self.newline {
self.write("<hr />\n")?
} else {
self.write("\n<hr />\n")?
}
}
Event::FootnoteReference(name) => {
let len = self.footnote_id.len() + 1;
self.write("<sup class=\"footnote-reference\"><a href=\"#")?;
escape::escape_html(&mut self.writer, &name)?;
self.write("\">")?;
let number = *self.footnote_id.entry(name).or_insert(len);
write!(&mut self.writer, "{}", number)?;
self.write("</a></sup>")?;
}
Event::TaskListMarker(true) => {
self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\"/>\n")?;
}
Event::TaskListMarker(false) => {
self.write("<input disabled=\"\" type=\"checkbox\"/>\n")?;
}
}
}
Ok(())
}
#[inline]
fn write(&mut self, s: &str) -> io::Result<()> {
self.writer.write_all(s.as_bytes())?;
if !s.is_empty() {
self.newline = s.ends_with('\n');
}
Ok(())
}
fn write_newline(&mut self) -> io::Result<()> {
self.newline = true;
self.writer.write(b"\n")?;
Ok(())
}
fn start(&mut self, tag: Tag<'e>) -> io::Result<()> {
match tag {
Tag::Emphasis => self.write("<em>"),
Tag::Strong => self.write("<strong>"),
Tag::Strikethrough => self.write("<del>"),
Tag::List(Some(1)) => {
if self.newline {
self.write("<ol>\n")
} else {
self.write("\n<ol>\n")
}
}
Tag::List(Some(start)) => {
if self.newline {
self.write("<ol start=\"")?;
} else {
self.write("\n<ol start=\"")?;
}
write!(&mut self.writer, "{}", start)?;
self.write("\">\n")
}
Tag::List(None) => {
if self.newline {
self.write("<ul>\n")
} else {
self.write("\n<ul>\n")
}
}
Tag::Item => {
if self.newline {
self.write("<li>")
} else {
self.write("\n<li>")
}
}
Tag::Paragraph => {
if self.newline {
self.write("<p>")
} else {
self.write("\n<p>")
}
}
Tag::DisplayMath => {
self.math = true;
self.write(r#"<div class="math">"#)
}
Tag::CodeBlock(_) => {
if !self.newline {
self.write_newline()?;
}
self.write("<pre><code>")
}
Tag::Heading(no) => {
if self.newline {
self.newline = false;
write!(&mut self.writer, "<h{}>", no)
} else {
write!(&mut self.writer, "\n<h{}>", no)
}
}
Tag::BlockQuote => {
if self.newline {
self.write("<blockquote>\n")
} else {
self.write("\n<blockquote>\n")
}
}
Tag::Link(LinkType::Email, dest, title) => {
self.write("<a href=\"mailto:")?;
escape::escape_href(&mut self.writer, &dest)?;
if !title.is_empty() {
self.write("\" title=\"")?;
escape::escape_html(&mut self.writer, &title)?;
}
self.write("\">")
}
Tag::Link(_link_type, dest, title) => {
self.write("<a href=\"")?;
escape::escape_href(&mut self.writer, &dest)?;
if !title.is_empty() {
self.write("\" title=\"")?;
escape::escape_html(&mut self.writer, &title)?;
}
self.write("\">")
}
Tag::FootnoteDefinition(name) => {
if self.newline {
self.write("<div class=\"footnote-definition\" id=\"")?;
} else {
self.write("\n<div class=\"footnote-definition\" id=\"")?;
}
escape::escape_html(&mut self.writer, &*name)?;
self.write("\"><sup class=\"footnote-definition-label\">")?;
let len = self.footnote_id.len() + 1;
let number = *self.footnote_id.entry(name).or_insert(len);
write!(&mut self.writer, "{}", number)?;
self.write("</sup>")
}
Tag::Image(_, url, title) => {
self.write("<img src=\"")?;
escape::escape_href(&mut self.writer, &url)?;
self.write(r#"\" alt=\""#)?;
self.raw_text()?;
if !title.is_empty() {
self.write("\" title=\"")?;
escape::escape_html(&mut self.writer, &title)?;
}
self.write("\">")
}
Tag::Table(align) => {
self.table_alignments = align;
self.write("<table>")
}
Tag::TableHead => {
self.table_state = TableState::Head;
self.table_cell_index = 0;
self.write("<thead><tr>")
}
Tag::TableRow => {
self.table_cell_index = 0;
self.write("<tr>")
}
Tag::TableCell => {
match self.table_state {
TableState::Head => {
self.write("<th")?;
}
TableState::Body => {
self.write("<td")?;
}
}
match self.table_alignments.get(self.table_cell_index) {
Some(&Alignment::Left) => self.write(" align=\"left\">"),
Some(&Alignment::Center) => self.write(" align=\"center\">"),
Some(&Alignment::Right) => self.write(" align=\"right\">"),
_ => self.write(" align=\"center\">"),
}
}
}
}
fn end(&mut self, tag: Tag<'e>) -> io::Result<()> {
match tag {
Tag::Emphasis => self.write("</em>"),
Tag::Strong => self.write("</strong>"),
Tag::Strikethrough => self.write("</del>"),
Tag::List(Some(_)) => self.write("</ol>\n"),
Tag::List(None) => self.write("</ul>"),
Tag::Item => self.write("</li>\n"),
Tag::Paragraph => self.write("</p>\n"),
Tag::DisplayMath => {
self.math = false;
self.write("</div>\n")
}
Tag::CodeBlock(_) => self.write("</code></pre>\n"),
Tag::Heading(no) => write!(&mut self.writer, "</h{}>\n", no),
Tag::BlockQuote => self.write("</blockquote>\n"),
Tag::Link(_, _, _) => self.write("</a>"),
Tag::FootnoteDefinition(_) => self.write("</div>\n"),
Tag::Image(_, _, _) => Ok(()),
Tag::Table(_) => self.write("</tbody></table>\n"),
Tag::TableHead => {
self.table_state = TableState::Body;
self.write("</tr></thead></tbody>\n")
}
Tag::TableRow => self.write("</tr>\n"),
Tag::TableCell => {
self.table_cell_index += 1;
match self.table_state {
TableState::Head => self.write("</th>"),
TableState::Body => self.write("</td>"),
}
}
}
}
fn raw_text(&mut self) -> io::Result<()> {
let mut nest = 0;
while let Some(event) = self.iter.next() {
match event {
Event::Start(_) => nest += 1,
Event::End(_) => {
if nest == 0 {
break;
}
nest -= 1;
}
Event::Frontmatter(_) => (),
Event::Html(text) | Event::Code(text) | Event::Text(text) | Event::Math(text) => {
escape::escape_html(&mut self.writer, &text)?;
self.newline = text.ends_with('\n');
}
Event::SoftBreak | Event::HardBreak | Event::Rule => {
self.write(" ")?;
}
Event::FootnoteReference(name) => {
let len = self.footnote_id.len() + 1;
let number = *self.footnote_id.entry(name).or_insert(len);
write!(&mut self.writer, "[{}]", number)?;
}
Event::TaskListMarker(true) => self.write("[x]")?,
Event::TaskListMarker(false) => self.write("[ ]")?,
}
}
Ok(())
}
}