use alloc::format;
use alloc::string::String;
use crate::ast::{AutolinkKind, DirectiveAttribute, Inline, MathInlineKind};
use super::escape::{
attr_escape, attr_escape_gfm, encode_href, escape_text, filter_img_protocol, filter_protocol,
};
use super::footnotes;
use super::refs::{escaped_alt, flatten_alt, visible_text};
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),
Inline::Emphasis(n) => format!("<em>{}</em>", render_inlines(&n.children, ctx)),
Inline::Strong(n) => format!("<strong>{}</strong>", render_inlines(&n.children, ctx)),
Inline::Underline(n) => format!("<u>{}</u>", render_inlines(&n.children, ctx)),
Inline::Delete(n) => format!("<del>{}</del>", render_inlines(&n.children, ctx)),
Inline::Insert(n) => format!("<ins>{}</ins>", render_inlines(&n.children, ctx)),
Inline::Mark(n) => format!("<mark>{}</mark>", render_inlines(&n.children, ctx)),
Inline::Subscript(n) => format!("<sub>{}</sub>", render_inlines(&n.children, ctx)),
Inline::Superscript(n) => format!("<sup>{}</sup>", render_inlines(&n.children, ctx)),
Inline::Spoiler(n) => {
format!(
"<span class=\"spoiler\">{}</span>",
render_inlines(&n.children, ctx)
)
}
Inline::Shortcode(s) => escape_text(&emoji_glyph(&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_deref());
format!(
"<a href=\"{href}\"{title}>{}</a>",
render_inlines(&n.children, ctx)
)
}
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_deref());
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_deref());
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_deref());
format!("<img src=\"{src}\" alt=\"{alt}\"{title} />")
}
None => image_reference_fallback(n),
},
Inline::Autolink(a) => match &a.kind {
AutolinkKind::Angle => {
let dest = autolink_href_dest(&a.destination);
let href = encode_href(&filter_protocol(
&dest,
ctx.allow_dangerous_protocol,
ctx.gfm_url_denylist(),
));
let text = escape_text(&visible_text(&a.destination));
format!("<a href=\"{href}\">{text}</a>")
}
AutolinkKind::GfmLiteral { original } => {
let href = encode_href(&filter_protocol(
&a.destination,
ctx.allow_dangerous_protocol,
ctx.gfm_url_denylist(),
));
let text = escape_text(original);
format!("<a href=\"{href}\">{text}</a>")
}
},
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 href = attr_escape_gfm(&encode_href(&w.target));
format!(
"<a href=\"{href}\" data-wikilink=\"true\">{}</a>",
escape_text(&w.label)
)
}
Inline::MdxExpression(_) => String::new(),
Inline::MdxJsx(_) => String::new(),
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 autolink_href_dest(dest: &str) -> String {
if dest.contains('@') && !has_uri_scheme(dest) {
return format!("mailto:{dest}");
}
String::from(dest)
}
fn has_uri_scheme(dest: &str) -> bool {
let mut chars = dest.char_indices();
match chars.next() {
Some((_, c)) if c.is_ascii_alphabetic() => {}
_ => return false,
}
for (_, c) in chars {
if c == ':' {
return true;
}
if !(c.is_ascii_alphanumeric() || c == '+' || c == '.' || c == '-') {
return false;
}
}
false
}
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)),
}
}
fn emoji_glyph(name: &str) -> String {
let glyph = match name {
"smile" => "\u{1F604}",
"+1" | "thumbsup" => "\u{1F44D}",
"-1" | "thumbsdown" => "\u{1F44E}",
"clock12" => "\u{1F55B}",
"heart" => "\u{2764}\u{FE0F}",
"tada" => "\u{1F389}",
"rocket" => "\u{1F680}",
"100" => "\u{1F4AF}",
"x" => "\u{274C}",
"1234" => "\u{1F522}",
"1st_place_medal" => "\u{1F947}",
"e-mail" => "\u{1F4E7}",
"non-potable_water" => "\u{1F6B1}",
_ => return format!(":{name}:"),
};
String::from(glyph)
}