use std::fmt::Write as _;
use std::io::Error;
#[cfg(feature = "fs")]
use std::path::Path;
use std::sync::Arc;
#[cfg(feature = "fs")]
use crate::input::ResolvedFileGenerationOptions;
use crate::input::{LoadedSvgSource, ResolvedGenerationOptions};
use crate::rendering::css::SharedTemplateData;
#[cfg(feature = "fs")]
use crate::rendering::css::TemplateDependencies;
#[cfg(feature = "fs")]
use crate::rendering::css::template_dependencies;
#[cfg(feature = "fs")]
use crate::rendering::css::{build_css_context_with_fonts_url, render_css_with_context};
#[cfg(feature = "fs")]
use crate::rendering::paths::{path_to_slashes, relative_path};
use crate::rendering::shared::to_io_err;
use handlebars::{
Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderErrorReason,
};
use serde_json::{Map, Value};
#[cfg(feature = "fs")]
pub(crate) fn build_html_context(
options: &ResolvedFileGenerationOptions,
shared: &SharedTemplateData,
source_files: &[LoadedSvgSource],
styles: Option<String>,
) -> Result<Map<String, Value>, Error> {
let styles = match styles {
Some(styles) => styles,
None => {
let html_fonts_url = html_css_fonts_url(options);
let css_ctx = build_css_context_with_fonts_url(options, shared, Some(&html_fonts_url));
render_css_with_context(shared, &css_ctx)?
}
};
Ok(make_ctx(options, shared, source_files, styles))
}
#[cfg(feature = "fs")]
pub(crate) fn build_html_context_with_css(
options: &ResolvedFileGenerationOptions,
shared: &SharedTemplateData,
source_files: &[LoadedSvgSource],
finalized_css: &Map<String, Value>,
) -> Result<Map<String, Value>, Error> {
let mut css_ctx = finalized_css.clone();
let original_css = crate::rendering::css::build_css_context(options, shared);
if css_ctx.get("src") == original_css.get("src") {
let html_css =
build_css_context_with_fonts_url(options, shared, Some(&html_css_fonts_url(options)));
css_ctx.insert("src".to_owned(), html_css["src"].clone());
}
let mut html_ctx = build_html_context(
options,
shared,
source_files,
Some(render_css_with_context(shared, &css_ctx)?),
)?;
for field in ["baseSelector", "classPrefix"] {
if let Some(value) = finalized_css.get(field) {
html_ctx.insert(field.to_owned(), value.clone());
} else {
html_ctx.remove(field);
}
}
Ok(html_ctx)
}
pub(crate) fn render_html_with_hbs_context(
cached_registry: Option<&Handlebars<'static>>,
hbs_ctx: &handlebars::Context,
map_ctx: &Map<String, Value>,
) -> Result<String, Error> {
match cached_registry {
Some(registry) => registry
.render_with_context("html", hbs_ctx)
.map_err(to_io_err),
None => Ok(render_default_html(map_ctx)),
}
}
#[cfg(all(test, feature = "fs"))]
fn render_html_with_context(
options: &ResolvedFileGenerationOptions,
cached_registry: Option<&Handlebars<'static>>,
ctx: &Map<String, Value>,
) -> Result<String, Error> {
match cached_registry {
Some(registry) => registry.render("html", ctx).map_err(to_io_err),
None if options.html_template.is_some() => {
let registry = build_html_registry(options)?
.ok_or_else(|| to_io_err("HTML template path set but failed to compile"))?;
registry.render("html", ctx).map_err(to_io_err)
}
None => Ok(render_default_html(ctx)),
}
}
#[cfg(all(test, feature = "fs"))]
pub(crate) fn build_html_registry(
options: &ResolvedFileGenerationOptions,
) -> Result<Option<Handlebars<'static>>, Error> {
Ok(super::files::load_html_template(options)?.registry)
}
pub(crate) struct HtmlTemplate {
#[cfg(feature = "fs")]
pub(crate) source: Option<Arc<str>>,
pub(crate) registry: Option<Handlebars<'static>>,
#[cfg(feature = "fs")]
pub(crate) dependencies: TemplateDependencies,
}
pub(crate) fn compile_html_template(source: Option<Arc<str>>) -> Result<HtmlTemplate, Error> {
let Some(text) = source.as_deref() else {
return Ok(HtmlTemplate {
#[cfg(feature = "fs")]
source,
registry: None,
#[cfg(feature = "fs")]
dependencies: TemplateDependencies::html_default(),
});
};
#[cfg(feature = "fs")]
let dependencies = template_dependencies(text);
let mut registry = Handlebars::new();
registry.register_helper("removePeriods", Box::new(RemovePeriodsHelper));
registry
.register_template_string("html", text)
.map_err(|error| to_io_err(format!("Failed to compile HTML template: {error}")))?;
Ok(HtmlTemplate {
#[cfg(feature = "fs")]
source,
registry: Some(registry),
#[cfg(feature = "fs")]
dependencies,
})
}
#[inline]
#[cfg(feature = "fs")]
pub(crate) fn render_default_html_with_styles(ctx: &Map<String, Value>, styles: &str) -> String {
render_default_html_inner(ctx, styles)
}
fn render_default_html(ctx: &Map<String, Value>) -> String {
render_default_html_inner(ctx, super::ctx_str(ctx, "styles", ""))
}
fn render_default_html_inner(ctx: &Map<String, Value>, styles: &str) -> String {
let font_name = super::ctx_str(ctx, "fontName", "");
let base_selector = super::ctx_str(ctx, "baseSelector", ".icon");
let class_prefix = super::ctx_str(ctx, "classPrefix", "icon-");
let names = ctx.get("names").and_then(|v| v.as_array());
let base_class = base_selector.replace('.', "");
let name_count = names.map_or(0, |n| n.len());
let mut result = String::with_capacity(512 + name_count * 120);
_ = write!(
result,
"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<title>{font_name}</title>\n\t<style>\n"
);
result.push_str("\t\tbody {\n\t\t\tfont-family: sans-serif;\n\t\t\tmargin: 0;\n\t\t\tpadding: 10px 20px;\n\t\t}\n\n");
result.push_str("\t\t.preview {\n\t\t\tline-height: 2em;\n\t\t}\n\n");
result.push_str("\t\t.preview__icon {\n\t\t\tdisplay: inline-block;\n\t\t\twidth: 32px;\n\t\t\ttext-align: center;\n\t\t}\n\n");
_ = write!(
result,
"\t\t{base_selector} {{\n\t\t\tdisplay: inline-block;\n\t\t\tfont-size: 16px;\n\t\t}}\n\n"
);
_ = writeln!(result, "\t\t{styles}");
result.push_str("\t</style>\n</head>\n<body>\n");
_ = writeln!(result, "\t<h1>{font_name}</h1>");
if let Some(names) = names {
for name_value in names {
let name = name_value.as_str().unwrap_or("");
_ = write!(
result,
"\t<div class=\"preview\">\n\t\t<span class=\"preview__icon\">\n\t\t\t<span class=\"{base_class} {class_prefix}{name}\"></span>\n\t\t</span>\n\t\t<span>{name}</span>\n\t</div>\n"
);
}
}
result.push_str("</body>\n</html>\n");
result
}
#[cfg(feature = "fs")]
fn html_css_fonts_url(options: &ResolvedFileGenerationOptions) -> String {
let html_dir = Path::new(&options.html_dest)
.parent()
.unwrap_or_else(|| Path::new("."));
path_to_slashes(relative_path(html_dir, Path::new(&options.dest)))
}
#[cfg(feature = "fs")]
fn make_ctx(
options: &ResolvedFileGenerationOptions,
shared: &SharedTemplateData,
source_files: &[LoadedSvgSource],
styles: String,
) -> Map<String, Value> {
let css_ctx = options.variants.as_ref().map(|_| {
build_css_context_with_fonts_url(options, shared, Some(&html_css_fonts_url(options)))
});
make_content_html_context(options, shared, source_files, styles, css_ctx.as_ref())
}
pub(crate) fn make_content_html_context(
options: &ResolvedGenerationOptions,
shared: &SharedTemplateData,
source_files: &[LoadedSvgSource],
styles: String,
variant_css_context: Option<&Map<String, Value>>,
) -> Map<String, Value> {
let mut ctx = shared.template_options.clone();
ctx.extend(Map::from_iter([
(
"codepoints".to_owned(),
Value::Object(shared.codepoints_num.clone()),
),
(
"fontName".to_owned(),
Value::String(options.font_name.clone()),
),
(
"names".to_owned(),
Value::Array(
source_files
.iter()
.map(|source_file| Value::String(source_file.glyph_name.clone()))
.collect(),
),
),
("styles".to_owned(), Value::String(styles)),
]));
if let Some(css_ctx) = variant_css_context {
for key in [
"variants",
"variantClassPrefix",
"defaultWeight",
"fontStyle",
] {
ctx.insert(key.to_owned(), css_ctx[key].clone());
}
let mut seen = std::collections::HashSet::new();
ctx.insert(
"names".to_owned(),
Value::Array(
source_files
.iter()
.filter(|source| seen.insert(source.glyph_name.as_str()))
.map(|source| Value::String(source.glyph_name.clone()))
.collect(),
),
);
}
ctx
}
struct RemovePeriodsHelper;
impl HelperDef for RemovePeriodsHelper {
fn call<'reg: 'rc, 'rc>(
&self,
helper: &Helper<'rc>,
_: &'reg Handlebars<'reg>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> HelperResult {
let selector = helper
.param(0)
.and_then(|value| value.value().as_str())
.ok_or_else(|| RenderErrorReason::ParamNotFoundForIndex("RemovePeriodsHelper", 0))?;
out.write(&selector.replace('.', ""))?;
Ok(())
}
}
#[cfg(all(test, feature = "fs"))]
mod tests;