use typst::{Feature, Features};
use typst_html::{HtmlDocument, HtmlOptions};
use crate::clock::Clock;
use crate::data::DataSet;
use crate::diagnostics::to_diagnostics;
use crate::error::{PublishError, Result};
use crate::fonts::SharedFonts;
use crate::template::{PreparedTemplate, TemplateSource};
use crate::world::PublishWorld;
#[derive(Clone)]
pub struct HtmlConfig {
pretty: bool,
pub(crate) clock: Clock,
}
impl HtmlConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn pretty(mut self, pretty: bool) -> Self {
self.pretty = pretty;
self
}
#[must_use]
pub fn clock(mut self, clock: Clock) -> Self {
self.clock = clock;
self
}
}
impl Default for HtmlConfig {
fn default() -> Self {
Self {
pretty: false,
clock: Clock::SystemLocal,
}
}
}
pub fn render_html(
fonts: &SharedFonts,
template: TemplateSource,
data: DataSet,
config: &HtmlConfig,
) -> Result<String> {
let prepared = PreparedTemplate::new(template)?;
render_html_prepared(fonts, &prepared, data, config)
}
pub fn render_html_prepared(
fonts: &SharedFonts,
template: &PreparedTemplate,
data: DataSet,
config: &HtmlConfig,
) -> Result<String> {
let world = PublishWorld::new(
template.root.clone(),
template.source.clone(),
fonts,
data,
config.clock,
Features::from_iter([Feature::Html]),
)?;
let warned = typst::compile::<HtmlDocument>(&world);
let doc = warned
.output
.map_err(|diags| PublishError::Compilation(to_diagnostics(&world, &diags)))?;
let options = HtmlOptions {
pretty: config.pretty,
};
typst_html::html(&doc, &options)
.map_err(|diags| PublishError::HtmlExport(to_diagnostics(&world, &diags)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DataSet;
fn fonts() -> SharedFonts {
SharedFonts::embedded_only()
}
#[test]
fn renders_basic_html() {
let html = render_html(
&fonts(),
TemplateSource::InMemory {
content: "= Title\n\nHello *world*.".to_owned(),
root: None,
},
DataSet::from_inputs(serde_json::json!({})).unwrap(),
&HtmlConfig::default(),
)
.unwrap();
assert!(html.contains("<html"), "expected an html document: {html}");
assert!(html.contains("Hello"), "expected body text: {html}");
}
#[test]
fn equations_become_mathml() {
let html = render_html(
&fonts(),
TemplateSource::InMemory {
content: "$ a^2 + b^2 = c^2 $".to_owned(),
root: None,
},
DataSet::from_inputs(serde_json::json!({})).unwrap(),
&HtmlConfig::default(),
)
.unwrap();
assert!(html.contains("<math"), "equation should export as MathML: {html}");
}
#[test]
fn sys_inputs_reach_the_template() {
let html = render_html(
&fonts(),
TemplateSource::InMemory {
content: "#sys.inputs.name".to_owned(),
root: None,
},
DataSet::from_inputs(serde_json::json!({ "name": "Acme" })).unwrap(),
&HtmlConfig::default(),
)
.unwrap();
assert!(html.contains("Acme"), "expected injected data: {html}");
}
#[test]
fn compilation_error_surfaces() {
let err = render_html(
&fonts(),
TemplateSource::InMemory {
content: "#let x = ".to_owned(),
root: None,
},
DataSet::from_inputs(serde_json::json!({})).unwrap(),
&HtmlConfig::default(),
)
.unwrap_err();
assert!(matches!(err, PublishError::Compilation(_)), "got {err:?}");
}
}