use crate::r#async::{AsyncRender, AsyncRenderExt, AsyncRenderOutput};
use crate::Alignment;
use crate::Container;
use crate::CowStr;
use crate::Event;
use crate::LinkType;
use crate::ListKind;
use crate::Map;
use crate::OrderedListNumbering::*;
use crate::SpanLinkType;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::AsyncWrite;
pub struct VecWriter(Vec<u8>);
impl VecWriter {
fn new() -> Self {
Self(Vec::new())
}
pub fn into_inner(self) -> Vec<u8> {
self.0
}
}
impl AsyncWrite for VecWriter {
fn poll_write(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, io::Error>> {
self.0.extend_from_slice(buf);
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
Poll::Ready(Ok(()))
}
}
pub async fn render_to_string(doc: &str) -> String {
let mut r = Renderer::default();
r.render_document(doc).await.unwrap();
let bytes = r.into_inner().into_inner();
String::from_utf8(bytes).expect("HTML output must be valid UTF-8")
}
#[derive(Clone)]
pub struct Indentation {
pub string: String,
pub initial_level: usize,
}
impl Default for Indentation {
fn default() -> Self {
Self {
string: "\t".to_string(),
initial_level: 0,
}
}
}
pub struct Renderer<'s, W> {
indent: Option<Indentation>,
writer: Writer<'s>,
output: W,
}
impl<'s> Renderer<'s, VecWriter> {
fn new(indent: Option<Indentation>) -> Self {
Self {
writer: Writer::new(&indent),
indent,
output: VecWriter::new(),
}
}
#[must_use]
pub fn minified() -> Self {
Self::new(None)
}
#[must_use]
pub fn indented(indent: Indentation) -> Self {
Self::new(Some(indent))
}
pub async fn render_to_string(self, input: &str) -> String {
use crate::r#async::AsyncRenderExt;
let mut s = self.with_writer(VecWriter::new());
s.render_document(input).await.expect("Can't fail");
let bytes = s.into_inner().into_inner();
String::from_utf8(bytes).expect("HTML output must be valid UTF-8")
}
pub async fn render_events_to_string<I>(self, events: I) -> String
where
I: Iterator<Item = Event<'s>> + Send,
{
use crate::r#async::AsyncRenderExt;
let mut s = self.with_writer(VecWriter::new());
s.render_events(events).await.expect("Can't fail");
let bytes = s.into_inner().into_inner();
String::from_utf8(bytes).expect("HTML output must be valid UTF-8")
}
}
impl<'s> Default for Renderer<'s, VecWriter> {
fn default() -> Self {
Renderer::new(Some(Indentation {
string: String::new(),
initial_level: 0,
}))
}
}
impl<'s, W> Renderer<'s, W> {
pub fn into_inner(self) -> W {
self.output
}
pub fn with_writer<NewWriter>(self, output: NewWriter) -> Renderer<'s, NewWriter>
where
NewWriter: AsyncWrite,
{
let Renderer {
indent,
writer,
output: _,
} = self;
Renderer {
indent,
writer,
output,
}
}
}
#[async_trait::async_trait]
impl<'s, W> AsyncRender<'s> for Renderer<'s, W>
where
W: AsyncWrite + Unpin + Send,
{
type Error = io::Error;
async fn emit(&mut self, event: Event<'s>) -> Result<(), Self::Error> {
use tokio::io::AsyncWriteExt;
let mut buffer = String::new();
match &event {
Event::Start(Container::Document, _) => {
self.writer.containers.push(Container::Document);
return Ok(());
}
Event::End if self.writer.containers.last() == Some(&Container::Document) => {
self.writer.containers.pop();
self.writer
.render_epilogue(&mut buffer, &self.indent)
.map_err(|_| io::Error::other("format error"))?;
}
_ => {
self.writer
.render_event(event, &mut buffer, &self.indent)
.map_err(|_| io::Error::other("format error"))?;
}
}
if !buffer.is_empty() {
self.output.write_all(buffer.as_bytes()).await?;
}
Ok(())
}
}
impl<'s, W> AsyncRenderOutput<'s> for Renderer<'s, W>
where
W: AsyncWrite + Unpin + Send,
{
type Output = W;
fn into_output(self) -> Self::Output {
self.output
}
}
#[derive(Default)]
enum Raw {
#[default]
None,
Html,
Other,
}
struct Writer<'s> {
depth: usize,
raw: Raw,
img_alt_text: usize,
list_tightness: Vec<bool>,
first_line: bool,
ignore: bool,
containers: Vec<Container<'s>>,
footnotes: Footnotes<'s>,
}
impl<'s> Writer<'s> {
fn new(indent: &Option<Indentation>) -> Self {
let depth = if let Some(indent) = indent {
indent.initial_level
} else {
0
};
Self {
depth,
raw: Raw::default(),
img_alt_text: 0,
list_tightness: Vec::new(),
first_line: true,
ignore: false,
containers: vec![],
footnotes: Footnotes::default(),
}
}
fn block<W>(
&mut self,
mut out: W,
indent: &Option<Indentation>,
depth_change: isize,
) -> std::fmt::Result
where
W: std::fmt::Write,
{
if indent.is_none() {
return Ok(());
}
if !self.first_line {
out.write_char('\n')?;
}
let next_depth = (self.depth as isize + depth_change) as usize;
if depth_change < 0 {
self.depth = next_depth;
}
self.indent(&mut out, indent)?;
if depth_change > 0 {
self.depth = next_depth;
}
Ok(())
}
fn indent<W>(&self, mut out: W, indent: &Option<Indentation>) -> std::fmt::Result
where
W: std::fmt::Write,
{
if let Some(indent) = indent {
if !indent.string.is_empty() {
for _ in 0..self.depth {
out.write_str(&indent.string)?;
}
}
}
Ok(())
}
fn render_event<W>(
&mut self,
e: Event<'s>,
out: W,
indent: &Option<Indentation>,
) -> std::fmt::Result
where
W: std::fmt::Write,
{
let c = if e == Event::End {
self.containers.pop()
} else {
None
};
let push = if let Event::Start(c, ..) = &e {
Some(c.clone())
} else {
None
};
let res = self.render_event_inner(e, c, out, indent);
if let Some(p) = push {
self.containers.push(p);
}
res
}
fn render_event_inner<W>(
&mut self,
e: Event<'s>,
end_container: Option<Container>,
mut out: W,
indent: &Option<Indentation>,
) -> std::fmt::Result
where
W: std::fmt::Write,
{
if let Event::Start(Container::Footnote { label }, ..) = e {
self.footnotes.start(label.clone(), Vec::new());
return Ok(());
} else if let Some(events) = self.footnotes.current() {
if matches!(e, Event::End)
&& matches!(
end_container.as_ref().expect("Missing container"),
Container::Footnote { .. }
)
{
self.footnotes.end();
} else {
events.push(e.clone());
}
return Ok(());
}
if let Event::Start(Container::LinkDefinition { .. }, ..) = e {
self.ignore = true;
return Ok(());
}
if matches!(e, Event::End)
&& matches!(
end_container.as_ref().expect("Missing container"),
Container::LinkDefinition { .. }
)
{
self.ignore = false;
return Ok(());
}
if self.ignore {
return Ok(());
}
match e {
Event::Start(c, attrs) => {
if c.is_block() {
self.block(&mut out, indent, c.is_block_container().into())?;
}
if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
return Ok(());
}
match &c {
Container::Blockquote => out.write_str("<blockquote")?,
Container::List { kind, tight } => {
self.list_tightness.push(*tight);
match kind {
ListKind::Unordered(..) | ListKind::Task(..) => out.write_str("<ul")?,
ListKind::Ordered {
numbering, start, ..
} => {
out.write_str("<ol")?;
if *start > 1 {
out.write_str(&format!(r#" start="{}""#, start))?;
}
if let Some(ty) = match numbering {
Decimal => None,
AlphaLower => Some('a'),
AlphaUpper => Some('A'),
RomanLower => Some('i'),
RomanUpper => Some('I'),
} {
out.write_str(&format!(r#" type="{}""#, ty))?;
}
}
}
}
Container::ListItem | Container::TaskListItem { .. } => {
out.write_str("<li")?;
}
Container::DescriptionList => out.write_str("<dl")?,
Container::DescriptionDetails => out.write_str("<dd")?,
Container::Footnote { .. } => unreachable!(),
Container::Table => out.write_str("<table")?,
Container::TableRow { .. } => out.write_str("<tr")?,
Container::Section { .. } => out.write_str("<section")?,
Container::Div { .. } => out.write_str("<div")?,
Container::Paragraph => {
if matches!(self.list_tightness.last(), Some(true)) {
return Ok(());
}
out.write_str("<p")?;
}
Container::Heading { level, .. } => out.write_str(&format!("<h{}", level))?,
Container::TableCell { head: false, .. } => out.write_str("<td")?,
Container::TableCell { head: true, .. } => out.write_str("<th")?,
Container::Caption => out.write_str("<caption")?,
Container::DescriptionTerm => out.write_str("<dt")?,
Container::CodeBlock { .. } => out.write_str("<pre")?,
Container::Span | Container::Math { .. } => out.write_str("<span")?,
Container::Link(dst, ty) => {
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
out.write_str("<a")?;
} else {
out.write_str(r#"<a href=""#)?;
if matches!(ty, LinkType::Email) {
out.write_str("mailto:")?;
}
write_attr(dst, &mut out)?;
out.write_char('"')?;
}
}
Container::Image(..) => {
self.img_alt_text += 1;
if self.img_alt_text == 1 {
out.write_str("<img")?;
} else {
return Ok(());
}
}
Container::Verbatim => out.write_str("<code")?,
Container::RawBlock { format } | Container::RawInline { format } => {
self.raw = if format == "html" {
Raw::Html
} else {
Raw::Other
};
return Ok(());
}
Container::Subscript => out.write_str("<sub")?,
Container::Superscript => out.write_str("<sup")?,
Container::Insert => out.write_str("<ins")?,
Container::Delete => out.write_str("<del")?,
Container::Strong => out.write_str("<strong")?,
Container::Emphasis => out.write_str("<em")?,
Container::Mark => out.write_str("<mark")?,
Container::LinkDefinition { .. } => return Ok(()),
Container::Document => unreachable!("Document start is handled in emit()"),
}
let mut id_written = false;
let mut class_written = false;
for (a, v) in attrs.unique_pairs() {
out.write_str(&format!(r#" {}=""#, a))?;
for part in v.parts() {
write_attr(part, &mut out)?;
}
match a {
"class" => {
class_written = true;
write_class(&c, true, &mut out)?;
}
"id" => id_written = true,
_ => {}
}
out.write_char('"')?;
}
if let Container::Heading {
id,
has_section: false,
..
}
| Container::Section { id } = &c
{
if !id_written {
out.write_str(r#" id=""#)?;
write_attr(id, &mut out)?;
out.write_char('"')?;
}
} else if (matches!(c.clone(), Container::Div { class } if !class.is_empty())
|| matches!(
c,
Container::Math { .. }
| Container::List {
kind: ListKind::Task(..),
..
}
))
&& !class_written
{
out.write_str(r#" class=""#)?;
write_class(&c, false, &mut out)?;
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",
};
out.write_str(&format!(r#" style="text-align: {};">"#, a))?;
}
Container::CodeBlock { language } => {
if language.is_empty() {
out.write_str("><code>")?;
} else {
out.write_str(r#"><code class="language-"#)?;
write_attr(&language, &mut out)?;
out.write_str(r#"">"#)?;
}
}
Container::Image(..) => {
if self.img_alt_text == 1 {
out.write_str(r#" alt=""#)?;
}
}
Container::Math { display } => {
out.write_str(if display { r">\[" } else { r">\(" })?;
}
Container::TaskListItem { checked } => {
out.write_char('>')?;
self.block(&mut out, indent, 0)?;
if checked {
out.write_str(r#"<input disabled="" type="checkbox" checked=""/>"#)?;
} else {
out.write_str(r#"<input disabled="" type="checkbox"/>"#)?;
}
}
_ => out.write_char('>')?,
}
}
Event::End => {
let c = end_container.expect("Missing removed container");
if c.is_block_container() {
self.block(&mut out, indent, -1)?;
}
if self.img_alt_text > 0 && !matches!(c, Container::Image { .. }) {
return Ok(());
}
match c {
Container::Blockquote => out.write_str("</blockquote>")?,
Container::List { kind, .. } => {
self.list_tightness.pop();
match kind {
ListKind::Unordered(..) | ListKind::Task(..) => {
out.write_str("</ul>")?;
}
ListKind::Ordered { .. } => out.write_str("</ol>")?,
}
}
Container::ListItem | Container::TaskListItem { .. } => {
out.write_str("</li>")?;
}
Container::DescriptionList => out.write_str("</dl>")?,
Container::DescriptionDetails => out.write_str("</dd>")?,
Container::Footnote { .. } => unreachable!(),
Container::Table => out.write_str("</table>")?,
Container::TableRow { .. } => out.write_str("</tr>")?,
Container::Section { .. } => out.write_str("</section>")?,
Container::Div { .. } => out.write_str("</div>")?,
Container::Paragraph => {
if matches!(self.list_tightness.last(), Some(true)) {
return Ok(());
}
if !self.footnotes.in_epilogue() {
out.write_str("</p>")?;
}
}
Container::Heading { level, .. } => out.write_str(&format!("</h{}>", level))?,
Container::TableCell { head: false, .. } => out.write_str("</td>")?,
Container::TableCell { head: true, .. } => out.write_str("</th>")?,
Container::Caption => out.write_str("</caption>")?,
Container::DescriptionTerm => out.write_str("</dt>")?,
Container::CodeBlock { .. } => out.write_str("</code></pre>")?,
Container::Span => out.write_str("</span>")?,
Container::Link { .. } => out.write_str("</a>")?,
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("</code>")?,
Container::Math { display } => {
out.write_str(if display { r"\]</span>" } else { r"\)</span>" })?;
}
Container::RawBlock { .. } | Container::RawInline { .. } => {
self.raw = Raw::None;
}
Container::Subscript => out.write_str("</sub>")?,
Container::Superscript => out.write_str("</sup>")?,
Container::Insert => out.write_str("</ins>")?,
Container::Delete => out.write_str("</del>")?,
Container::Strong => out.write_str("</strong>")?,
Container::Emphasis => out.write_str("</em>")?,
Container::Mark => out.write_str("</mark>")?,
Container::Document => unreachable!("Document end is handled in emit()"),
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(label) => {
let number = self.footnotes.reference(label.clone());
if self.img_alt_text == 0 {
out.write_str(&format!(
r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##,
number, number, number
))?;
}
}
Event::Symbol(sym) => out.write_str(&format!(":{}:", 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("<br>")?;
self.block(&mut out, indent, 0)?;
}
Event::Softbreak => {
out.write_char('\n')?;
self.indent(&mut out, indent)?;
}
Event::Escape | Event::Blankline | Event::Attributes(..) => {}
Event::ThematicBreak(attrs) => {
self.block(&mut out, indent, 0)?;
out.write_str("<hr")?;
for (a, v) in attrs.unique_pairs() {
out.write_str(&format!(r#" {}=""#, a))?;
for part in v.parts() {
write_attr(part, &mut out)?;
}
out.write_char('"')?;
}
out.write_str(">")?;
}
}
self.first_line = false;
Ok(())
}
fn render_epilogue<W>(&mut self, mut out: W, indent: &Option<Indentation>) -> std::fmt::Result
where
W: std::fmt::Write,
{
if self.footnotes.reference_encountered() {
self.block(&mut out, indent, 0)?;
out.write_str("<section role=\"doc-endnotes\">")?;
self.block(&mut out, indent, 0)?;
out.write_str("<hr>")?;
self.block(&mut out, indent, 0)?;
out.write_str("<ol>")?;
while let Some((number, events)) = self.footnotes.next() {
self.block(&mut out, indent, 0)?;
out.write_str(&format!("<li id=\"fn{}\">", number))?;
let mut unclosed_para = false;
for e in events.into_iter().flatten() {
if matches!(&e, Event::Blankline | Event::Escape) {
continue;
}
if unclosed_para {
out.write_str("</p>")?;
}
let is_end_paragraph = matches!(e, Event::End)
&& self.containers.last() == Some(&Container::Paragraph);
self.render_event(e, &mut out, indent)?;
unclosed_para =
is_end_paragraph && !matches!(self.list_tightness.last(), Some(true));
}
if !unclosed_para {
self.block(&mut out, indent, 0)?;
out.write_str("<p>")?;
}
out.write_str(&format!(
"<a href=\"#fnref{}\" role=\"doc-backlink\">\u{21A9}\u{FE0E}</a></p>",
number,
))?;
self.block(&mut out, indent, 0)?;
out.write_str("</li>")?;
}
self.block(&mut out, indent, 0)?;
out.write_str("</ol>")?;
self.block(&mut out, indent, 0)?;
out.write_str("</section>")?;
}
if indent.is_some() {
out.write_char('\n')?;
}
Ok(())
}
}
fn write_class<W>(c: &Container, mut first_written: bool, out: &mut W) -> std::fmt::Result
where
W: std::fmt::Write,
{
if let Some(cls) = match c {
Container::List {
kind: ListKind::Task(..),
..
} => Some("task-list"),
Container::Math { display: false } => Some("math inline"),
Container::Math { display: true } => Some("math display"),
_ => None,
} {
first_written = true;
out.write_str(cls)?;
}
if let Container::Div { class } = c {
if !class.is_empty() {
if first_written {
out.write_char(' ')?;
}
out.write_str(class)?;
}
}
Ok(())
}
fn write_text<W>(s: &str, out: W) -> std::fmt::Result
where
W: std::fmt::Write,
{
write_escape(s, false, out)
}
fn write_attr<W>(s: &str, out: W) -> std::fmt::Result
where
W: std::fmt::Write,
{
write_escape(s, true, out)
}
fn write_escape<W>(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,
}
.is_some_and(|s| {
ent = s;
true
})
}) {
out.write_str(&s[..i])?;
out.write_str(ent)?;
s = &s[i + 1..];
}
out.write_str(s)
}
#[derive(Default)]
struct Footnotes<'s> {
open: Vec<(CowStr<'s>, Vec<Event<'s>>)>,
references: Vec<CowStr<'s>>,
events: Map<CowStr<'s>, Vec<Event<'s>>>,
number: usize,
}
impl<'s> Footnotes<'s> {
fn reference_encountered(&self) -> bool {
!self.references.is_empty()
}
fn in_epilogue(&self) -> bool {
self.number > 0
}
fn reference(&mut self, label: CowStr<'s>) -> usize {
self.references
.iter()
.position(|t| *t == label)
.map_or_else(
|| {
self.references.push(label);
self.references.len()
},
|i| i + 1,
)
}
fn start(&mut self, label: CowStr<'s>, events: Vec<Event<'s>>) {
self.open.push((label, events));
}
fn current(&mut self) -> Option<&mut Vec<Event<'s>>> {
self.open.last_mut().map(|(_, e)| e)
}
fn end(&mut self) {
let (label, stage) = self.open.pop().unwrap();
self.events.insert(label, stage);
}
}
impl<'s> Iterator for Footnotes<'s> {
type Item = (usize, Option<Vec<Event<'s>>>);
fn next(&mut self) -> Option<Self::Item> {
self.references.get(self.number).map(|label| {
self.number += 1;
(self.number, self.events.remove(label))
})
}
}