mod anchorizer;
mod context;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::{self, Write};
use std::str;
use crate::adapters::HeadingMeta;
use crate::character_set::character_set;
use crate::ctype::isspace;
#[cfg(feature = "shortcodes")]
use crate::nodes::NodeShortCode;
use crate::nodes::{
ListType, Node, NodeAlert, NodeCode, NodeCodeBlock, NodeFootnoteDefinition,
NodeFootnoteReference, NodeHeading, NodeHtmlBlock, NodeLink, NodeList, NodeMath, NodeValue,
NodeWikiLink, TableAlignment,
};
use crate::parser::options::{Options, Plugins};
use crate::{node_matches, scanners};
#[doc(hidden)]
pub use anchorizer::Anchorizer;
pub use context::Context;
pub fn format_document<'a>(
root: Node<'a>,
options: &Options,
output: &mut dyn Write,
) -> fmt::Result {
format_document_with_formatter(
root,
options,
output,
&Plugins::default(),
format_node_default,
(),
)
}
pub fn format_document_with_plugins<'a>(
root: Node<'a>,
options: &Options,
output: &mut dyn fmt::Write,
plugins: &Plugins,
) -> fmt::Result {
format_document_with_formatter(root, options, output, plugins, format_node_default, ())
}
#[derive(Debug, Clone, Copy)]
pub enum ChildRendering {
HTML,
Plain,
Skip,
}
#[macro_export]
macro_rules! create_formatter {
($name:ident, { $( $pat:pat => | $( $capture:ident ),* | $case:tt ),* }) => {
$crate::create_formatter!($name, { $( $pat => | $( $capture ),* | $case ),*, });
};
($name:ident<$type:ty>, { $( $pat:pat => | $( $capture:ident ),* | $case:tt ),* }) => {
$crate::create_formatter!($name<$type>, { $( $pat => | $( $capture ),* | $case ),*, });
};
($name:ident, { $( $pat:pat => | $( $capture:ident ),* | $case:tt ),*, }) => {
$crate::create_formatter!($name<()>, { $( $pat => | $( $capture ),* | $case ),*, });
};
($name:ident<()>, { $( $pat:pat => | $( $capture:ident ),* | $case:tt ),*, }) => {
#[allow(missing_copy_implementations)]
#[allow(missing_debug_implementations)]
pub struct $name;
impl $name {
#[inline]
pub fn format_document<'a>(
root: &'a $crate::nodes::AstNode<'a>,
options: &$crate::Options,
output: &mut dyn ::std::fmt::Write,
) -> ::std::fmt::Result {
$crate::html::format_document_with_formatter(
root,
options,
output,
&$crate::options::Plugins::default(),
Self::formatter,
()
)
}
#[inline]
pub fn format_document_with_plugins<'a, 'o, 'c: 'o>(
root: &'a $crate::nodes::AstNode<'a>,
options: &'o $crate::Options<'c>,
output: &'o mut dyn ::std::fmt::Write,
plugins: &'o $crate::options::Plugins<'o>,
) -> ::std::fmt::Result {
$crate::html::format_document_with_formatter(
root,
options,
output,
plugins,
Self::formatter,
()
)
}
fn formatter<'a>(
context: &mut $crate::html::Context<()>,
node: &'a $crate::nodes::AstNode<'a>,
entering: bool,
) -> ::std::result::Result<$crate::html::ChildRendering, ::std::fmt::Error> {
match node.data.borrow().value {
$(
$pat => {
$crate::formatter_captures!((context, node, entering), ($( $capture ),*));
$case
#[allow(unreachable_code)]
::std::result::Result::Ok($crate::html::ChildRendering::HTML)
}
),*
_ => $crate::html::format_node_default(context, node, entering),
}
}
}
};
($name:ident<$type:ty>, { $( $pat:pat => | $( $capture:ident ),* | $case:tt ),*, }) => {
#[allow(missing_copy_implementations)]
#[allow(missing_debug_implementations)]
pub struct $name;
impl $name {
#[inline]
pub fn format_document<'a>(
root: &'a $crate::nodes::AstNode<'a>,
options: &$crate::Options,
output: &mut dyn ::std::fmt::Write,
user: $type,
) -> ::std::result::Result<$type, ::std::fmt::Error> {
$crate::html::format_document_with_formatter(
root,
options,
output,
&$crate::options::Plugins::default(),
Self::formatter,
user
)
}
#[inline]
pub fn format_document_with_plugins<'a, 'o, 'c: 'o>(
root: &'a $crate::nodes::AstNode<'a>,
options: &'o $crate::Options<'c>,
output: &'o mut dyn ::std::fmt::Write,
plugins: &'o $crate::options::Plugins<'o>,
user: $type,
) -> ::std::result::Result<$type, ::std::fmt::Error> {
$crate::html::format_document_with_formatter(
root,
options,
output,
plugins,
Self::formatter,
user
)
}
fn formatter<'a>(
context: &mut $crate::html::Context<$type>,
node: &'a $crate::nodes::AstNode<'a>,
entering: bool,
) -> ::std::result::Result<$crate::html::ChildRendering, ::std::fmt::Error> {
match node.data.borrow().value {
$(
$pat => {
$crate::formatter_captures!((context, node, entering), ($( $capture ),*));
$case
#[allow(unreachable_code)]
::std::result::Result::Ok($crate::html::ChildRendering::HTML)
}
),*
_ => $crate::html::format_node_default(context, node, entering),
}
}
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! formatter_captures {
(($context:ident, $node:ident, $entering:ident), context, $bind:ident) => {
let $bind = $context;
};
(($context:ident, $node:ident, $entering:ident), node, $bind:ident) => {
let $bind = $node;
};
(($context:ident, $node:ident, $entering:ident), entering, $bind:ident) => {
let $bind = $entering;
};
(($context:ident, $node:ident, $entering:ident), $unknown:ident, $bind:ident) => {
compile_error!(concat!("unknown capture '", stringify!($unknown), "'; available are 'context', 'node', 'entering'"));
};
(($context:ident, $node:ident, $entering:ident), ($capture:ident)) => {
$crate::formatter_captures!(($context, $node, $entering), $capture, $capture);
};
(($context:ident, $node:ident, $entering:ident), ($capture:ident, $( $rest:ident ),*)) => {
$crate::formatter_captures!(($context, $node, $entering), $capture, $capture);
$crate::formatter_captures!(($context, $node, $entering), ($( $rest ),*));
};
}
pub fn format_document_with_formatter<'a, 'o, 'c: 'o, T>(
root: Node<'a>,
options: &'o Options<'c>,
output: &'o mut dyn Write,
plugins: &'o Plugins<'o>,
formatter: fn(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error>,
user: T,
) -> Result<T, fmt::Error> {
let mut context = Context::new(output, options, plugins, user);
enum Phase {
Pre,
Post,
}
let mut stack = vec![(root, ChildRendering::HTML, Phase::Pre)];
while let Some((node, child_rendering, phase)) = stack.pop() {
match phase {
Phase::Pre => {
let new_cr = match child_rendering {
ChildRendering::Plain => {
match node.data.borrow().value {
NodeValue::Text(ref literal) => {
context.escape(literal)?;
}
NodeValue::Code(NodeCode { ref literal, .. })
| NodeValue::HtmlInline(ref literal) => {
context.escape(literal)?;
}
NodeValue::LineBreak | NodeValue::SoftBreak => {
fmt::Write::write_str(&mut context, " ")?;
}
NodeValue::Math(NodeMath { ref literal, .. }) => {
context.escape(literal)?;
}
_ => (),
}
ChildRendering::Plain
}
ChildRendering::HTML => {
stack.push((node, ChildRendering::HTML, Phase::Post));
formatter(&mut context, node, true)?
}
ChildRendering::Skip => {
unreachable!()
}
};
if !matches!(new_cr, ChildRendering::Skip) {
for ch in node.reverse_children() {
stack.push((ch, new_cr, Phase::Pre));
}
}
}
Phase::Post => {
debug_assert!(matches!(child_rendering, ChildRendering::HTML));
formatter(&mut context, node, false)?;
}
}
}
context.finish()
}
#[inline]
pub fn format_node_default<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
match node.data.borrow().value {
NodeValue::BlockQuote => render_block_quote(context, node, entering),
NodeValue::Code(ref nc) => render_code(context, node, entering, nc),
NodeValue::CodeBlock(ref ncb) => render_code_block(context, node, entering, ncb),
NodeValue::Document => Ok(ChildRendering::HTML),
NodeValue::Emph => render_emph(context, node, entering),
NodeValue::Heading(ref nh) => render_heading(context, node, entering, nh),
NodeValue::HtmlBlock(ref nhb) => render_html_block(context, entering, nhb),
NodeValue::HtmlInline(ref literal) => render_html_inline(context, entering, literal),
NodeValue::Image(ref nl) => render_image(context, node, entering, nl),
NodeValue::Item(_) => render_item(context, node, entering),
NodeValue::LineBreak => render_line_break(context, node, entering),
NodeValue::Link(ref nl) => render_link(context, node, entering, nl),
NodeValue::List(ref nl) => render_list(context, node, entering, nl),
NodeValue::Paragraph => render_paragraph(context, node, entering),
NodeValue::SoftBreak => render_soft_break(context, node, entering),
NodeValue::Strong => render_strong(context, node, entering),
NodeValue::Text(ref literal) => render_text(context, entering, literal),
NodeValue::ThematicBreak => render_thematic_break(context, node, entering),
NodeValue::FootnoteDefinition(ref nfd) => {
render_footnote_definition(context, node, entering, nfd)
}
NodeValue::FootnoteReference(ref nfr) => {
render_footnote_reference(context, node, entering, nfr)
}
NodeValue::Strikethrough => render_strikethrough(context, node, entering),
NodeValue::Table(_) => render_table(context, node, entering),
NodeValue::TableCell => render_table_cell(context, node, entering),
NodeValue::TableRow(thead) => render_table_row(context, node, entering, thead),
NodeValue::TaskItem(symbol) => render_task_item(context, node, entering, symbol),
NodeValue::Alert(ref alert) => render_alert(context, node, entering, alert),
NodeValue::DescriptionDetails => render_description_details(context, node, entering),
NodeValue::DescriptionItem(_) => Ok(ChildRendering::HTML),
NodeValue::DescriptionList => render_description_list(context, node, entering),
NodeValue::DescriptionTerm => render_description_term(context, node, entering),
NodeValue::Escaped => render_escaped(context, node, entering),
NodeValue::EscapedTag(ref net) => render_escaped_tag(context, net),
NodeValue::FrontMatter(_) => Ok(ChildRendering::HTML),
NodeValue::Math(ref nm) => render_math(context, node, entering, nm),
NodeValue::MultilineBlockQuote(_) => render_multiline_block_quote(context, node, entering),
NodeValue::Raw(ref literal) => render_raw(context, entering, literal),
#[cfg(feature = "shortcodes")]
NodeValue::ShortCode(ref nsc) => render_short_code(context, entering, nsc),
NodeValue::SpoileredText => render_spoiler_text(context, node, entering),
NodeValue::Subscript => render_subscript(context, node, entering),
NodeValue::Superscript => render_superscript(context, node, entering),
NodeValue::Underline => render_underline(context, node, entering),
NodeValue::WikiLink(ref nwl) => render_wiki_link(context, node, entering, nwl),
}
}
pub fn render_sourcepos<'a, T>(context: &mut Context<T>, node: Node<'a>) -> fmt::Result {
if context.options.render.sourcepos {
let ast = node.data.borrow();
if ast.sourcepos.start.line > 0 {
write!(context, " data-sourcepos=\"{}\"", ast.sourcepos)?;
}
}
Ok(())
}
fn render_block_quote<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.cr()?;
context.write_str("<blockquote")?;
render_sourcepos(context, node)?;
context.write_str(">\n")?;
} else {
context.cr()?;
context.write_str("</blockquote>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_code<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
nc: &NodeCode,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<code")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
context.escape(&nc.literal)?;
context.write_str("</code>")?;
}
Ok(ChildRendering::HTML)
}
fn render_code_block<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
ncb: &NodeCodeBlock,
) -> Result<ChildRendering, fmt::Error> {
if entering {
if ncb.info.eq("math") {
render_math_code_block(context, node, &ncb.literal)?;
} else {
context.cr()?;
let mut first_tag = 0;
let mut pre_attributes: HashMap<&str, Cow<str>> = HashMap::new();
let mut code_attributes: HashMap<&str, Cow<str>> = HashMap::new();
let code_attr: String;
let literal = &ncb.literal;
let info = &ncb.info;
let info_bytes = info.as_bytes();
if !info.is_empty() {
while first_tag < info.len() && !isspace(info_bytes[first_tag]) {
first_tag += 1;
}
let lang_str = &info[..first_tag];
let info_str = info[first_tag..].trim();
if context.options.render.github_pre_lang {
pre_attributes.insert("lang", lang_str.into());
if context.options.render.full_info_string && !info_str.is_empty() {
pre_attributes.insert("data-meta", info_str.trim().into());
}
} else {
code_attr = format!("language-{}", lang_str);
code_attributes.insert("class", code_attr.into());
if context.options.render.full_info_string && !info_str.is_empty() {
code_attributes.insert("data-meta", info_str.into());
}
}
}
if context.options.render.sourcepos {
let ast = node.data.borrow();
pre_attributes.insert("data-sourcepos", ast.sourcepos.to_string().into());
}
match context.plugins.render.codefence_syntax_highlighter {
None => {
write_opening_tag(context, "pre", pre_attributes.into_iter())?;
write_opening_tag(context, "code", code_attributes.into_iter())?;
context.escape(literal)?;
context.write_str("</code></pre>\n")?
}
Some(highlighter) => {
highlighter.write_pre_tag(context, pre_attributes)?;
highlighter.write_code_tag(context, code_attributes)?;
highlighter.write_highlighted(
context,
Some(&info[..first_tag]),
&ncb.literal,
)?;
context.write_str("</code></pre>\n")?
}
}
}
}
Ok(ChildRendering::HTML)
}
fn render_emph<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<em")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.write_str("</em>")?;
}
Ok(ChildRendering::HTML)
}
fn render_heading<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
nh: &NodeHeading,
) -> Result<ChildRendering, fmt::Error> {
match context.plugins.render.heading_adapter {
None => {
if entering {
context.cr()?;
write!(context, "<h{}", nh.level)?;
render_sourcepos(context, node)?;
context.write_str(">")?;
if let Some(ref prefix) = context.options.extension.header_ids {
let text_content = collect_text(node);
let id = context.anchorizer.anchorize(&text_content);
write!(
context,
"<a href=\"#{}\" aria-hidden=\"true\" class=\"anchor\" id=\"{}{}\"></a>",
id, prefix, id
)?;
}
} else {
writeln!(context, "</h{}>", nh.level)?;
}
}
Some(adapter) => {
let text_content = collect_text(node);
let heading = HeadingMeta {
level: nh.level,
content: text_content,
};
if entering {
context.cr()?;
let sp = if context.options.render.sourcepos {
Some(node.data.borrow().sourcepos)
} else {
None
};
adapter.enter(context, &heading, sp)?;
} else {
adapter.exit(context, &heading)?;
}
}
}
Ok(ChildRendering::HTML)
}
fn render_html_block<T>(
context: &mut Context<T>,
entering: bool,
nhb: &NodeHtmlBlock,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.cr()?;
let literal = &nhb.literal;
if context.options.render.escape {
context.escape(literal)?;
} else if !context.options.render.unsafe_ {
context.write_str("<!-- raw HTML omitted -->")?;
} else if context.options.extension.tagfilter {
tagfilter_block(context, literal)?;
} else {
context.write_str(literal)?;
}
context.cr()?;
}
Ok(ChildRendering::HTML)
}
fn render_html_inline<T>(
context: &mut Context<T>,
entering: bool,
literal: &str,
) -> Result<ChildRendering, fmt::Error> {
if entering {
if context.options.render.escape {
context.escape(literal)?;
} else if !context.options.render.unsafe_ {
context.write_str("<!-- raw HTML omitted -->")?;
} else if context.options.extension.tagfilter && tagfilter(literal) {
context.write_str("<")?;
context.write_str(&literal[1..])?;
} else {
context.write_str(literal)?;
}
}
Ok(ChildRendering::HTML)
}
fn render_image<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
nl: &NodeLink,
) -> Result<ChildRendering, fmt::Error> {
if entering {
if context.options.render.figure_with_caption {
context.write_str("<figure>")?;
}
context.write_str("<img")?;
render_sourcepos(context, node)?;
context.write_str(" src=\"")?;
let url = &nl.url;
if context.options.render.unsafe_ || !dangerous_url(url) {
if let Some(rewriter) = &context.options.extension.image_url_rewriter {
context.escape_href(&rewriter.to_html(&nl.url))?;
} else {
context.escape_href(url)?;
}
}
context.write_str("\" alt=\"")?;
return Ok(ChildRendering::Plain);
} else {
if !nl.title.is_empty() {
context.write_str("\" title=\"")?;
context.escape(&nl.title)?;
}
context.write_str("\" />")?;
if context.options.render.figure_with_caption {
if !nl.title.is_empty() {
context.write_str("<figcaption>")?;
context.escape(&nl.title)?;
context.write_str("</figcaption>")?;
}
context.write_str("</figure>")?;
};
}
Ok(ChildRendering::HTML)
}
fn render_item<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.cr()?;
context.write_str("<li")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.write_str("</li>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_line_break<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<br")?;
render_sourcepos(context, node)?;
context.write_str(" />\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_link<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
nl: &NodeLink,
) -> Result<ChildRendering, fmt::Error> {
let parent_node = node.parent();
if !context.options.parse.relaxed_autolinks
|| (parent_node.is_none()
|| !matches!(
parent_node.unwrap().data.borrow().value,
NodeValue::Link(..)
))
{
if entering {
context.write_str("<a")?;
render_sourcepos(context, node)?;
context.write_str(" href=\"")?;
let url = &nl.url;
if context.options.render.unsafe_ || !dangerous_url(url) {
if let Some(rewriter) = &context.options.extension.link_url_rewriter {
context.escape_href(&rewriter.to_html(&nl.url))?;
} else {
context.escape_href(url)?;
}
}
if !nl.title.is_empty() {
context.write_str("\" title=\"")?;
context.escape(&nl.title)?;
}
context.write_str("\">")?;
} else {
context.write_str("</a>")?;
}
}
Ok(ChildRendering::HTML)
}
fn render_list<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
nl: &NodeList,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.cr()?;
match nl.list_type {
ListType::Bullet => {
context.write_str("<ul")?;
if nl.is_task_list && context.options.render.tasklist_classes {
context.write_str(" class=\"contains-task-list\"")?;
}
render_sourcepos(context, node)?;
context.write_str(">\n")?;
}
ListType::Ordered => {
context.write_str("<ol")?;
if nl.is_task_list && context.options.render.tasklist_classes {
context.write_str(" class=\"contains-task-list\"")?;
}
render_sourcepos(context, node)?;
if nl.start == 1 {
context.write_str(">\n")?;
} else {
writeln!(context, " start=\"{}\">", nl.start)?;
}
}
}
} else if nl.list_type == ListType::Bullet {
context.write_str("</ul>\n")?;
} else {
context.write_str("</ol>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_paragraph<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
let tight =
node.parent()
.and_then(|n| n.parent())
.map_or(false, |n| match n.data.borrow().value {
NodeValue::List(nl) => nl.tight,
NodeValue::DescriptionItem(nd) => nd.tight,
_ => false,
})
|| node
.parent()
.map_or(false, |n| node_matches!(n, NodeValue::DescriptionTerm));
if !tight {
if entering {
context.cr()?;
context.write_str("<p")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
if let Some(parent) = node.parent() {
if let NodeValue::FootnoteDefinition(ref nfd) = parent.data.borrow().value {
if node.next_sibling().is_none() {
context.write_str(" ")?;
put_footnote_backref(context, nfd)?;
}
}
}
context.write_str("</p>\n")?;
}
}
Ok(ChildRendering::HTML)
}
fn render_soft_break<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
if context.options.render.hardbreaks {
context.write_str("<br")?;
render_sourcepos(context, node)?;
context.write_str(" />\n")?;
} else {
context.write_str("\n")?;
}
}
Ok(ChildRendering::HTML)
}
fn render_strong<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
let parent_node = node.parent();
if !context.options.render.gfm_quirks
|| (parent_node.is_none()
|| !matches!(parent_node.unwrap().data.borrow().value, NodeValue::Strong))
{
if entering {
context.write_str("<strong")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.write_str("</strong>")?;
}
}
Ok(ChildRendering::HTML)
}
fn render_text<T>(
context: &mut Context<T>,
entering: bool,
literal: &str,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.escape(literal)?;
}
Ok(ChildRendering::HTML)
}
fn render_thematic_break<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.cr()?;
context.write_str("<hr")?;
render_sourcepos(context, node)?;
context.write_str(" />\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_footnote_definition<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
nfd: &NodeFootnoteDefinition,
) -> Result<ChildRendering, fmt::Error> {
if entering {
if context.footnote_ix == 0 {
context.write_str("<section")?;
render_sourcepos(context, node)?;
context.write_str(" class=\"footnotes\" data-footnotes>\n<ol>\n")?;
}
context.footnote_ix += 1;
context.write_str("<li")?;
render_sourcepos(context, node)?;
context.write_str(" id=\"fn-")?;
context.escape_href(&nfd.name)?;
context.write_str("\">")?;
} else {
if put_footnote_backref(context, nfd)? {
context.write_str("\n")?;
}
context.write_str("</li>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_footnote_reference<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
nfr: &NodeFootnoteReference,
) -> Result<ChildRendering, fmt::Error> {
if entering {
let mut ref_id = format!("fnref-{}", nfr.name);
if nfr.ref_num > 1 {
ref_id = format!("{}-{}", ref_id, nfr.ref_num);
}
context.write_str("<sup")?;
render_sourcepos(context, node)?;
context.write_str(" class=\"footnote-ref\"><a href=\"#fn-")?;
context.escape_href(&nfr.name)?;
context.write_str("\" id=\"")?;
context.escape_href(&ref_id)?;
write!(context, "\" data-footnote-ref>{}</a></sup>", nfr.ix)?;
}
Ok(ChildRendering::HTML)
}
fn render_strikethrough<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<del")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.write_str("</del>")?;
}
Ok(ChildRendering::HTML)
}
fn render_table<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.cr()?;
context.write_str("<table")?;
render_sourcepos(context, node)?;
context.write_str(">\n")?;
} else {
if let Some(true) = node
.last_child()
.map(|n| !n.same_node(node.first_child().unwrap()))
{
context.cr()?;
context.write_str("</tbody>\n")?;
}
context.cr()?;
context.write_str("</table>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_table_cell<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
let Some(row_node) = node.parent() else {
panic!("rendered a table cell without a containing table row");
};
let row = &row_node.data.borrow().value;
let in_header = match *row {
NodeValue::TableRow(header) => header,
_ => panic!("rendered a table cell contained by something other than a table row"),
};
let Some(table_node) = row_node.parent() else {
panic!("rendered a table cell without a containing table");
};
let table = &table_node.data.borrow().value;
let alignments = match table {
NodeValue::Table(nt) => &nt.alignments,
_ => {
panic!("rendered a table cell in a table row contained by something other than a table")
}
};
if entering {
context.cr()?;
if in_header {
context.write_str("<th")?;
render_sourcepos(context, node)?;
} else {
context.write_str("<td")?;
render_sourcepos(context, node)?;
}
let mut start = row_node.first_child().unwrap(); let mut i = 0;
while !start.same_node(node) {
i += 1;
start = start.next_sibling().unwrap();
}
match alignments[i] {
TableAlignment::Left => {
context.write_str(" align=\"left\"")?;
}
TableAlignment::Right => {
context.write_str(" align=\"right\"")?;
}
TableAlignment::Center => {
context.write_str(" align=\"center\"")?;
}
TableAlignment::None => (),
}
context.write_str(">")?;
} else if in_header {
context.write_str("</th>")?;
} else {
context.write_str("</td>")?;
}
Ok(ChildRendering::HTML)
}
fn render_table_row<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
thead: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.cr()?;
if thead {
context.write_str("<thead>\n")?;
} else if let Some(n) = node.previous_sibling() {
if let NodeValue::TableRow(true) = n.data.borrow().value {
context.write_str("<tbody>\n")?;
}
}
context.write_str("<tr")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.cr()?;
context.write_str("</tr>")?;
if thead {
context.cr()?;
context.write_str("</thead>")?;
}
}
Ok(ChildRendering::HTML)
}
fn render_task_item<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
symbol: Option<char>,
) -> Result<ChildRendering, fmt::Error> {
let write_li = node
.parent()
.map(|p| node_matches!(p, NodeValue::List(_)))
.unwrap_or_default();
if entering {
context.cr()?;
if write_li {
context.write_str("<li")?;
if context.options.render.tasklist_classes {
context.write_str(" class=\"task-list-item\"")?;
}
render_sourcepos(context, node)?;
context.write_str(">")?;
}
context.write_str("<input type=\"checkbox\"")?;
if !write_li {
render_sourcepos(context, node)?;
}
if context.options.render.tasklist_classes {
context.write_str(" class=\"task-list-item-checkbox\"")?;
}
if symbol.is_some() {
context.write_str(" checked=\"\"")?;
}
context.write_str(" disabled=\"\" /> ")?;
} else if write_li {
context.write_str("</li>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_alert<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
alert: &NodeAlert,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.cr()?;
context.write_str("<div class=\"markdown-alert ")?;
context.write_str(&alert.alert_type.css_class())?;
context.write_str("\"")?;
render_sourcepos(context, node)?;
context.write_str(">\n")?;
context.write_str("<p class=\"markdown-alert-title\">")?;
match alert.title {
Some(ref title) => context.escape(title)?,
None => {
context.write_str(&alert.alert_type.default_title())?;
}
}
context.write_str("</p>\n")?;
} else {
context.cr()?;
context.write_str("</div>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_description_details<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<dd")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.write_str("</dd>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_description_list<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.cr()?;
context.write_str("<dl")?;
render_sourcepos(context, node)?;
context.write_str(">\n")?;
} else {
context.write_str("</dl>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_description_term<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<dt")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.write_str("</dt>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_escaped<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if context.options.render.escaped_char_spans {
if entering {
context.write_str("<span data-escaped-char")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.write_str("</span>")?;
}
}
Ok(ChildRendering::HTML)
}
fn render_escaped_tag<T>(
context: &mut Context<T>,
net: &str,
) -> Result<ChildRendering, fmt::Error> {
context.write_str(net)?;
Ok(ChildRendering::HTML)
}
pub fn render_math<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
nm: &NodeMath,
) -> Result<ChildRendering, fmt::Error> {
if entering {
let mut tag_attributes: Vec<(&str, Cow<str>)> = Vec::new();
let style_attr = if nm.display_math { "display" } else { "inline" };
let tag: &str = if nm.dollar_math { "span" } else { "code" };
tag_attributes.push(("data-math-style", style_attr.into()));
if context.options.render.sourcepos {
let ast = node.data.borrow();
tag_attributes.push(("data-sourcepos", ast.sourcepos.to_string().into()));
}
write_opening_tag(context, tag, tag_attributes.into_iter())?;
context.escape(&nm.literal)?;
write!(context, "</{tag}>")?;
}
Ok(ChildRendering::HTML)
}
pub fn render_math_code_block<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
literal: &str,
) -> Result<ChildRendering, fmt::Error> {
context.cr()?;
let mut pre_attributes: Vec<(&str, Cow<str>)> = Vec::new();
let mut code_attributes: Vec<(&str, Cow<str>)> = Vec::new();
let lang_str = "math";
if context.options.render.github_pre_lang {
pre_attributes.push(("lang", lang_str.into()));
pre_attributes.push(("data-math-style", "display".into()));
} else {
let code_attr = format!("language-{}", lang_str);
code_attributes.push(("class", code_attr.into()));
code_attributes.push(("data-math-style", "display".into()));
}
if context.options.render.sourcepos {
let ast = node.data.borrow();
pre_attributes.push(("data-sourcepos", ast.sourcepos.to_string().into()));
}
write_opening_tag(context, "pre", pre_attributes.into_iter())?;
write_opening_tag(context, "code", code_attributes.into_iter())?;
context.escape(literal)?;
context.write_str("</code></pre>\n")?;
Ok(ChildRendering::HTML)
}
fn render_multiline_block_quote<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.cr()?;
context.write_str("<blockquote")?;
render_sourcepos(context, node)?;
context.write_str(">\n")?;
} else {
context.cr()?;
context.write_str("</blockquote>\n")?;
}
Ok(ChildRendering::HTML)
}
fn render_raw<T>(
context: &mut Context<T>,
entering: bool,
literal: &str,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str(literal)?;
}
Ok(ChildRendering::HTML)
}
#[cfg(feature = "shortcodes")]
fn render_short_code<'a, T>(
context: &mut Context<T>,
entering: bool,
nsc: &NodeShortCode,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str(&nsc.emoji)?;
}
Ok(ChildRendering::HTML)
}
fn render_spoiler_text<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<span")?;
render_sourcepos(context, node)?;
context.write_str(" class=\"spoiler\">")?;
} else {
context.write_str("</span>")?;
}
Ok(ChildRendering::HTML)
}
fn render_subscript<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<sub")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.write_str("</sub>")?;
}
Ok(ChildRendering::HTML)
}
fn render_superscript<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<sup")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.write_str("</sup>")?;
}
Ok(ChildRendering::HTML)
}
fn render_underline<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<u")?;
render_sourcepos(context, node)?;
context.write_str(">")?;
} else {
context.write_str("</u>")?;
}
Ok(ChildRendering::HTML)
}
fn render_wiki_link<'a, T>(
context: &mut Context<T>,
node: Node<'a>,
entering: bool,
nwl: &NodeWikiLink,
) -> Result<ChildRendering, fmt::Error> {
if entering {
context.write_str("<a")?;
render_sourcepos(context, node)?;
context.write_str(" href=\"")?;
let url = &nwl.url;
if context.options.render.unsafe_ || !dangerous_url(url) {
context.escape_href(url)?;
}
context.write_str("\" data-wikilink=\"true")?;
context.write_str("\">")?;
} else {
context.write_str("</a>")?;
}
Ok(ChildRendering::HTML)
}
pub fn collect_text<'a>(node: Node<'a>) -> String {
let mut text = String::with_capacity(20);
collect_text_append(node, &mut text);
text
}
pub fn collect_text_append<'a>(node: Node<'a>, output: &mut String) {
match node.data.borrow().value {
NodeValue::Text(ref literal) => output.push_str(literal),
NodeValue::Code(NodeCode { ref literal, .. }) => output.push_str(literal),
NodeValue::LineBreak | NodeValue::SoftBreak => output.push(' '),
NodeValue::Math(NodeMath { ref literal, .. }) => output.push_str(literal),
_ => {
for n in node.children() {
collect_text_append(n, output);
}
}
}
}
fn put_footnote_backref<T>(
context: &mut Context<T>,
nfd: &NodeFootnoteDefinition,
) -> Result<bool, fmt::Error> {
if context.written_footnote_ix >= context.footnote_ix {
return Ok(false);
}
context.written_footnote_ix = context.footnote_ix;
let mut ref_suffix = String::new();
let mut superscript = String::new();
for ref_num in 1..=nfd.total_references {
if ref_num > 1 {
ref_suffix = format!("-{ref_num}");
superscript = format!("<sup class=\"footnote-ref\">{ref_num}</sup>");
write!(context, " ")?;
}
context.write_str("<a href=\"#fnref-")?;
context.escape_href(&nfd.name)?;
let fnix = context.footnote_ix;
write!(
context,
"{ref_suffix}\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"{fnix}{ref_suffix}\" aria-label=\"Back to reference {fnix}{ref_suffix}\">↩{superscript}</a>",
)?;
}
Ok(true)
}
fn tagfilter(literal: &str) -> bool {
let bytes = literal.as_bytes();
static TAGFILTER_BLACKLIST: [&str; 9] = [
"title",
"textarea",
"style",
"xmp",
"iframe",
"noembed",
"noframes",
"script",
"plaintext",
];
if bytes.len() < 3 || bytes[0] != b'<' {
return false;
}
let mut i = 1;
if bytes[i] == b'/' {
i += 1;
}
let lc = literal[i..].to_lowercase();
for t in TAGFILTER_BLACKLIST.iter() {
if lc.starts_with(t) {
let j = i + t.len();
return isspace(bytes[j])
|| bytes[j] == b'>'
|| (bytes[j] == b'/' && bytes.len() >= j + 2 && bytes[j + 1] == b'>');
}
}
false
}
fn tagfilter_block(output: &mut dyn Write, buffer: &str) -> fmt::Result {
let bytes = buffer.as_bytes();
let matcher = jetscii::bytes!(b'<');
let mut offset = 0;
while let Some(i) = matcher.find(&bytes[offset..]) {
output.write_str(&buffer[offset..offset + i])?;
if tagfilter(&buffer[offset + i..]) {
output.write_str("<")?;
} else {
output.write_str("<")?;
}
offset += i + 1;
}
output.write_str(&buffer[offset..])?;
Ok(())
}
pub fn dangerous_url(input: &str) -> bool {
scanners::dangerous_url(input).is_some()
}
pub fn escape(output: &mut dyn Write, buffer: &str) -> fmt::Result {
let bytes = buffer.as_bytes();
let matcher = jetscii::bytes!(b'"', b'&', b'<', b'>');
let mut offset = 0;
while let Some(i) = matcher.find(&bytes[offset..]) {
let esc: &str = match bytes[offset + i] {
b'"' => """,
b'&' => "&",
b'<' => "<",
b'>' => ">",
_ => unreachable!(),
};
output.write_str(&buffer[offset..offset + i])?;
output.write_str(esc)?;
offset += i + 1;
}
output.write_str(&buffer[offset..])?;
Ok(())
}
pub fn escape_href(output: &mut dyn Write, buffer: &str, relaxed_ipv6: bool) -> fmt::Result {
const HREF_SAFE: [bool; 256] = character_set!(
b"-_.+!*(),%#@?=;:/,+$~",
b"abcdefghijklmnopqrstuvwxyz",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
);
let bytes = buffer.as_bytes();
let size = buffer.len();
let mut i = 0;
let possible_ipv6_url_end = if relaxed_ipv6 {
scanners::ipv6_relaxed_url_start(buffer)
} else {
scanners::ipv6_url_start(buffer)
};
if let Some(ipv6_url_end) = possible_ipv6_url_end {
output.write_str(&buffer[0..ipv6_url_end])?;
i = ipv6_url_end;
}
while i < size {
let org = i;
while i < size && HREF_SAFE[bytes[i] as usize] {
i += 1;
}
if i > org {
output.write_str(&buffer[org..i])?;
}
if i >= size {
break;
}
match bytes[i] {
b'&' => {
output.write_str("&")?;
}
b'\'' => {
output.write_str("'")?;
}
_ => write!(output, "%{:02X}", bytes[i])?,
}
i += 1;
}
Ok(())
}
pub fn write_opening_tag<K: AsRef<str>, V: AsRef<str>>(
output: &mut dyn Write,
tag: &str,
attributes: impl IntoIterator<Item = (K, V)>,
) -> fmt::Result {
write!(output, "<{tag}")?;
for (attr, val) in attributes {
write!(output, " {}=\"", attr.as_ref())?;
escape(output, val.as_ref())?;
output.write_str("\"")?;
}
output.write_str(">")?;
Ok(())
}