use alloc::{string::String, vec::Vec};
#[cfg(feature = "std")]
use pulldown_cmark_escape::IoWriter;
use pulldown_cmark_escape::{escape_href, escape_html, escape_html_body_text, FmtWriter, StrWrite};
use rustc_hash::FxHashMap;
use crate::{
strings::CowStr,
Alignment, BlockQuoteKind, CodeBlockKind,
ContainerKind::*,
Event::{self, *},
LinkType, Tag, TagEnd,
};
enum TableState {
Head,
Body,
}
struct HtmlWriter<'a, I, W> {
iter: I,
writer: W,
end_newline: bool,
in_non_writing_block: bool,
table_state: TableState,
table_alignments: Vec<Alignment>,
table_cell_index: usize,
numbers: FxHashMap<CowStr<'a>, usize>,
}
impl<'a, I, W> HtmlWriter<'a, I, W>
where
I: Iterator<Item = Event<'a>>,
W: StrWrite,
{
fn new(iter: I, writer: W) -> Self {
Self {
iter,
writer,
end_newline: true,
in_non_writing_block: false,
table_state: TableState::Head,
table_alignments: vec![],
table_cell_index: 0,
numbers: FxHashMap::default(),
}
}
#[inline]
fn write_newline(&mut self) -> Result<(), W::Error> {
self.end_newline = true;
self.writer.write_str("\n")
}
#[inline]
fn write(&mut self, s: &str) -> Result<(), W::Error> {
self.writer.write_str(s)?;
if !s.is_empty() {
self.end_newline = s.ends_with('\n');
}
Ok(())
}
fn run(mut self) -> Result<(), W::Error> {
while let Some(event) = self.iter.next() {
match event {
Start(tag) => {
self.start_tag(tag)?;
}
End(tag) => {
self.end_tag(tag)?;
}
Text(text) => {
if !self.in_non_writing_block {
escape_html_body_text(&mut self.writer, &text)?;
self.end_newline = text.ends_with('\n');
}
}
Code(text) => {
self.write("<code>")?;
escape_html_body_text(&mut self.writer, &text)?;
self.write("</code>")?;
}
InlineMath(text) => {
self.write(r#"<span class="math math-inline">"#)?;
escape_html(&mut self.writer, &text)?;
self.write("</span>")?;
}
DisplayMath(text) => {
self.write(r#"<span class="math math-display">"#)?;
escape_html(&mut self.writer, &text)?;
self.write("</span>")?;
}
Html(html) | InlineHtml(html) => {
self.write(&html)?;
}
SoftBreak => {
self.write_newline()?;
}
HardBreak => {
self.write("<br />\n")?;
}
Rule => {
if self.end_newline {
self.write("<hr />\n")?;
} else {
self.write("\n<hr />\n")?;
}
}
FootnoteReference(name) => {
let len = self.numbers.len() + 1;
self.write("<sup class=\"footnote-reference\"><a href=\"#")?;
escape_html(&mut self.writer, &name)?;
self.write("\">")?;
let number = *self.numbers.entry(name).or_insert(len);
write!(&mut self.writer, "{}", number)?;
self.write("</a></sup>")?;
}
TaskListMarker(true) => {
self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\"/>\n")?;
}
TaskListMarker(false) => {
self.write("<input disabled=\"\" type=\"checkbox\"/>\n")?;
}
MdxFlowExpression(s) | MdxTextExpression(s) => {
self.write("{")?;
self.write(&s)?;
self.write("}")?;
}
MdxEsm(s) => {
self.write(&s)?;
self.write("\n")?;
}
}
}
Ok(())
}
fn start_tag(&mut self, tag: Tag<'a>) -> Result<(), W::Error> {
match tag {
Tag::HtmlBlock => Ok(()),
Tag::Paragraph => {
if self.end_newline {
self.write("<p>")
} else {
self.write("\n<p>")
}
}
Tag::Heading {
level,
id,
classes,
attrs,
} => {
if self.end_newline {
self.write("<")?;
} else {
self.write("\n<")?;
}
write!(&mut self.writer, "{}", level)?;
if let Some(id) = id {
self.write(" id=\"")?;
escape_html(&mut self.writer, &id)?;
self.write("\"")?;
}
let mut classes = classes.iter();
if let Some(class) = classes.next() {
self.write(" class=\"")?;
escape_html(&mut self.writer, class)?;
for class in classes {
self.write(" ")?;
escape_html(&mut self.writer, class)?;
}
self.write("\"")?;
}
for (attr, value) in attrs {
self.write(" ")?;
escape_html(&mut self.writer, &attr)?;
if let Some(val) = value {
self.write("=\"")?;
escape_html(&mut self.writer, &val)?;
self.write("\"")?;
} else {
self.write("=\"\"")?;
}
}
self.write(">")
}
Tag::Table(alignments) => {
self.table_alignments = alignments;
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(" style=\"text-align: left\">"),
Some(&Alignment::Center) => self.write(" style=\"text-align: center\">"),
Some(&Alignment::Right) => self.write(" style=\"text-align: right\">"),
_ => self.write(">"),
}
}
Tag::BlockQuote(kind) => {
let class_str = match kind {
None => "",
Some(kind) => match kind {
BlockQuoteKind::Note => " class=\"markdown-alert-note\"",
BlockQuoteKind::Tip => " class=\"markdown-alert-tip\"",
BlockQuoteKind::Important => " class=\"markdown-alert-important\"",
BlockQuoteKind::Warning => " class=\"markdown-alert-warning\"",
BlockQuoteKind::Caution => " class=\"markdown-alert-caution\"",
},
};
if self.end_newline {
self.write(&format!("<blockquote{}>\n", class_str))
} else {
self.write(&format!("\n<blockquote{}>\n", class_str))
}
}
Tag::CodeBlock(info) => {
if !self.end_newline {
self.write_newline()?;
}
match info {
CodeBlockKind::Fenced(info) => {
let lang = info.split(' ').next().unwrap();
if lang.is_empty() {
self.write("<pre><code>")
} else {
self.write("<pre><code class=\"language-")?;
escape_html(&mut self.writer, lang)?;
self.write("\">")
}
}
CodeBlockKind::Indented => self.write("<pre><code>"),
}
}
Tag::ContainerBlock(Default, kind) => {
if !self.end_newline {
self.write_newline()?;
}
self.write("<div class=\"")?;
escape_html(&mut self.writer, &kind)?;
self.write("\">")
}
Tag::ContainerBlock(Spoiler, summary) => {
if !self.end_newline {
self.write_newline()?;
}
if summary.is_empty() {
self.write("<details>")
} else {
self.write("<details><summary>")?;
escape_html(&mut self.writer, summary.as_ref())?;
self.write("</summary>")
}
}
Tag::List(Some(1), _) => {
if self.end_newline {
self.write("<ol>\n")
} else {
self.write("\n<ol>\n")
}
}
Tag::List(Some(start), _) => {
if self.end_newline {
self.write("<ol start=\"")?;
} else {
self.write("\n<ol start=\"")?;
}
write!(&mut self.writer, "{}", start)?;
self.write("\">\n")
}
Tag::List(None, _) => {
if self.end_newline {
self.write("<ul>\n")
} else {
self.write("\n<ul>\n")
}
}
Tag::Item => {
if self.end_newline {
self.write("<li>")
} else {
self.write("\n<li>")
}
}
Tag::DefinitionList => {
if self.end_newline {
self.write("<dl>\n")
} else {
self.write("\n<dl>\n")
}
}
Tag::DefinitionListTitle => {
if self.end_newline {
self.write("<dt>")
} else {
self.write("\n<dt>")
}
}
Tag::DefinitionListDefinition => {
if self.end_newline {
self.write("<dd>")
} else {
self.write("\n<dd>")
}
}
Tag::Subscript => self.write("<sub>"),
Tag::Superscript => self.write("<sup>"),
Tag::Emphasis => self.write("<em>"),
Tag::Strong => self.write("<strong>"),
Tag::Strikethrough => self.write("<del>"),
Tag::Link {
link_type: LinkType::Email,
dest_url,
title,
id: _,
} => {
self.write("<a href=\"mailto:")?;
escape_href(&mut self.writer, &dest_url)?;
if !title.is_empty() {
self.write("\" title=\"")?;
escape_html(&mut self.writer, &title)?;
}
self.write("\">")
}
Tag::Link {
link_type: _,
dest_url,
title,
id: _,
} => {
self.write("<a href=\"")?;
escape_href(&mut self.writer, &dest_url)?;
if !title.is_empty() {
self.write("\" title=\"")?;
escape_html(&mut self.writer, &title)?;
}
self.write("\">")
}
Tag::Image {
link_type: _,
dest_url,
title,
id: _,
} => {
self.write("<img src=\"")?;
escape_href(&mut self.writer, &dest_url)?;
self.write("\" alt=\"")?;
self.raw_text()?;
if !title.is_empty() {
self.write("\" title=\"")?;
escape_html(&mut self.writer, &title)?;
}
self.write("\" />")
}
Tag::FootnoteDefinition(name) => {
if self.end_newline {
self.write("<div class=\"footnote-definition\" id=\"")?;
} else {
self.write("\n<div class=\"footnote-definition\" id=\"")?;
}
escape_html(&mut self.writer, &name)?;
self.write("\"><sup class=\"footnote-definition-label\">")?;
let len = self.numbers.len() + 1;
let number = *self.numbers.entry(name).or_insert(len);
write!(&mut self.writer, "{}", number)?;
self.write("</sup>")
}
Tag::MetadataBlock(_) => {
self.in_non_writing_block = true;
Ok(())
}
Tag::MdxJsxFlowElement(_) | Tag::MdxJsxTextElement(_) => {
Ok(())
}
}
}
fn end_tag(&mut self, tag: TagEnd) -> Result<(), W::Error> {
match tag {
TagEnd::HtmlBlock => {}
TagEnd::Paragraph => {
self.write("</p>\n")?;
}
TagEnd::Heading(level) => {
self.write("</")?;
write!(&mut self.writer, "{}", level)?;
self.write(">\n")?;
}
TagEnd::Table => {
self.write("</tbody></table>\n")?;
}
TagEnd::TableHead => {
self.write("</tr></thead><tbody>\n")?;
self.table_state = TableState::Body;
}
TagEnd::TableRow => {
self.write("</tr>\n")?;
}
TagEnd::TableCell => {
match self.table_state {
TableState::Head => {
self.write("</th>")?;
}
TableState::Body => {
self.write("</td>")?;
}
}
self.table_cell_index += 1;
}
TagEnd::BlockQuote(_) => {
self.write("</blockquote>\n")?;
}
TagEnd::CodeBlock => {
self.write("</code></pre>\n")?;
}
TagEnd::ContainerBlock(Spoiler) => {
self.write("</details>\n")?;
}
TagEnd::ContainerBlock(Default) => {
self.write("</div>\n")?;
}
TagEnd::List(true) => {
self.write("</ol>\n")?;
}
TagEnd::List(false) => {
self.write("</ul>\n")?;
}
TagEnd::Item => {
self.write("</li>\n")?;
}
TagEnd::DefinitionList => {
self.write("</dl>\n")?;
}
TagEnd::DefinitionListTitle => {
self.write("</dt>\n")?;
}
TagEnd::DefinitionListDefinition => {
self.write("</dd>\n")?;
}
TagEnd::Emphasis => {
self.write("</em>")?;
}
TagEnd::Superscript => {
self.write("</sup>")?;
}
TagEnd::Subscript => {
self.write("</sub>")?;
}
TagEnd::Strong => {
self.write("</strong>")?;
}
TagEnd::Strikethrough => {
self.write("</del>")?;
}
TagEnd::Link => {
self.write("</a>")?;
}
TagEnd::Image => (), TagEnd::FootnoteDefinition => {
self.write("</div>\n")?;
}
TagEnd::MetadataBlock(_) => {
self.in_non_writing_block = false;
}
TagEnd::MdxJsxFlowElement | TagEnd::MdxJsxTextElement => {}
}
Ok(())
}
fn raw_text(&mut self) -> Result<(), W::Error> {
let mut nest = 0;
while let Some(event) = self.iter.next() {
match event {
Start(_) => nest += 1,
End(_) => {
if nest == 0 {
break;
}
nest -= 1;
}
Html(_) => {}
InlineHtml(text) | Code(text) | Text(text) => {
escape_html(&mut self.writer, &text)?;
self.end_newline = text.ends_with('\n');
}
InlineMath(text) => {
self.write("$")?;
escape_html(&mut self.writer, &text)?;
self.write("$")?;
}
DisplayMath(text) => {
self.write("$$")?;
escape_html(&mut self.writer, &text)?;
self.write("$$")?;
}
SoftBreak | HardBreak | Rule => {
self.write(" ")?;
}
FootnoteReference(name) => {
let len = self.numbers.len() + 1;
let number = *self.numbers.entry(name).or_insert(len);
write!(&mut self.writer, "[{}]", number)?;
}
TaskListMarker(true) => self.write("[x]")?,
TaskListMarker(false) => self.write("[ ]")?,
MdxFlowExpression(_) | MdxTextExpression(_) | MdxEsm(_) => {}
}
}
Ok(())
}
}
pub fn push_html<'a, I>(s: &mut String, iter: I)
where
I: Iterator<Item = Event<'a>>,
{
write_html_fmt(s, iter).unwrap()
}
#[cfg(feature = "std")]
pub fn write_html_io<'a, I, W>(writer: W, iter: I) -> std::io::Result<()>
where
I: Iterator<Item = Event<'a>>,
W: std::io::Write,
{
HtmlWriter::new(iter, IoWriter(writer)).run()
}
pub fn write_html_fmt<'a, I, W>(writer: W, iter: I) -> core::fmt::Result
where
I: Iterator<Item = Event<'a>>,
W: core::fmt::Write,
{
HtmlWriter::new(iter, FmtWriter(writer)).run()
}