1use ammonia::clean;
4
5use crate::CONFIG;
6use crate::config::Config;
7use crate::types::{MdBlockElement, ToHtml};
8use crate::utils::build_rel_prefix;
9
10pub fn generate_html(
23 file_name: &str,
24 md_elements: &[MdBlockElement],
25 output_dir: &str,
26 input_dir: &str,
27 html_rel_path: &str,
28) -> String {
29 let mut html_output = String::new();
30 let config = CONFIG.get().unwrap();
31
32 let head = generate_head(file_name, html_rel_path, config);
33
34 let mut body = String::from("\t<body>\n");
35 body.push_str(&indent_html(&generate_navbar(html_rel_path), 2));
36 body.push_str("\n\t\t<div id=\"content\">");
37
38 let inner_html: String = md_elements
39 .iter()
40 .map(|element| element.to_html(output_dir, input_dir, html_rel_path))
41 .collect::<Vec<String>>()
42 .join("\n");
43
44 let inner_html = if config.html.sanitize_html {
45 let mut builder = ammonia::Builder::default();
46 builder
47 .add_tag_attributes("a", &["href", "title", "target"])
48 .add_tag_attribute_values("a", "target", &["_blank", "_self"])
49 .add_tag_attributes("pre", &["class"])
50 .add_tag_attributes("code", &["class"])
51 .add_tags(&["iframe"])
52 .add_tag_attributes(
53 "iframe",
54 &[
55 "src",
56 "width",
57 "height",
58 "title",
59 "frameborder",
60 "allowfullscreen",
61 ],
62 );
63 for tag in &["h1", "h2", "h3", "h4", "h5", "h6"] {
64 builder.add_tag_attributes(tag, &["id"]);
65 }
66
67 builder.clean(&inner_html).to_string()
68 } else {
69 inner_html
70 };
71
72 body.push_str(&indent_html(&inner_html, 3));
73 body.push_str("\n\t\t</div>");
74
75 if config.html.use_prism {
76 body.push_str(
77 "\n\n\t\t<script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/components/prism-core.min.js\" integrity=\"sha512-Uw06iFFf9hwoN77+kPl/1DZL66tKsvZg6EWm7n6QxInyptVuycfrO52hATXDRozk7KWeXnrSueiglILct8IkkA==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>",
78 );
79 body.push_str("\n\t\t<script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/line-numbers/prism-line-numbers.min.js\" integrity=\"sha512-BttltKXFyWnGZQcRWj6osIg7lbizJchuAMotOkdLxHxwt/Hyo+cl47bZU0QADg+Qt5DJwni3SbYGXeGMB5cBcw==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>");
80 body.push_str(
81 "\n\t\t<script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/autoloader/prism-autoloader.min.js\" integrity=\"sha512-SkmBfuA2hqjzEVpmnMt/LINrjop3GKWqsuLSSB3e7iBmYK7JuWw4ldmmxwD9mdm2IRTTi0OxSAfEGvgEi0i2Kw==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>"
82 );
83 body.push_str("\n\t\t<script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/toolbar/prism-toolbar.min.js\" integrity=\"sha512-st608h+ZqzliahyzEpETxzU0f7z7a9acN6AFvYmHvpFhmcFuKT8a22TT5TpKpjDa3pt3Wv7Z3SdQBCBdDPhyWA==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>");
84 body.push_str("\n\t\t<script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js\" integrity=\"sha512-/kVH1uXuObC0iYgxxCKY41JdWOkKOxorFVmip+YVifKsJ4Au/87EisD1wty7vxN2kAhnWh6Yc8o/dSAXj6Oz7A==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>");
85 body.push_str("\n\t\t<script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/show-language/prism-show-language.min.js\" integrity=\"sha512-d1t+YumgzdIHUL78me4B9NzNTu9Lcj6RdGVbdiFDlxRV9JTN9s+iBQRhUqLRq5xtWUp1AD+cW2sN2OlST716fw==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>");
86 }
87
88 body.push_str("\n\t</body>\n");
89
90 html_output.push_str(&head);
91 html_output.push_str(&body);
92 html_output.push_str("</html>\n");
93
94 html_output
95}
96
97pub fn generate_index(file_names: &[String]) -> String {
105 let mut html_output = String::new();
106
107 let head = generate_head("index", "index.html", CONFIG.get().unwrap());
108
109 let mut body = String::from("\t<body>\n");
110 body.push_str(&generate_navbar("index.html"));
111 body.push_str("\n\t<div id=\"content\">\n");
112 body.push_str("<h1>All Pages</h1>\n");
113
114 file_names.iter().for_each(|file_name| {
115 body.push_str(&format!(
116 "<a href=\"./{}.html\">{}</a><br>\n",
117 file_name.trim_end_matches(".md"),
118 format_title(file_name)
119 ));
120 });
121
122 body.push_str("\n</div>\n\t</body>\n");
123
124 html_output.push_str(&head);
125 html_output.push_str(&body);
126 html_output.push_str("</html>\n");
127
128 html_output
129}
130
131fn generate_head(file_name: &str, html_rel_path: &str, config: &Config) -> String {
138 let mut head = String::from(
139 r#"<!DOCTYPE html>
140 <html lang="en">
141 <head>
142 <meta charset="UTF-8">
143 <meta name="viewport" content="width=device-width, initial-scale=1.0">
144 "#,
145 );
146
147 let title = format_title(file_name);
149 head.push_str(&format!("\t<title>{}</title>\n", title));
150
151 let favicon_file = &config.html.favicon_file;
152 if !favicon_file.is_empty() {
153 let mut favicon_path = build_rel_prefix(html_rel_path);
154 favicon_path.push("media");
155 favicon_path.push(favicon_file.rsplit("/").next().unwrap());
156 let favicon_href = favicon_path.to_string_lossy();
157
158 head.push_str(&format!(
159 "\t<link rel=\"icon\" href=\"{}\">\n",
160 favicon_href
161 ));
162 }
163
164 let css_file = &config.html.css_file;
165 let mut css_path = build_rel_prefix(html_rel_path);
166 css_path.push("styles.css");
167 let css_href = css_path.to_string_lossy();
168
169 if css_file == "default" {
170 head.push_str(&format!(
171 "\t\t<link rel=\"stylesheet\" href=\"{}\">\n",
172 css_href
173 ));
174 } else {
175 head.push_str(&format!(
176 "\t\t<link rel=\"stylesheet\" href=\"{}\">\n",
177 css_file
178 ));
179 }
180
181 if config.html.use_prism {
182 if !config.html.prism_theme.is_empty() {
183 let theme = if config.html.sanitize_html {
184 &clean(&config.html.prism_theme)
185 } else {
186 &config.html.prism_theme
187 };
188
189 head.push_str(&format!("\t\t<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/prism-themes/1.9.0/prism-{}.min.css\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\" />", theme));
190 } else {
191 head.push_str("\t\t<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/prismjs@1.30.0/themes/prism-okaidia.min.css\">");
192 }
193 head.push_str("\t\t<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/toolbar/prism-toolbar.min.css\" integrity=\"sha512-Dqf5696xtofgH089BgZJo2lSWTvev4GFo+gA2o4GullFY65rzQVQLQVlzLvYwTo0Bb2Gpb6IqwxYWtoMonfdhQ==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\" />");
194 head.push_str("\t\t<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/line-numbers/prism-line-numbers.min.css\" integrity=\"sha512-cbQXwDFK7lj2Fqfkuxbo5iD1dSbLlJGXGpfTDqbggqjHJeyzx88I3rfwjS38WJag/ihH7lzuGlGHpDBymLirZQ==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\" />");
195 }
196
197 head.push_str("\t</head>\n");
198 head
199}
200
201fn generate_navbar(html_rel_path: &str) -> String {
203 let mut navbar = String::from("<header>\n\t<nav>\n\t\t<ul>\n");
204
205 let mut home_path = build_rel_prefix(html_rel_path);
206 home_path.push("index.html");
207 let home_href = home_path.to_string_lossy();
208
209 navbar.push_str(&format!(
210 "\t\t\t<li><a href=\"{}\">Home</a></li>",
211 home_href
212 ));
213 navbar.push_str("\n\t\t</ul>\n\t</nav>\n</header>\n\n");
214 navbar
215}
216fn format_title(file_name: &str) -> String {
224 let title = file_name.trim_end_matches(".md").replace('_', " ");
225
226 title
227 .split_whitespace()
228 .map(|word| {
229 let mut chars = word.chars();
230 match chars.next() {
231 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
232 None => String::new(),
233 }
234 })
235 .collect::<Vec<String>>()
236 .join(" ")
237}
238
239pub fn indent_html(html: &str, level: usize) -> String {
241 let indent = "\t".repeat(level);
242 html.lines()
243 .map(|line| {
244 let first_non_whitespace_token = line.chars().find(|c| !c.is_whitespace());
245
246 match first_non_whitespace_token {
247 Some('<') => format!("{indent}{line}"),
248 Some(_) => line.to_string(),
249 None => line.to_string(), }
251 })
252 .collect::<Vec<_>>()
253 .join("\n")
254}
255
256pub fn generate_default_css() -> String {
258 r#"
259 body {
260 background-color: #121212;
261 color: #e0e0e0;
262 font-family:
263 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
264 Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
265 line-height: 1.75;
266 margin: 0;
267 padding: 0;
268 }
269
270 /* Card-like container for the page content */
271 #content {
272 background-color: #1e1e1e;
273 max-width: 780px;
274 margin: 1.5rem auto;
275 padding: 2rem;
276 border-radius: 12px;
277 box-shadow: 0 0 0 1px #2c2c2c;
278 }
279
280 header {
281 background-color: #1a1a1a;
282 border-bottom: 1px solid #333;
283 position: sticky;
284 top: 0;
285 z-index: 1000;
286 }
287
288 nav {
289 padding: 1rem 2rem;
290 display: flex;
291 justify-content: flex-start;
292 }
293
294 nav ul {
295 list-style: none;
296 margin: 0;
297 padding: 0;
298 display: flex;
299 gap: 1rem;
300 }
301
302 nav ul li {
303 margin: 0;
304 }
305
306 nav ul li a {
307 color: #ddd;
308 text-decoration: none;
309 padding: 0.5rem 1rem;
310 border-radius: 6px;
311 transition: background-color 0.2s ease, color 0.2s ease;
312 }
313
314 nav ul li a:hover {
315 background-color: #2f2f2f;
316 color: #fff;
317 }
318
319 nav ul li a.active {
320 background-color: #4ea1f3;
321 color: #121212;
322 }
323 h1,
324 h2,
325 h3,
326 h4,
327 h5,
328 h6 {
329 color: #ffffff;
330 line-height: 1.3;
331 margin-top: 2rem;
332 margin-bottom: 1rem;
333 }
334
335 h1 {
336 font-size: 2.25rem;
337 border-bottom: 2px solid #2c2c2c;
338 padding-bottom: 0.3rem;
339 }
340 h2 {
341 font-size: 1.75rem;
342 border-bottom: 1px solid #2c2c2c;
343 padding-bottom: 0.2rem;
344 }
345 h3 {
346 font-size: 1.5rem;
347 }
348 h4 {
349 font-size: 1.25rem;
350 }
351 h5,
352 h6 {
353 font-size: 1rem;
354 font-weight: normal;
355 }
356
357 p {
358 margin-bottom: 1.2rem;
359 }
360
361 a {
362 color: #4ea1f3;
363 text-decoration: none;
364 transition: color 0.2s ease-in-out;
365 }
366 a:hover {
367 color: #82cfff;
368 text-decoration: underline;
369 }
370
371 img {
372 max-width: 100%;
373 height: auto;
374 display: block;
375 margin: 1.5rem auto;
376 border-radius: 8px;
377 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
378 }
379
380 /* Styles for when "use_prism = false" is set in config.toml */
381 pre.non_prism {
382 background-color: #2a2a2a;
383 padding: 1rem;
384 border-radius: 8px;
385 overflow-x: auto;
386 font-size: 0.9rem;
387 box-shadow: inset 0 0 0 1px #333;
388 }
389 pre.non_prism::before {
390 counter-reset: listing;
391 }
392 code.non_prism {
393 font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
394 font-style: normal;
395 background-color: #2a2a2a;
396 padding: 0.2em 0.4em;
397 border-radius: 4px;
398 font-size: 0.95em;
399 color: #dcdcdc;
400 }
401 pre.non_prism code.non_prism {
402 counter-increment: listing;
403 padding: 0 0.4em;
404 text-align: left;
405 float: left;
406 clear: left;
407 }
408 pre.non_prism code.non_prism::before {
409 content: counter(listing) ". ";
410 display: inline-block;
411 font-size: 0.85em;
412 float: left;
413 height: 1em;
414 padding-top: 0.2em;
415 padding-left: auto;
416 margin-left: auto;
417 text-align: right;
418 }
419
420 code {
421 font-style: normal;
422 }
423
424 blockquote {
425 border-left: 4px solid #555;
426 padding: 0.1rem 1rem;
427 color: #aaa;
428 font-style: italic;
429 margin: 1.5rem 0;
430 background-color: #1a1a1a;
431 border-radius: 2px;
432 }
433
434 .toolbar-item {
435 font-style: normal;
436 margin-right: 0.2em;
437 }
438
439 ul,
440 ol {
441 padding-left: 1.5rem;
442 margin-bottom: 1.2rem;
443 }
444 li {
445 margin-bottom: 0.5rem;
446 }
447
448 table {
449 width: 100%;
450 border-spacing: 0;
451 margin: 2rem 0;
452 background-color: #1e1e1e;
453 border: 1px solid #333;
454 border-radius: 8px;
455 overflow: hidden;
456 font-size: 0.95rem;
457 }
458
459 th,
460 td {
461 padding: 0.75rem 1rem;
462 text-align: left;
463 }
464
465 th {
466 background-color: #2a2a2a;
467 color: #ffffff;
468 font-weight: 600;
469 }
470
471 tr:nth-child(even) td {
472 background-color: #222;
473 }
474
475 tr:hover td {
476 background-color: #2f2f2f;
477 }
478
479 td {
480 color: #ddd;
481 border-top: 1px solid #333;
482 }
483
484 hr {
485 border: none;
486 border-top: 1px solid #333;
487 margin: 2rem 0;
488 }
489 "#
490 .to_string()
491}