use std::{ fs::{read_to_string, File}, io::Write, path::Path, }; use indoc::formatdoc; use spacebadgers_utils::minify::minify_svg; use walkdir::WalkDir; /// Main entry point for the build script. fn main() { println!("cargo:rerun-if-changed=vendor/*"); IconSetCompiler::new() .compile( "Feather Icons", "feather_icons", "feather", "vendor/feather/icons", "vendor/feather/LICENSE", // Feather icons use `currentColor` for strokes, which doesn't work in our case. // We embed the code as a base64 data URI, so we need to replace `currentColor`. Some(|svg: &str| svg.replace("currentColor", "#fff")), ) .compile( "css.gg Icons", "cssgg_icons", "cssgg", "vendor/cssgg/icons/svg", "vendor/cssgg/LICENSE", // css.gg icons use `currentColor` for strokes, which doesn't work in our case. // We embed the code as a base64 data URI, so we need to replace `currentColor`. Some(|svg: &str| svg.replace("currentColor", "#fff")), ) .compile( "Eva Icons / Filled", "eva_icons_fill", "eva", "vendor/eva/package/icons/fill/svg", "vendor/eva/LICENSE.txt", Some(|svg: &str| svg.replace("#231f20", "#fff")), ) .compile( "Eva Icons / Outlined", "eva_icons_outline", "eva", "vendor/eva/package/icons/outline/svg", "vendor/eva/LICENSE.txt", Some(|svg: &str| svg.replace("#231f20", "#fff")), ) .finalize(); } /// A single icon entry. /// Used for generating the icon hashmap. struct Icon { name: String, svg: String, } impl Icon { /// Generate a phf map entry for this icon. fn line(&self) -> String { let cleaned_svg = minify_svg(&self.svg); format!( r###""{name}" => r##"{svg}"##"###, name = self.name, svg = cleaned_svg.trim() ) } } /// Basic information about an icon set. /// Used for generating module declarations and exports. struct IconSet { module: String, export: String, } /// Icon set compiler. struct IconSetCompiler { icon_sets: Vec, } impl IconSetCompiler { /// Create a new icon set compiler. fn new() -> Self { Self { icon_sets: Vec::new(), } } /// Compile an icon set to a Rust module. fn compile( mut self, name: impl AsRef, module: impl AsRef, prefix: impl AsRef, icon_path: impl AsRef, license_path: impl AsRef, post_process: Option String>, ) -> Self { let prefix = prefix.as_ref(); let module = module.as_ref(); let export = module.to_uppercase().replace([' ', '.'], "_"); let mut icons = Vec::new(); // Read and format the license let license = read_to_string(&license_path) .expect(&format!( "Unable to read license file: {:?}", license_path.as_ref() )) .split("\n") .map(|line| format!("//! {line}")) .collect::>() .join("\n"); // Find all SVG files for entry in WalkDir::new(icon_path).into_iter().filter_map(Result::ok) { let path = entry.path(); if path.is_file() && path.extension().map(|e| e == "svg").unwrap_or(false) { let icon_name = path .file_stem() .expect(&format!("Unable to get file stem for file: {:?}", path)) .to_string_lossy(); let icon_name = format!("{prefix}-{icon_name}"); let icon_svg = read_to_string(path).expect(&format!("Unable to read file: {:?}", path)); let icon_svg = post_process .as_ref() .map(|f| f(&icon_svg)) .unwrap_or(icon_svg); icons.push(Icon { name: icon_name, svg: icon_svg, }); } } // Generate hashmap entries let hashmap_lines = icons .into_iter() .map(|icon| format!(" {line}", line = icon.line())) .collect::>() .join(",\n"); // Generate code let code = formatdoc! {r###" //! THIS FILE IS AUTO-GENERATED BY `build.rs`. //! DO NOT EDIT THIS FILE DIRECTLY. //! //! ## License //! ```plain,no_run {license} //! ``` use phf::phf_map; use super::IconSet; pub const {export}: IconSet = IconSet {{ name: "{name}", icons: phf_map! {{ {hashmap_lines} }}, }}; "###, name = name.as_ref(), }; // Write to file File::options() .write(true) .create(true) .truncate(true) .open(format!("src/icons/{module}.rs")) .expect(&format!( "Unable to open/create file: src/icons/{module}.rs" )) .write_all(code.trim().as_bytes()) .expect(&format!("Unable to write to file: src/icons/{module}.rs")); // Register for finalization self.icon_sets.push(IconSet { module: module.to_string(), export, }); self } fn finalize(self) { // Generate module declarations let modules = self .icon_sets .iter() .map(|set| format!("#[rustfmt::skip]\npub mod {};", set.module)) .collect::>() .join("\n"); // Generate reexports let reexports = self .icon_sets .iter() .map(|set| format!("#[rustfmt::skip]\npub use {}::{};", set.module, set.export)) .collect::>() .join("\n"); // Generate list of all icon sets let all_icon_sets = self .icon_sets .iter() .map(|set| format!("&{}", set.export)) .collect::>() .join(", "); // Generate icons.rs let code = formatdoc! {r###" //! THIS FILE IS AUTO-GENERATED BY `build.rs`. //! DO NOT EDIT THIS FILE DIRECTLY. pub mod icon_set; {modules} pub use icon_set::IconSet; {reexports} /// All available icon sets. #[rustfmt::skip] pub const ALL_ICON_SETS: &[&IconSet] = &[{all_icon_sets}]; /// Get the code for a named icon. pub fn get_icon_svg(name: impl AsRef) -> Option<&'static str> {{ let name = name.as_ref(); ALL_ICON_SETS.iter().find_map(|icon_set| icon_set.get(name)) }} "###}; // Write to file File::options() .write(true) .create(true) .truncate(true) .open(format!("src/icons.rs")) .expect("Unable to open/create file: src/icons.rs") .write_all(code.as_bytes()) .expect("Unable to write to file: src/icons.rs"); } }