use crate::error::FormatError;
use crate::formats::html::{HtmlOptions, HtmlTheme};
use html5ever::{
ns, serialize, serialize::SerializeOpts, serialize::TraversalScope, Attribute, LocalName,
QualName,
};
use markup5ever_rcdom::{Handle, Node, NodeData, RcDom, SerializableHandle};
use std::cell::{Cell, RefCell};
use std::default::Default;
use std::rc::Rc;
pub(super) fn create_element(tag: &str, attrs: Vec<(&str, &str)>) -> Handle {
let qual_name = QualName::new(None, ns!(html), LocalName::from(tag));
let attributes = attrs
.into_iter()
.map(|(name, value)| Attribute {
name: QualName::new(None, ns!(), LocalName::from(name)),
value: value.to_string().into(),
})
.collect();
Rc::new(Node {
parent: Cell::new(None),
children: RefCell::new(Vec::new()),
data: NodeData::Element {
name: qual_name,
attrs: RefCell::new(attributes),
template_contents: Default::default(),
mathml_annotation_xml_integration_point: false,
},
})
}
pub(super) fn is_element_with_tag(handle: &Handle, tag: &str) -> bool {
if let NodeData::Element { name, .. } = &handle.data {
name.local.as_ref() == tag
} else {
false
}
}
pub(super) fn create_text(text: &str) -> Handle {
Rc::new(Node {
parent: Cell::new(None),
children: RefCell::new(Vec::new()),
data: NodeData::Text {
contents: RefCell::new(text.to_string().into()),
},
})
}
pub(super) fn create_comment(text: &str) -> Handle {
Rc::new(Node {
parent: Cell::new(None),
children: RefCell::new(Vec::new()),
data: NodeData::Comment {
contents: text.to_string().into(),
},
})
}
pub(super) fn serialize_dom(dom: &RcDom) -> Result<String, FormatError> {
let mut output = Vec::new();
let doc_container = dom
.document
.children
.borrow()
.first()
.ok_or_else(|| FormatError::SerializationError("Empty document".to_string()))?
.clone();
let opts = SerializeOpts {
traversal_scope: TraversalScope::IncludeNode,
..Default::default()
};
for child in doc_container.children.borrow().iter() {
let serializable = SerializableHandle::from(child.clone());
serialize(&mut output, &serializable, opts.clone()).map_err(|e| {
FormatError::SerializationError(format!("HTML serialization failed: {e}"))
})?;
}
String::from_utf8(output)
.map_err(|e| FormatError::SerializationError(format!("UTF-8 conversion failed: {e}")))
}
pub(super) fn wrap_in_document(
body_html: &str,
head_title: &str,
body_title: Option<&str>,
body_subtitle: Option<&str>,
has_math: bool,
options: &HtmlOptions,
) -> Result<String, FormatError> {
let baseline_css = include_str!("../../../../css/baseline.css");
let theme_css = match options.theme {
HtmlTheme::FancySerif => include_str!("../../../../css/themes/theme-fancy-serif.css"),
HtmlTheme::Modern => include_str!("../../../../css/themes/theme-modern.css"),
};
let custom_css = options.custom_css.as_deref().unwrap_or("");
let escaped_head_title = html_escape(head_title);
let header_html = match body_title {
Some(t) => {
let escaped_t = html_escape(t);
match body_subtitle {
Some(s) => format!(
"<header class=\"lex-doc-header\"><h1 class=\"lex-doc-title\">{escaped_t}</h1><p class=\"lex-doc-subtitle\">{}</p></header>\n",
html_escape(s)
),
None => format!(
"<header class=\"lex-doc-header\"><h1 class=\"lex-doc-title\">{escaped_t}</h1></header>\n"
),
}
}
None => String::new(),
};
let katex_html = if has_math {
r#" <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" integrity="sha384-nB0miv6/jRmo5UMMR1wu3Gz6NLsoTkbqJghGIsx//Rlm+ZU03BU6SQNC66uf4l5+" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js" integrity="sha384-7zkQWkzuo3B5mTepMUcHkMB5jZaolc2xDwL6VFqjFALcbeS9Ggm/Yr2r3Dy4lfFg" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous" onload="renderMathInElement(document.body, {delimiters: [{left: '$', right: '$', display: false}, {left: '$$', right: '$$', display: true}], throwOnError: false});"></script>
"#
} else {
""
};
let html = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="lex-babel">
<title>{escaped_head_title}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css" integrity="sha384-eFTL69TLRZTkNfYZOLM+G04821K1qZao/4QLJbet1pP4tcF+fdXq/9CdqAbWRl/L" crossorigin="anonymous">
{katex_html} <style>
{baseline_css}
{theme_css}
{custom_css}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js" integrity="sha384-RH2xi4eIQ/gjtbs9fUXM68sLSi99C7ZWBRX1vDrVv6GQXRibxXLbwO2NGZB74MbU" crossorigin="anonymous"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div class="lex-document">
{header_html}{body_html}
</div>
</body>
</html>"#
);
Ok(html)
}
pub(super) fn normalize_language(lang: &str) -> &str {
match lang {
"js" => "javascript",
"ts" => "typescript",
"py" => "python",
"sh" => "bash",
"c++" | "cpp" => "cpp",
"c#" | "csharp" => "csharp",
"yml" => "yaml",
"rb" => "ruby",
"rs" => "rust",
"kt" => "kotlin",
"md" => "markdown",
"objc" | "obj-c" => "objectivec",
other => other,
}
}
pub(super) fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}