use std::{fmt::Debug, sync::LazyLock};
use regex::Regex;
use crate::{Parser, attributes::Attrlist, parser::ResolvedReference};
pub trait InlineSubstitutionRenderer: Debug {
fn render_special_character(&self, type_: SpecialCharacter, dest: &mut String);
fn render_quoted_substitition(
&self,
type_: QuoteType,
scope: QuoteScope,
attrlist: Option<Attrlist<'_>>,
id: Option<String>,
body: &str,
dest: &mut String,
);
fn render_character_replacement(&self, type_: CharacterReplacementType, dest: &mut String);
fn render_line_break(&self, dest: &mut String);
fn render_image(&self, params: &ImageRenderParams, dest: &mut String);
fn image_uri(
&self,
target_image_path: &str,
parser: &Parser,
asset_dir_key: Option<&str>,
) -> String;
fn render_icon(&self, params: &IconRenderParams, dest: &mut String);
fn icon_uri(&self, name: &str, _attrlist: &Attrlist, parser: &Parser) -> String {
let icontype = parser
.attribute_value("icontype")
.as_maybe_str()
.unwrap_or("png")
.to_owned();
if false {
todo!(
"Enable this when doing block-related icon attributes: {}",
r#"
let icon = if let Some(icon) = attrlist.named_attribute("icon") {
let icon_str = icon.value();
if has_extname(icon_str) {
icon_str.to_string()
} else {
format!("{icon_str}.{icontype}")
}
} else {
// This part is defaulted for now.
format!("{name}.{icontype}")
};
"#
);
}
let icon = format!("{name}.{icontype}");
self.image_uri(&icon, parser, Some("iconsdir"))
}
fn render_link(&self, params: &LinkRenderParams, dest: &mut String);
fn render_anchor(&self, id: &str, reftext: Option<String>, dest: &mut String);
fn render_xref(&self, params: &XrefRenderParams, dest: &mut String);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SpecialCharacter {
Lt,
Gt,
Ampersand,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QuoteType {
Strong,
DoubleQuote,
SingleQuote,
Monospaced,
Emphasis,
Mark,
Superscript,
Subscript,
Unquoted,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QuoteScope {
Constrained,
Unconstrained,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CharacterReplacementType {
Copyright,
Registered,
Trademark,
EmDashSurroundedBySpaces,
EmDashWithoutSpace,
Ellipsis,
SingleRightArrow,
DoubleRightArrow,
SingleLeftArrow,
DoubleLeftArrow,
TypographicApostrophe,
CharacterReference(String),
}
#[derive(Clone, Debug)]
pub struct ImageRenderParams<'a> {
pub target: &'a str,
pub alt: String,
pub width: Option<&'a str>,
pub height: Option<&'a str>,
pub attrlist: &'a Attrlist<'a>,
pub parser: &'a Parser,
}
#[derive(Clone, Debug)]
pub struct IconRenderParams<'a> {
pub target: &'a str,
pub alt: String,
pub size: Option<&'a str>,
pub attrlist: &'a Attrlist<'a>,
pub parser: &'a Parser,
}
#[derive(Clone, Debug)]
pub struct LinkRenderParams<'a> {
pub target: String,
pub link_text: String,
pub extra_roles: Vec<&'a str>,
pub window: Option<&'static str>,
pub type_: LinkRenderType,
pub attrlist: &'a Attrlist<'a>,
pub parser: &'a Parser,
}
#[derive(Clone, Debug)]
pub enum LinkRenderType {
Link,
}
#[derive(Clone, Debug)]
pub struct XrefRenderParams<'a> {
pub target: &'a str,
pub provided_text: Option<&'a str>,
pub resolved: Option<&'a ResolvedReference>,
}
#[derive(Debug)]
pub struct HtmlSubstitutionRenderer {}
impl InlineSubstitutionRenderer for HtmlSubstitutionRenderer {
fn render_special_character(&self, type_: SpecialCharacter, dest: &mut String) {
match type_ {
SpecialCharacter::Lt => {
dest.push_str("<");
}
SpecialCharacter::Gt => {
dest.push_str(">");
}
SpecialCharacter::Ampersand => {
dest.push_str("&");
}
}
}
fn render_quoted_substitition(
&self,
type_: QuoteType,
_scope: QuoteScope,
attrlist: Option<Attrlist<'_>>,
mut id: Option<String>,
body: &str,
dest: &mut String,
) {
let mut roles: Vec<&str> = attrlist.as_ref().map(|a| a.roles()).unwrap_or_default();
if let Some(block_style) = attrlist
.as_ref()
.and_then(|a| a.nth_attribute(1))
.and_then(|attr1| attr1.block_style())
{
roles.insert(0, block_style);
}
if id.is_none() {
id = attrlist
.as_ref()
.and_then(|a| a.nth_attribute(1))
.and_then(|attr1| attr1.id())
.map(|id| id.to_owned())
}
match type_ {
QuoteType::Strong => {
wrap_body_in_html_tag(attrlist.as_ref(), "strong", id, roles, body, dest);
}
QuoteType::DoubleQuote => {
dest.push_str("“");
dest.push_str(body);
dest.push_str("”");
}
QuoteType::SingleQuote => {
dest.push_str("‘");
dest.push_str(body);
dest.push_str("’");
}
QuoteType::Monospaced => {
wrap_body_in_html_tag(attrlist.as_ref(), "code", id, roles, body, dest);
}
QuoteType::Emphasis => {
wrap_body_in_html_tag(attrlist.as_ref(), "em", id, roles, body, dest);
}
QuoteType::Mark => {
if roles.is_empty() && id.is_none() {
wrap_body_in_html_tag(attrlist.as_ref(), "mark", id, roles, body, dest);
} else {
wrap_body_in_html_tag(attrlist.as_ref(), "span", id, roles, body, dest);
}
}
QuoteType::Superscript => {
wrap_body_in_html_tag(attrlist.as_ref(), "sup", id, roles, body, dest);
}
QuoteType::Subscript => {
wrap_body_in_html_tag(attrlist.as_ref(), "sub", id, roles, body, dest);
}
QuoteType::Unquoted => {
if roles.is_empty() && id.is_none() {
dest.push_str(body);
} else {
wrap_body_in_html_tag(attrlist.as_ref(), "span", id, roles, body, dest);
}
}
}
}
fn render_character_replacement(&self, type_: CharacterReplacementType, dest: &mut String) {
match type_ {
CharacterReplacementType::Copyright => {
dest.push_str("©");
}
CharacterReplacementType::Registered => {
dest.push_str("®");
}
CharacterReplacementType::Trademark => {
dest.push_str("™");
}
CharacterReplacementType::EmDashSurroundedBySpaces => {
dest.push_str(" — ");
}
CharacterReplacementType::EmDashWithoutSpace => {
dest.push_str("—​");
}
CharacterReplacementType::Ellipsis => {
dest.push_str("…​");
}
CharacterReplacementType::SingleLeftArrow => {
dest.push_str("←");
}
CharacterReplacementType::DoubleLeftArrow => {
dest.push_str("⇐");
}
CharacterReplacementType::SingleRightArrow => {
dest.push_str("→");
}
CharacterReplacementType::DoubleRightArrow => {
dest.push_str("⇒");
}
CharacterReplacementType::TypographicApostrophe => {
dest.push_str("’");
}
CharacterReplacementType::CharacterReference(name) => {
dest.push('&');
dest.push_str(&name);
dest.push(';');
}
}
}
fn render_line_break(&self, dest: &mut String) {
dest.push_str("<br>");
}
fn render_image(&self, params: &ImageRenderParams, dest: &mut String) {
let src = self.image_uri(params.target, params.parser, None);
let mut attrs: Vec<String> = vec![
format!(r#"src="{src}""#),
format!(
r#"alt="{alt}""#,
alt = encode_attribute_value(params.alt.to_string())
),
];
if let Some(width) = params.width {
attrs.push(format!(r#"width="{width}""#));
}
if let Some(height) = params.height {
attrs.push(format!(r#"height="{height}""#));
}
if let Some(title) = params.attrlist.named_attribute("title") {
attrs.push(format!(
r#"title="{title}""#,
title = encode_attribute_value(title.value().to_owned())
));
}
let format = params
.attrlist
.named_attribute("format")
.map(|format| format.value());
let img = if format == Some("svg") || params.target.contains(".svg") {
if params.attrlist.has_option("inline") {
todo!(
"Port this: {}",
r#"img = (read_svg_contents node, target) || %(<span class="alt">#{node.alt}</span>)
NOTE: The attrs list calculated above may not be usable.
"#
);
} else if params.attrlist.has_option("interactive") {
todo!(
"Port this: {}",
r##"
fallback = (node.attr? 'fallback') ? %(<img src="#{node.image_uri node.attr 'fallback'}" alt="#{encode_attribute_value node.alt}"#{attrs}#{@void_element_slash}>) : %(<span class="alt">#{node.alt}</span>)
img = %(<object type="image/svg+xml" data="#{src = node.image_uri target}"#{attrs}>#{fallback}</object>)
NOTE: The attrs list calculated above may not be usable.
"##
);
} else {
format!(
r#"<img {attrs}{void_element_slash}>"#,
attrs = attrs.join(" "),
void_element_slash = "",
)
}
} else {
format!(
r#"<img {attrs}{void_element_slash}>"#,
attrs = attrs.join(" "),
void_element_slash = "",
)
};
render_icon_or_image(params.attrlist, &img, &src, "image", dest);
}
fn image_uri(
&self,
target_image_path: &str,
parser: &Parser,
asset_dir_key: Option<&str>,
) -> String {
let asset_dir_key = asset_dir_key.unwrap_or("imagesdir");
if false {
todo!(
"Port this when implementing safe modes: {}",
r#"
if (doc = @document).safe < SafeMode::SECURE && (doc.attr? 'data-uri')
if ((Helpers.uriish? target_image) && (target_image = Helpers.encode_spaces_in_uri target_image)) ||
(asset_dir_key && (images_base = doc.attr asset_dir_key) && (Helpers.uriish? images_base) &&
(target_image = normalize_web_path target_image, images_base, false))
(doc.attr? 'allow-uri-read') ? (generate_data_uri_from_uri target_image, (doc.attr? 'cache-uri')) : target_image
else
generate_data_uri target_image, asset_dir_key
end
else
normalize_web_path target_image, (asset_dir_key ? (doc.attr asset_dir_key) : nil)
end
"#
);
} else {
let asset_dir = parser
.attribute_value(asset_dir_key)
.as_maybe_str()
.map(|s| s.to_string());
normalize_web_path(target_image_path, parser, asset_dir.as_deref(), true)
}
}
fn render_icon(&self, params: &IconRenderParams, dest: &mut String) {
let src = self.icon_uri(params.target, params.attrlist, params.parser);
let img = if params.parser.has_attribute("icons") {
let icons = params.parser.attribute_value("icons");
if let Some(icons) = icons.as_maybe_str()
&& icons == "font"
{
let mut i_class_attrs: Vec<String> = vec![
"fa".to_owned(),
format!("fa-{target}", target = params.target),
];
if let Some(size) = params.attrlist.named_or_positional_attribute("size", 1) {
i_class_attrs.push(format!("fa-{size}", size = size.value()));
}
if let Some(flip) = params.attrlist.named_attribute("flip") {
i_class_attrs.push(format!("fa-flip-{flip}", flip = flip.value()));
} else if let Some(rotate) = params.attrlist.named_attribute("rotate") {
i_class_attrs.push(format!("fa-rotate-{rotate}", rotate = rotate.value()));
}
format!(
r##"<i class="{i_class_attr_val}"{title_attr}></i>"##,
i_class_attr_val = i_class_attrs.join(" "),
title_attr = if let Some(title) = params.attrlist.named_attribute("title") {
format!(r#" title="{title}""#, title = title.value())
} else {
"".to_owned()
}
)
} else {
let mut attrs: Vec<String> = vec![
format!(r#"src="{src}""#),
format!(
r#"alt="{alt}""#,
alt = encode_attribute_value(params.alt.to_string())
),
];
if let Some(width) = params.attrlist.named_attribute("width") {
attrs.push(format!(r#"width="{width}""#, width = width.value()));
}
if let Some(height) = params.attrlist.named_attribute("height") {
attrs.push(format!(r#"height="{height}""#, height = height.value()));
}
if let Some(title) = params.attrlist.named_attribute("title") {
attrs.push(format!(r#"title="{title}""#, title = title.value()));
}
format!(
"<img {attrs}{void_element_slash}>",
attrs = attrs.join(" "),
void_element_slash = "",
)
}
} else {
format!("[{alt}]", alt = params.alt)
};
render_icon_or_image(params.attrlist, &img, &src, "icon", dest);
}
fn render_link(&self, params: &LinkRenderParams, dest: &mut String) {
let id = params.attrlist.id();
let mut roles = params.extra_roles.clone();
let mut attrlist_roles = params.attrlist.roles().clone();
roles.append(&mut attrlist_roles);
let link = format!(
r##"<a href="{target}"{id}{class}{link_constraint_attrs}>{link_text}</a>"##,
target = params.target,
id = if let Some(id) = id {
format!(r#" id="{id}""#)
} else {
"".to_owned()
},
class = if roles.is_empty() {
"".to_owned()
} else {
format!(r#" class="{roles}""#, roles = roles.join(" "))
},
link_constraint_attrs = link_constraint_attrs(params.attrlist, params.window),
link_text = params.link_text,
);
dest.push_str(&link);
}
fn render_anchor(&self, id: &str, _reftext: Option<String>, dest: &mut String) {
dest.push_str(&format!("<a id=\"{id}\"></a>"));
}
fn render_xref(&self, params: &XrefRenderParams, dest: &mut String) {
match params.resolved {
Some(resolved) => {
let text = params
.provided_text
.map(str::to_string)
.or_else(|| resolved.text.clone())
.unwrap_or_else(|| format!("[{target}]", target = params.target));
dest.push_str(&format!(
r#"<a href="{href}">{text}</a>"#,
href = resolved.href
));
}
None => {
let text = params
.provided_text
.map(str::to_string)
.unwrap_or_else(|| format!("[{target}]", target = params.target));
dest.push_str(&format!(
r##"<a href="#{target}">{text}</a>"##,
target = params.target
));
}
}
}
}
fn wrap_body_in_html_tag(
_attrlist: Option<&Attrlist<'_>>,
tag: &'static str,
id: Option<String>,
roles: Vec<&str>,
body: &str,
dest: &mut String,
) {
dest.push('<');
dest.push_str(tag);
if let Some(id) = id.as_ref() {
dest.push_str(" id=\"");
dest.push_str(id);
dest.push('"');
}
if !roles.is_empty() {
let roles = roles.join(" ");
dest.push_str(" class=\"");
dest.push_str(&roles);
dest.push('"');
}
dest.push('>');
dest.push_str(body);
dest.push_str("</");
dest.push_str(tag);
dest.push('>');
}
fn render_icon_or_image(
attrlist: &Attrlist,
img: &str,
src: &str,
type_: &'static str,
dest: &mut String,
) {
let mut img = img.to_string();
if let Some(link) = attrlist.named_attribute("link") {
let mut link = link.value();
if link == "self" {
link = src;
}
img = format!(
r#"<a class="image" href="{link}"{link_constraint_attrs}>{img}</a>"#,
link_constraint_attrs = link_constraint_attrs(attrlist, None)
);
}
let mut roles: Vec<&str> = attrlist.roles();
if let Some(float) = attrlist.named_attribute("float") {
roles.insert(0, float.value());
}
roles.insert(0, type_);
dest.push_str(r#"<span class=""#);
dest.push_str(&roles.join(" "));
dest.push_str(r#"">"#);
dest.push_str(&img);
dest.push_str("</span>");
}
fn encode_attribute_value(value: String) -> String {
value.replace('"', """)
}
fn normalize_web_path(
target: &str,
parser: &Parser,
start: Option<&str>,
preserve_uri_target: bool,
) -> String {
if preserve_uri_target && is_uri_ish(target) {
encode_spaces_in_uri(target)
} else {
parser.path_resolver.web_path(target, start)
}
}
fn is_uri_ish(path: &str) -> bool {
path.contains(':') && URI_SNIFF.is_match(path)
}
fn encode_spaces_in_uri(s: &str) -> String {
s.replace(' ', "%20")
}
static URI_SNIFF: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::unwrap_used)]
Regex::new(
r#"(?x)
\A # Anchor to start of string
\p{Alphabetic} # First character must be a letter
[\p{Alphabetic}\p{Nd}.+-]+ # Followed by one or more alphanum or . + -
: # Literal colon
/{0,2} # Zero to two slashes
"#,
)
.unwrap()
});
fn link_constraint_attrs(attrlist: &Attrlist<'_>, window: Option<&'static str>) -> String {
let rel = if attrlist.has_option("nofollow") {
Some("nofollow")
} else {
None
};
if let Some(window) = attrlist
.named_attribute("window")
.map(|a| a.value())
.or(window)
{
let rel_noopener = if window == "_blank" || attrlist.has_option("noopener") {
if let Some(rel) = rel {
format!(r#" rel="{rel}" noopener"#)
} else {
r#" rel="noopener""#.to_owned()
}
} else {
"".to_string()
};
format!(r#" target="{window}"{rel_noopener}"#)
} else if let Some(rel) = rel {
format!(r#" rel="{rel}""#)
} else {
"".to_string()
}
}