use alloc::format;
use alloc::string::String;
use crate::ast::{DirectiveAttribute, Inline, MathInlineKind};
use super::escape::{attr_escape, encode_href, escape_text, filter_img_protocol, filter_protocol};
use super::footnotes;
use super::refs::{escaped_alt, flatten_alt};
use super::{Ctx, SafeRawHtmlForm};
pub fn render_inlines(children: &[Inline], ctx: &Ctx) -> String {
let mut out = String::new();
for child in children {
out.push_str(&render_inline(child, ctx));
}
out
}
pub fn render_inline(inline: &Inline, ctx: &Ctx) -> String {
match inline {
Inline::Text(t) => escape_text(&t.value),
Inline::Escape(e) => {
let mut buf = [0u8; 4];
escape_text(e.value.encode_utf8(&mut buf))
}
Inline::CharacterReference(c) => escape_text(&c.value().unwrap_or_default()),
Inline::Emphasis(n) => format!("<em>{}</em>", render_inlines(&n.children, ctx)),
Inline::Strong(n) => format!("<strong>{}</strong>", render_inlines(&n.children, ctx)),
Inline::Delete(n) => format!("<del>{}</del>", render_inlines(&n.children, ctx)),
Inline::Mark(n) => format!("<mark>{}</mark>", render_inlines(&n.children, ctx)),
Inline::Shortcode(s) => match s.glyph() {
Some(glyph) => escape_text(glyph),
None => escape_text(&format!(":{}:", s.name)),
},
Inline::Code(c) => format!("<code>{}</code>", escape_text(&c.value)),
Inline::Link(n) => {
let href = encode_href(&filter_protocol(
&n.destination,
ctx.allow_dangerous_protocol,
ctx.gfm_url_denylist(),
));
let title = title_attr(n.title.as_ref().map(|title| title.value.as_str()));
format!(
"<a href=\"{href}\"{title}>{}</a>",
render_inlines(&n.children, ctx)
)
}
Inline::Autolink(n) => {
let href = encode_href(&filter_protocol(
&n.destination().unwrap_or_default(),
ctx.allow_dangerous_protocol,
ctx.gfm_url_denylist(),
));
format!("<a href=\"{href}\">{}</a>", escape_text(&n.text))
}
Inline::Image(n) => {
let src = encode_href(&filter_img_protocol(
&n.destination,
ctx.allow_dangerous_protocol,
ctx.allow_any_img_src,
));
let alt = escaped_alt(&n.alt);
let title = title_attr(n.title.as_ref().map(|title| title.value.as_str()));
format!("<img src=\"{src}\" alt=\"{alt}\"{title} />")
}
Inline::LinkReference(n) => match ctx.defs.resolve(&n.identifier) {
Some(def) => {
let href = encode_href(&filter_protocol(
&def.destination,
ctx.allow_dangerous_protocol,
ctx.gfm_url_denylist(),
));
let title = title_attr(def.title.as_ref().map(|title| title.value.as_str()));
format!(
"<a href=\"{href}\"{title}>{}</a>",
render_inlines(&n.children, ctx)
)
}
None => link_reference_fallback(n, ctx),
},
Inline::ImageReference(n) => match ctx.defs.resolve(&n.identifier) {
Some(def) => {
let src = encode_href(&filter_img_protocol(
&def.destination,
ctx.allow_dangerous_protocol,
ctx.allow_any_img_src,
));
let alt = escaped_alt(&n.alt);
let title = title_attr(def.title.as_ref().map(|title| title.value.as_str()));
format!("<img src=\"{src}\" alt=\"{alt}\"{title} />")
}
None => image_reference_fallback(n),
},
Inline::Html(h) => render_raw_html(&h.value, ctx),
Inline::SoftBreak(_) => String::from("\n"),
Inline::LineBreak(_) => String::from("<br />\n"),
Inline::Math(m) => match m.kind {
MathInlineKind::Code => format!(
"<code data-math-style=\"inline\">{}</code>",
escape_text(&m.value)
),
MathInlineKind::Dollar { dollars } if dollars >= 2 => format!(
"<span data-math-style=\"display\">{}</span>",
escape_text(&m.value)
),
MathInlineKind::Dollar { .. } => format!(
"<span data-math-style=\"inline\">{}</span>",
escape_text(&m.value)
),
},
Inline::FootnoteReference(fr) => {
if ctx.footnotes.is_defined(&fr.identifier) {
footnote_marker(&fr.identifier, ctx)
} else {
format!("[^{}]", escape_text(&fr.label))
}
}
Inline::InlineFootnote(_) => {
let id = footnotes::next_inline_id(ctx.footnotes);
footnote_marker(&id, ctx)
}
Inline::WikiLink(w) => {
let target = filter_protocol(&w.decoded_target(), ctx.allow_dangerous_protocol, true);
let href = encode_href(&target);
let embed = if w.embed {
" data-wikilink-embed=\"true\""
} else {
""
};
format!(
"<a href=\"{href}\" data-wikilink=\"true\"{embed}>{}</a>",
escape_text(&w.decoded_label())
)
}
Inline::TextDirective(d) => {
let attrs = directive_attrs(&d.attributes);
format!(
"<span class=\"directive directive-text\" data-directive-name=\"{}\"{attrs}>{}</span>",
attr_escape(&d.name),
render_inlines(&d.label, ctx)
)
}
}
}
fn title_attr(title: Option<&str>) -> String {
match title {
Some(t) if !t.is_empty() => format!(" title=\"{}\"", attr_escape(t)),
_ => String::new(),
}
}
fn footnote_marker(id: &str, ctx: &Ctx) -> String {
let (number, fnref) = footnotes::reference_marker(ctx.footnotes, id);
let enc = footnotes::reference_fn_target(ctx.footnotes, id);
format!(
"<sup class=\"footnote-ref\"><a href=\"#fn-{enc}\" id=\"{fnref}\" data-footnote-ref>{number}</a></sup>",
)
}
pub(super) fn render_raw_html(value: &str, ctx: &Ctx) -> String {
if ctx.allow_dangerous_html {
if ctx.gfm_tagfilter {
return apply_tagfilter(value);
}
return String::from(value);
}
safe_raw_html(value, ctx)
}
pub(super) fn directive_attrs(attributes: &[DirectiveAttribute]) -> String {
let mut out = String::new();
for attr in attributes {
let value = attr.value.as_deref().unwrap_or("");
out.push_str(&format!(
" data-{}=\"{}\"",
attr_escape(&attr.name),
attr_escape(value)
));
}
out
}
pub(super) fn safe_raw_html(value: &str, ctx: &Ctx) -> String {
match ctx.safe_raw_html_form {
SafeRawHtmlForm::OmitPlaceholder => String::from(RAW_HTML_OMITTED),
SafeRawHtmlForm::EscapeText => escape_text(value),
}
}
pub(super) const RAW_HTML_OMITTED: &str = "<!-- raw HTML omitted -->";
pub fn apply_tagfilter(value: &str) -> String {
const BLOCKED: [&str; 9] = [
"title",
"textarea",
"style",
"xmp",
"iframe",
"noembed",
"noframes",
"script",
"plaintext",
];
let bytes = value.as_bytes();
let mut out = String::with_capacity(value.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'<' {
let after = &value[i + 1..];
let tag_body = after.strip_prefix('/').unwrap_or(after);
let matched = BLOCKED.iter().find(|tag| {
tag_body.len() >= tag.len()
&& tag_body[..tag.len()].eq_ignore_ascii_case(tag)
&& tag_terminates(&tag_body[tag.len()..])
});
if matched.is_some() {
out.push_str("<");
i += 1;
continue;
}
}
let ch = value[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
out
}
fn tag_terminates(rest: &str) -> bool {
match rest.chars().next() {
None => true,
Some(c) => c == '>' || c == '/' || c.is_whitespace(),
}
}
fn link_reference_fallback(n: &crate::ast::LinkReference, ctx: &Ctx) -> String {
use crate::ast::ReferenceKind;
let inner = render_inlines(&n.children, ctx);
match n.kind {
ReferenceKind::Shortcut => format!("[{inner}]"),
ReferenceKind::Collapsed => format!("[{inner}][]"),
ReferenceKind::Full => format!("[{inner}][{}]", escape_text(&n.label)),
}
}
fn image_reference_fallback(n: &crate::ast::ImageReference) -> String {
use crate::ast::ReferenceKind;
let inner = escape_text(&flatten_alt(&n.alt));
match n.kind {
ReferenceKind::Shortcut => format!("![{inner}]"),
ReferenceKind::Collapsed => format!("![{inner}][]"),
ReferenceKind::Full => format!("![{inner}][{}]", escape_text(&n.label)),
}
}