citum_engine/render/
html.rs1use super::format::{OutputFormat, SemanticAttribute};
9use citum_schema::template::WrapPunctuation;
10use std::fmt::Write;
11
12#[derive(Default, Clone)]
13pub struct Html;
15
16impl Html {
17 fn sanitize_href(value: &str) -> String {
18 let mut escaped = String::with_capacity(value.len());
19 for ch in value.chars() {
20 if ch.is_ascii_control()
21 || ch.is_whitespace()
22 || matches!(ch, '"' | '\'' | '<' | '>' | '&')
23 {
24 let mut buf = [0u8; 4];
25 for byte in ch.encode_utf8(&mut buf).as_bytes() {
26 escaped.push('%');
27 let _ = write!(escaped, "{byte:02X}");
28 }
29 } else {
30 escaped.push(ch);
31 }
32 }
33 escaped
34 }
35
36 fn escape_attribute_value(value: &str) -> String {
37 value
38 .replace('&', "&")
39 .replace('"', """)
40 .replace('<', "<")
41 .replace('>', ">")
42 }
43}
44
45impl OutputFormat for Html {
46 type Output = String;
47
48 fn text(&self, s: &str) -> Self::Output {
49 s.to_string()
51 }
52
53 fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output {
54 items.join(delimiter)
55 }
56
57 fn finish(&self, output: Self::Output) -> String {
58 output
59 }
60
61 fn emph(&self, content: Self::Output) -> Self::Output {
62 if content.is_empty() {
63 return content;
64 }
65 format!("<i>{content}</i>")
66 }
67
68 fn strong(&self, content: Self::Output) -> Self::Output {
69 if content.is_empty() {
70 return content;
71 }
72 format!("<b>{content}</b>")
73 }
74
75 fn small_caps(&self, content: Self::Output) -> Self::Output {
76 if content.is_empty() {
77 return content;
78 }
79 format!(r#"<span style="font-variant:small-caps">{content}</span>"#)
80 }
81
82 fn superscript(&self, content: Self::Output) -> Self::Output {
83 if content.is_empty() {
84 return content;
85 }
86 format!("<sup>{content}</sup>")
87 }
88
89 fn quote(&self, content: Self::Output) -> Self::Output {
90 if content.is_empty() {
91 return content;
92 }
93 format!("\u{201C}{content}\u{201D}")
94 }
95
96 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
97 format!("{prefix}{content}{suffix}")
98 }
99
100 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
101 format!("{prefix}{content}{suffix}")
102 }
103
104 fn wrap_punctuation(&self, wrap: &WrapPunctuation, content: Self::Output) -> Self::Output {
105 match wrap {
106 WrapPunctuation::Parentheses => format!("({content})"),
107 WrapPunctuation::Brackets => format!("[{content}]"),
108 WrapPunctuation::Quotes => format!("\u{201C}{content}\u{201D}"),
109 }
110 }
111
112 fn semantic(&self, class: &str, content: Self::Output) -> Self::Output {
113 if content.is_empty() {
114 return content;
115 }
116 format!(r#"<span class="{class}">{content}</span>"#)
117 }
118
119 fn annotation(&self, content: Self::Output) -> Self::Output {
120 if content.is_empty() {
121 return content;
122 }
123 format!("<div class=\"citum-annotation\">{content}</div>")
124 }
125
126 fn semantic_with_attributes(
127 &self,
128 class: &str,
129 content: Self::Output,
130 attributes: &[SemanticAttribute],
131 ) -> Self::Output {
132 if content.is_empty() {
133 return content;
134 }
135
136 let mut extra_attrs = String::new();
137 for attribute in attributes {
138 let _ = write!(
139 &mut extra_attrs,
140 r#" {}="{}""#,
141 attribute.name,
142 Self::escape_attribute_value(&attribute.value)
143 );
144 }
145
146 format!(r#"<span class="{class}"{extra_attrs}>{content}</span>"#)
147 }
148
149 fn citation(&self, ids: Vec<String>, content: Self::Output) -> Self::Output {
150 if content.is_empty() {
151 return content;
152 }
153 let ids_str = ids.join(" ");
154 format!(r#"<span class="citum-citation" data-ref="{ids_str}">{content}</span>"#)
155 }
156
157 fn link(&self, url: &str, content: Self::Output) -> Self::Output {
158 if content.is_empty() {
159 return content;
160 }
161 format!(r#"<a href="{}">{}</a>"#, Self::sanitize_href(url), content)
162 }
163
164 fn format_id(&self, id: &str) -> String {
165 format!("ref-{id}")
166 }
167
168 fn bibliography(&self, entries: Vec<Self::Output>) -> Self::Output {
169 format!(
170 r#"<div class="citum-bibliography">
171{}
172</div>"#,
173 self.join(entries, "\n")
174 )
175 }
176
177 fn entry(
178 &self,
179 id: &str,
180 content: Self::Output,
181 url: Option<&str>,
182 metadata: &super::format::ProcEntryMetadata,
183 ) -> Self::Output {
184 let content = if let Some(u) = url {
185 self.link(u, content)
186 } else {
187 content
188 };
189
190 let mut attrs = format!(
191 r#"id="{}""#,
192 Self::escape_attribute_value(&self.format_id(id))
193 );
194 if let Some(author) = &metadata.author {
195 attrs.push_str(r#" data-author=""#);
196 attrs.push_str(&Self::escape_attribute_value(author));
197 attrs.push('"');
198 }
199 if let Some(year) = &metadata.year {
200 attrs.push_str(r#" data-year=""#);
201 attrs.push_str(&Self::escape_attribute_value(year));
202 attrs.push('"');
203 }
204 if let Some(title) = &metadata.title {
205 attrs.push_str(r#" data-title=""#);
206 attrs.push_str(&Self::escape_attribute_value(title));
207 attrs.push('"');
208 }
209
210 format!(r#"<div class="citum-entry" {attrs}>{content}</div>"#)
211 }
212}