webfont-generator 0.11.0

Generate webfonts (SVG, TTF, EOT, WOFF, WOFF2) from SVG icons
Documentation
use std::fmt::Write as _;
use std::io::Error;

use serde_json::{Map, Value};

use super::context::SharedTemplateData;
use crate::rendering::shared::to_io_err;

/// Render CSS using a pre-built Handlebars Context (no serialization).
/// Falls back to the hot-path renderer when no custom template is configured.
#[cfg(feature = "fs")]
pub(crate) fn render_css_with_hbs_context(
    shared: &SharedTemplateData,
    hbs_ctx: &handlebars::Context,
    map_ctx: &Map<String, Value>,
) -> Result<String, Error> {
    #[cfg(test)]
    shared
        .css_render_count
        .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    match shared.css_registry()? {
        Some(registry) => registry
            .render_with_context("css", hbs_ctx)
            .map_err(to_io_err),
        None => Ok(render_default_css(map_ctx)),
    }
}

/// Render CSS from a Map context (used during init when no pre-built hbs Context exists yet).
pub(crate) fn render_css_with_context(
    shared: &SharedTemplateData,
    ctx: &Map<String, Value>,
) -> Result<String, Error> {
    #[cfg(test)]
    shared
        .css_render_count
        .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    match shared.css_registry()? {
        Some(registry) => registry.render("css", ctx).map_err(to_io_err),
        None => Ok(render_default_css(ctx)),
    }
}

/// Render CSS with a different `src` value by mutating the Context in place,
/// rendering, then restoring the original value. Zero allocation.
/// Falls back to the hot-path renderer when no custom template is configured.
#[cfg(feature = "fs")]
pub(crate) fn render_css_with_src_mutate(
    shared: &SharedTemplateData,
    hbs_ctx: &mut handlebars::Context,
    map_ctx: &Map<String, Value>,
    src: &str,
) -> Result<String, Error> {
    #[cfg(test)]
    shared
        .css_render_count
        .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    match shared.css_registry()? {
        Some(registry) => crate::rendering::shared::render_with_field_swap(
            hbs_ctx,
            "src",
            Value::String(src.to_owned()),
            |ctx| registry.render_with_context("css", ctx).map_err(to_io_err),
        ),
        None => Ok(render_default_css_inner(
            map_ctx,
            crate::rendering::ctx_str(map_ctx, "fontName", ""),
            src,
        )),
    }
}

pub(super) fn render_default_css(ctx: &Map<String, Value>) -> String {
    render_default_css_inner(
        ctx,
        crate::rendering::ctx_str(ctx, "fontName", ""),
        crate::rendering::ctx_str(ctx, "src", ""),
    )
}

fn render_default_css_inner(ctx: &Map<String, Value>, font_name: &str, src: &str) -> String {
    let base_selector = crate::rendering::ctx_str(ctx, "baseSelector", ".icon");
    let class_prefix = crate::rendering::ctx_str(ctx, "classPrefix", "icon-");
    let codepoints = ctx.get("codepoints").and_then(|v| v.as_object());

    let codepoint_count = codepoints.map_or(0, |c| c.len());
    let mut result = String::with_capacity(256 + codepoint_count * 60);

    if ctx.get("__webfontVariantMode") == Some(&Value::Bool(true)) {
        let variants = ctx
            .get("variants")
            .and_then(Value::as_array)
            .map(Vec::as_slice)
            .unwrap_or_default();
        let style = crate::rendering::ctx_str(ctx, "fontStyle", "normal");
        let default_weight = &ctx["defaultWeight"];
        for variant in variants {
            let weight = &variant["weight"];
            _ = write!(
                result,
                "@font-face {{\n\tfont-family: \"{font_name}\";\n\tfont-display: block;\n\tfont-style: {style};\n\tfont-weight: {weight};\n\tsrc: {src};\n}}\n\n"
            );
        }
        _ = write!(result, "{base_selector} {{\n\tline-height: 1;\n}}\n\n");
        _ = write!(
            result,
            "{base_selector}:before {{\n\tfont-family: {font_name} !important;\n\tfont-style: {style};\n\tfont-weight: {default_weight};\n\tfont-synthesis: none;\n\tvertical-align: top;\n}}\n\n"
        );
        for variant in variants {
            let selector = variant["selector"].as_str().unwrap_or("");
            let weight = &variant["weight"];
            // :is() ties modifiers to the configured base, including selector lists.
            _ = write!(
                result,
                ":is({base_selector}).{selector}:before {{\n\tfont-weight: {weight} !important;\n}}\n\n"
            );
        }
    } else {
        _ = write!(
            result,
            "@font-face {{\n\tfont-family: \"{font_name}\";\n\tfont-display: block;\n\tsrc: {src};\n}}\n\n"
        );
        _ = write!(result, "{base_selector} {{\n\tline-height: 1;\n}}\n\n");
        _ = write!(
            result,
            "{base_selector}:before {{\n\tfont-family: {font_name} !important;\n\tfont-style: normal;\n\tfont-weight: normal !important;\n\tvertical-align: top;\n}}\n\n"
        );
    }

    if let Some(codepoints) = codepoints {
        for (name, value) in codepoints {
            let code = value.as_str().unwrap_or("");
            _ = write!(
                result,
                ".{class_prefix}{name}:before {{\n\tcontent: \"\\{code}\";\n}}\n"
            );
        }
    }

    result
}