tessera-components 0.0.0

Basic components for tessera-ui, using md3e design principles.
Documentation
use std::{
    collections::HashSet,
    env,
    error::Error,
    fmt::Write as _,
    fs,
    path::{Path, PathBuf},
};

const STYLES: &[(&str, &str)] = &[
    ("filled", "filled"),
    ("outlined", "outlined"),
    ("round", "round"),
    ("sharp", "sharp"),
    ("two-tone", "two_tone"),
];

struct IconEntry {
    style: &'static str,
    func: String,
    doc_name: String,
    offset: u32,
    len: u32,
}

fn main() -> Result<(), Box<dyn Error>> {
    println!("cargo:rerun-if-changed=assets/material_icons");

    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
    let assets_dir = manifest_dir.join("assets").join("material_icons");
    let out_dir = PathBuf::from(env::var("OUT_DIR")?);

    let mut blob: Vec<u8> = Vec::new();
    let mut entries: Vec<IconEntry> = Vec::new();

    for (style, _module) in STYLES {
        let style_dir = assets_dir.join(style);
        let mut files = read_svg_entries(&style_dir)?;
        files.sort_by_key(|a| a.file_name());
        let mut used_funcs = HashSet::new();

        for file in files {
            let path = file.path();
            let name = path
                .file_stem()
                .expect("SVG entry missing a file stem")
                .to_string_lossy()
                .into_owned();
            let func = make_func_name(&name, &mut used_funcs);
            let bytes = fs::read(&path)?;
            let offset = blob.len() as u32;
            blob.extend_from_slice(&bytes);
            entries.push(IconEntry {
                style,
                func,
                doc_name: name,
                offset,
                len: bytes.len() as u32,
            });
        }
    }

    fs::write(out_dir.join("material_icons.bin"), &blob)?;

    let mut generated = String::new();
    writeln!(
        generated,
        "// @generated by tessera-components/build.rs; do not edit."
    )?;
    writeln!(generated, "use std::sync::Arc;")?;
    writeln!(generated, "use crate::icon::IconContent;")?;
    writeln!(generated, "use crate::material_icons::load_icon_bytes;")?;
    writeln!(
        generated,
        "use crate::pipelines::image_vector::command::ImageVectorData;"
    )?;

    writeln!(
        generated,
        "/// Concatenated SVG payloads for all bundled Material icons."
    )?;
    writeln!(
        generated,
        "#[allow(missing_docs)] pub static ICON_BLOB: &[u8] = include_bytes!(concat!(env!(\"OUT_DIR\"), \"/material_icons.bin\"));"
    )?;

    for (style, module) in STYLES {
        let style_entries: Vec<_> = entries.iter().filter(|e| e.style == *style).collect();

        writeln!(
            generated,
            "/// Material Design `{style}` style icons as content helpers."
        )?;
        writeln!(generated, "pub mod {module} {{")?;
        writeln!(generated, "    use super::*;")?;
        for entry in &style_entries {
            let func = &entry.func;
            writeln!(generated, "    /// # {func}")?;
            writeln!(
                generated,
                "    ///\n    /// Return the Material Design `{raw}` icon content in the `{style}` style.",
                raw = entry.doc_name,
                style = style
            )?;
            writeln!(
                generated,
                "    ///\n    /// ## Usage\n    ///\n    /// Use with [`crate::icon::IconArgs`] and [`crate::icon::icon`]."
            )?;
            writeln!(generated, "    ///")?;
            writeln!(generated, "    #[inline]")?;
            writeln!(generated, "    pub fn {func}() -> IconContent {{")?;
            writeln!(
                generated,
                "        let data: Arc<ImageVectorData> = load_icon_bytes(\"{style}\", {offset}, {len});",
                style = style,
                offset = entry.offset,
                len = entry.len
            )?;
            writeln!(generated, "        IconContent::from(data)")?;
            writeln!(generated, "    }}")?;
        }

        writeln!(generated, "}}")?;
    }

    fs::write(out_dir.join("material_icons.rs"), generated)?;
    Ok(())
}

fn read_svg_entries(dir: &Path) -> Result<Vec<fs::DirEntry>, Box<dyn Error>> {
    let mut entries = Vec::new();
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        if entry
            .path()
            .extension()
            .map(|ext| ext.eq_ignore_ascii_case("svg"))
            .unwrap_or(false)
        {
            entries.push(entry);
        }
    }
    Ok(entries)
}

fn make_func_name(name: &str, used: &mut HashSet<String>) -> String {
    let mut parts = Vec::new();
    let mut current = String::new();
    for ch in name.chars() {
        if ch.is_ascii_alphanumeric() {
            current.push(ch);
        } else if !current.is_empty() {
            parts.push(current.clone());
            current.clear();
        }
    }
    if !current.is_empty() {
        parts.push(current);
    }
    if parts.is_empty() {
        parts.push("icon".to_string());
    }

    let mut func = parts.join("_").to_ascii_lowercase();
    func.push_str("_icon");
    if func
        .chars()
        .next()
        .map(|c| c.is_ascii_digit())
        .unwrap_or(false)
    {
        func.insert_str(0, "icon_");
    }
    if is_keyword(&func) {
        func.insert_str(0, "r#");
    }
    let mut suffix = 2;
    while used.contains(&func) {
        let candidate = format!("{func}_{suffix}");
        suffix += 1;
        if !used.contains(&candidate) {
            func = candidate;
            break;
        }
    }
    used.insert(func.clone());
    func
}

fn is_keyword(s: &str) -> bool {
    matches!(
        s,
        "as" | "break"
            | "const"
            | "continue"
            | "crate"
            | "else"
            | "enum"
            | "extern"
            | "false"
            | "fn"
            | "for"
            | "if"
            | "impl"
            | "in"
            | "let"
            | "loop"
            | "match"
            | "mod"
            | "move"
            | "mut"
            | "pub"
            | "ref"
            | "return"
            | "Self"
            | "self"
            | "static"
            | "struct"
            | "super"
            | "trait"
            | "true"
            | "type"
            | "unsafe"
            | "use"
            | "where"
            | "while"
            | "async"
            | "await"
            | "dyn"
            | "abstract"
            | "become"
            | "box"
            | "do"
            | "final"
            | "macro"
            | "override"
            | "priv"
            | "typeof"
            | "unsized"
            | "virtual"
            | "yield"
            | "try"
    )
}