webfont-generator 0.11.0

Generate webfonts (SVG, TTF, EOT, WOFF, WOFF2) from SVG icons
Documentation
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct TemplateDependencies {
    pub names: bool,
    pub codepoints: bool,
    pub src: bool,
    pub styles: bool,
    pub dynamic: bool,
}

impl TemplateDependencies {
    pub(crate) const fn css_default() -> Self {
        Self {
            names: false,
            codepoints: true,
            src: true,
            styles: false,
            dynamic: false,
        }
    }

    pub(crate) const fn html_default() -> Self {
        Self {
            names: true,
            codepoints: false,
            src: false,
            styles: true,
            dynamic: false,
        }
    }

    #[cfg(feature = "fs")]
    pub(crate) const fn may_depend_on_src(self) -> bool {
        self.src || self.dynamic
    }

    pub(crate) const fn can_reuse_css_no_urls(
        self,
        names_unchanged: bool,
        codepoints_unchanged: bool,
    ) -> bool {
        !self.dynamic
            && !self.src
            && (!self.names || names_unchanged)
            && (!self.codepoints || codepoints_unchanged)
    }

    #[cfg(feature = "fs")]
    pub(crate) const fn can_reuse_css_with_urls(
        self,
        names_unchanged: bool,
        codepoints_unchanged: bool,
    ) -> bool {
        !self.dynamic
            && (!self.names || names_unchanged)
            && (!self.codepoints || codepoints_unchanged)
    }

    pub(crate) const fn can_reuse_html(
        self,
        names_unchanged: bool,
        codepoints_unchanged: bool,
        styles_unchanged: bool,
    ) -> bool {
        !self.dynamic
            && (!self.names || names_unchanged)
            && (!self.codepoints || codepoints_unchanged)
            && (!self.styles || styles_unchanged)
    }
}

pub(crate) fn template_dependencies(source: &str) -> TemplateDependencies {
    let mut deps = TemplateDependencies::default();
    for expression in mustache_expressions(source) {
        collect_expression_dependencies(expression, &mut deps);
        if deps.dynamic {
            break;
        }
    }
    deps
}

fn mustache_expressions(mut source: &str) -> Vec<&str> {
    let mut expressions = Vec::new();
    loop {
        let Some(open) = source.find("{{") else {
            return expressions;
        };
        source = &source[open + 2..];
        let (inner_start, close_pat) = match source.strip_prefix('{') {
            Some(rest) => (rest, "}}}"),
            None => (source, "}}"),
        };
        let Some(close) = inner_start.find(close_pat) else {
            return expressions;
        };
        expressions.push(inner_start[..close].trim());
        source = &inner_start[close + close_pat.len()..];
    }
}

fn collect_expression_dependencies(expression: &str, deps: &mut TemplateDependencies) {
    let expression = expression.trim().trim_matches('~').trim();
    if expression.is_empty() || expression.starts_with('!') || expression.starts_with('/') {
        return;
    }
    if matches!(expression, "." | "this" | "@root") || expression.starts_with('>') {
        deps.dynamic = true;
        return;
    }
    if expression.contains('[') || expression.contains(']') {
        deps.dynamic = true;
        return;
    }
    if expression.contains('|') {
        deps.dynamic = true;
        return;
    }
    let tokens = expression.split_whitespace().collect::<Vec<_>>();
    let Some(first) = tokens.first().copied() else {
        return;
    };
    let first = first.trim_start_matches(['#', '^']);
    if first.starts_with('>') {
        deps.dynamic = true;
        return;
    }
    if tokens.iter().any(|token| is_lookup_token(token)) {
        deps.dynamic = true;
        return;
    }
    if matches!(first, "each" | "if" | "unless" | "with") {
        for token in tokens.iter().skip(1) {
            collect_path_dependency(token, deps);
        }
        return;
    }
    if tokens.len() == 1 {
        collect_path_dependency(first, deps);
        return;
    }
    if first == "removePeriods" {
        for token in tokens.iter().skip(1) {
            collect_path_dependency(token, deps);
        }
        return;
    }
    deps.dynamic = true;
}

fn is_lookup_token(token: &str) -> bool {
    token
        .trim_matches(|c| matches!(c, '(' | ')' | ',' | '"' | '\'' | '~'))
        .trim_start_matches(['#', '^'])
        == "lookup"
}

fn collect_path_dependency(path: &str, deps: &mut TemplateDependencies) {
    let mut path = path
        .trim_matches(|c| matches!(c, '(' | ')' | ',' | '"' | '\'' | '~'))
        .trim_start_matches('&')
        .trim_start_matches("../")
        .trim_start_matches("./");
    path = path
        .strip_prefix("@root.")
        .or_else(|| path.strip_prefix("@root/"))
        .unwrap_or(path);
    path = path
        .strip_prefix("this.")
        .or_else(|| path.strip_prefix("this/"))
        .unwrap_or(path);
    if matches!(path, "" | "." | "this" | "@root") {
        deps.dynamic = true;
        return;
    }
    if path.starts_with('@') {
        return;
    }
    let root = path.split(['.', '/']).next().unwrap_or(path);
    match root {
        "names" => deps.names = true,
        "codepoints" => deps.codepoints = true,
        "src" => deps.src = true,
        "styles" => deps.styles = true,
        _ => {}
    }
}