use super::format::{OutputFormat, SemanticAttribute};
use citum_schema::template::WrapPunctuation;
use std::fmt::Write;
#[derive(Default, Clone)]
pub struct Html;
impl Html {
fn sanitize_href(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for ch in value.chars() {
if ch.is_ascii_control()
|| ch.is_whitespace()
|| matches!(ch, '"' | '\'' | '<' | '>' | '&')
{
let mut buf = [0u8; 4];
for byte in ch.encode_utf8(&mut buf).as_bytes() {
escaped.push('%');
let _ = write!(escaped, "{byte:02X}");
}
} else {
escaped.push(ch);
}
}
escaped
}
fn escape_attribute_value(value: &str) -> String {
value
.replace('&', "&")
.replace('"', """)
.replace('<', "<")
.replace('>', ">")
}
}
impl OutputFormat for Html {
type Output = String;
fn text(&self, s: &str) -> Self::Output {
s.to_string()
}
fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output {
items.join(delimiter)
}
fn finish(&self, output: Self::Output) -> String {
output
}
fn emph(&self, content: Self::Output) -> Self::Output {
if content.is_empty() {
return content;
}
format!("<em>{content}</em>")
}
fn strong(&self, content: Self::Output) -> Self::Output {
if content.is_empty() {
return content;
}
format!("<b>{content}</b>")
}
fn small_caps(&self, content: Self::Output) -> Self::Output {
if content.is_empty() {
return content;
}
format!(r#"<span style="font-variant:small-caps">{content}</span>"#)
}
fn superscript(&self, content: Self::Output) -> Self::Output {
if content.is_empty() {
return content;
}
format!("<sup>{content}</sup>")
}
fn quote(&self, content: Self::Output) -> Self::Output {
if content.is_empty() {
return content;
}
format!("\u{201C}{content}\u{201D}")
}
fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
format!("{prefix}{content}{suffix}")
}
fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
format!("{prefix}{content}{suffix}")
}
fn wrap_punctuation(&self, wrap: &WrapPunctuation, content: Self::Output) -> Self::Output {
match wrap {
WrapPunctuation::Parentheses => format!("({content})"),
WrapPunctuation::Brackets => format!("[{content}]"),
WrapPunctuation::Quotes => format!("\u{201C}{content}\u{201D}"),
}
}
fn semantic(&self, class: &str, content: Self::Output) -> Self::Output {
if content.is_empty() {
return content;
}
format!(r#"<span class="{class}">{content}</span>"#)
}
fn annotation(&self, content: Self::Output) -> Self::Output {
if content.is_empty() {
return content;
}
format!("<div class=\"citum-annotation\">{content}</div>")
}
fn semantic_with_attributes(
&self,
class: &str,
content: Self::Output,
attributes: &[SemanticAttribute],
) -> Self::Output {
if content.is_empty() {
return content;
}
let mut extra_attrs = String::new();
for attribute in attributes {
let _ = write!(
&mut extra_attrs,
r#" {}="{}""#,
attribute.name,
Self::escape_attribute_value(&attribute.value)
);
}
format!(r#"<span class="{class}"{extra_attrs}>{content}</span>"#)
}
fn citation(&self, ids: Vec<String>, content: Self::Output) -> Self::Output {
if content.is_empty() {
return content;
}
let ids_str = ids.join(" ");
format!(r#"<span class="citum-citation" data-ref="{ids_str}">{content}</span>"#)
}
fn link(&self, url: &str, content: Self::Output) -> Self::Output {
if content.is_empty() {
return content;
}
format!(r#"<a href="{}">{}</a>"#, Self::sanitize_href(url), content)
}
fn format_id(&self, id: &str) -> String {
format!("ref-{id}")
}
fn bibliography(&self, entries: Vec<Self::Output>) -> Self::Output {
format!(
r#"<div class="citum-bibliography">
{}
</div>"#,
self.join(entries, "\n")
)
}
fn entry(
&self,
id: &str,
content: Self::Output,
url: Option<&str>,
metadata: &super::format::ProcEntryMetadata,
) -> Self::Output {
let content = if let Some(u) = url {
self.link(u, content)
} else {
content
};
let mut attrs = format!(
r#"id="{}""#,
Self::escape_attribute_value(&self.format_id(id))
);
if let Some(author) = &metadata.author {
attrs.push_str(r#" data-author=""#);
attrs.push_str(&Self::escape_attribute_value(author));
attrs.push('"');
}
if let Some(year) = &metadata.year {
attrs.push_str(r#" data-year=""#);
attrs.push_str(&Self::escape_attribute_value(year));
attrs.push('"');
}
if let Some(title) = &metadata.title {
attrs.push_str(r#" data-title=""#);
attrs.push_str(&Self::escape_attribute_value(title));
attrs.push('"');
}
format!(r#"<div class="citum-entry" {attrs}>{content}</div>"#)
}
}