use super::html_uri::HtmlFileUri;
use crate::markdown::color_preset::DiagramColorPreset;
pub(crate) struct HtmlExportTemplate;
impl HtmlExportTemplate {
const FONT_FAMILIES: &'static str = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'";
const MONOSPACE_FAMILIES: &'static str =
"SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace";
pub(crate) fn assemble_html_document(css: &str, body: &str) -> String {
format!(
"{}{}{}",
Self::document_head(css),
body,
Self::document_tail()
)
}
fn document_head(css: &str) -> String {
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Exported Document</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<style>
{css}
</style>
</head>
<body>
"#
)
}
fn document_tail() -> &'static str {
r#"
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false}
],
ignoredTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'option', 'svg'],
throwOnError: false
});
document.querySelectorAll('[data-math-style=display]').forEach(function(el) {
var src = el.textContent;
el.innerHTML = '';
katex.render(src, el, {displayMode: true, throwOnError: false});
});
document.querySelectorAll('[data-math-style=inline]').forEach(function(el) {
var src = el.textContent;
el.innerHTML = '';
katex.render(src, el, {displayMode: false, throwOnError: false});
});
"></script>
</body>
</html>"#
}
pub(crate) fn resolve_relative_paths(html: &str, base_dir: &std::path::Path) -> String {
let mut output = String::with_capacity(html.len());
let mut remaining = html;
while let Some(marker_start) = remaining.find(r#"src=""#) {
let value_start = marker_start + r#"src=""#.len();
output.push_str(&remaining[..value_start]);
let Some(value_end) = remaining[value_start..].find('"') else {
output.push_str(&remaining[value_start..]);
return output;
};
let src = &remaining[value_start..value_start + value_end];
output.push_str(&Self::resolve_src(src, base_dir));
remaining = &remaining[value_start + value_end..];
}
output.push_str(remaining);
output
}
fn resolve_src(src: &str, base_dir: &std::path::Path) -> String {
if src.starts_with("http://")
|| src.starts_with("https://")
|| src.starts_with("data:")
|| src.starts_with("file://")
{
return src.to_string();
}
HtmlFileUri::from_path(&base_dir.join(src))
}
pub(crate) fn generate_css(preset: &DiagramColorPreset) -> String {
let bg_color = Self::background_color(preset);
let base = Self::generate_base_css(preset, bg_color);
let elements = Self::generate_elements_css(preset, bg_color);
format!("{base}{elements}")
}
fn background_color(preset: &DiagramColorPreset) -> &str {
if preset.background == "transparent" {
if preset.text == "#E0E0E0" {
"#1e1e1e"
} else {
"#ffffff"
}
} else {
preset.background.as_ref()
}
}
fn generate_base_css(preset: &DiagramColorPreset, bg_color: &str) -> String {
format!(
r#"
body {{ font-family: {fonts}; background-color: {bg_color}; color: {text}; line-height: 1.6; max-width: 900px; margin: 0 auto; padding: 2rem; }}
h1, h2, h3, h4, h5, h6 {{ margin-top: 1.5em; margin-bottom: 0.5em; font-weight: 600; }}
h1 {{ border-bottom: 1px solid {stroke}; padding-bottom: 0.3em; }}
h2 {{ border-bottom: 1px solid {stroke}; padding-bottom: 0.3em; }}
a {{ color: #0366d6; text-decoration: none; }}
pre {{ background-color: {fill}; border: 1px solid {stroke}; border-radius: 6px; padding: 16px; overflow: auto; line-height: 1.5; }}
code {{ font-family: {monos}; background-color: {fill}; border-radius: 3px; padding: 0.2em 0.4em; font-size: 85%; }}
pre code {{ background-color: transparent; padding: 0; }}
"#,
fonts = Self::FONT_FAMILIES,
bg_color = bg_color,
text = preset.text,
stroke = preset.stroke,
fill = preset.fill,
monos = Self::MONOSPACE_FAMILIES
)
}
fn generate_elements_css(preset: &DiagramColorPreset, bg_color: &str) -> String {
let base = format!(
r#"
blockquote {{ border-left: 0.25em solid {stroke}; color: {text}; opacity: 0.8; padding: 0 1em; margin: 0; }}
table {{ border-spacing: 0; border-collapse: collapse; margin-top: 0; margin-bottom: 16px; }}
table th, table td {{ padding: 6px 13px; border: 1px solid {stroke}; }}
img {{ max-width: 100%; box-sizing: content-box; background-color: {bg_color}; }}
.katana-diagram img {{ background-color: transparent; }}
hr {{ height: 0.25em; padding: 0; margin: 24px 0; background-color: {stroke}; border: 0; }}
"#,
bg_color = bg_color,
text = preset.text,
stroke = preset.stroke
);
let alerts = Self::generate_alerts_css();
let extras = Self::generate_extras_css(preset);
format!("{base}{alerts}{extras}")
}
fn generate_alerts_css() -> String {
r#"
.markdown-alert { padding: 0.5rem 1rem; margin-bottom: 16px; border-left: 0.25em solid; border-radius: 4px; }
.markdown-alert-title { font-weight: 600; margin-bottom: 0.25rem; }
.markdown-alert-note { border-left-color: #539bf5; }
.markdown-alert-note .markdown-alert-title { color: #539bf5; }
.markdown-alert-tip { border-left-color: #57ab5a; }
.markdown-alert-tip .markdown-alert-title { color: #57ab5a; }
.markdown-alert-important { border-left-color: #986ee2; }
.markdown-alert-important .markdown-alert-title { color: #986ee2; }
.markdown-alert-warning { border-left-color: #c69026; }
.markdown-alert-warning .markdown-alert-title { color: #c69026; }
.markdown-alert-caution { border-left-color: #e5534b; }
.markdown-alert-caution .markdown-alert-title { color: #e5534b; }
"#.to_string()
}
fn generate_extras_css(preset: &DiagramColorPreset) -> String {
format!(
r#"
ul.contains-task-list {{ list-style: none; padding-left: 1.5em; }}
input[type="checkbox"] {{ margin-right: 0.5em; }}
.footnotes {{ border-top: 1px solid {stroke}; margin-top: 2em; padding-top: 1em; font-size: 0.9em; }}
.footnote-ref {{ font-size: 0.75em; vertical-align: super; }}
math, .math-display, .math-inline {{ font-family: 'KaTeX_Main', 'Times New Roman', serif; }}
dt {{ font-weight: 600; margin-top: 0.5em; }}
dd {{ margin-left: 1.5em; margin-bottom: 0.5em; }}
"#,
stroke = preset.stroke
)
}
}
#[cfg(test)]
#[path = "html_template_tests.rs"]
mod tests;