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"
)
}