use std::{
fs::{read_to_string, File},
io::Write,
path::Path,
};
use indoc::formatdoc;
use spacebadgers_utils::minify::minify_svg;
use walkdir::WalkDir;
fn main() {
println!("cargo:rerun-if-changed=vendor/*");
IconSetCompiler::new()
.compile(
"Feather Icons",
"feather_icons",
"feather",
"vendor/feather/icons",
"vendor/feather/LICENSE",
Some(|svg: &str| svg.replace("currentColor", "#fff")),
)
.compile(
"css.gg Icons",
"cssgg_icons",
"cssgg",
"vendor/cssgg/icons/svg",
"vendor/cssgg/LICENSE",
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();
}
struct Icon {
name: String,
svg: String,
}
impl Icon {
fn line(&self) -> String {
let cleaned_svg = minify_svg(&self.svg);
format!(
r###""{name}" => r##"{svg}"##"###,
name = self.name,
svg = cleaned_svg.trim()
)
}
}
struct IconSet {
module: String,
export: String,
}
struct IconSetCompiler {
icon_sets: Vec<IconSet>,
}
impl IconSetCompiler {
fn new() -> Self {
Self {
icon_sets: Vec::new(),
}
}
fn compile(
mut self,
name: impl AsRef<str>,
module: impl AsRef<str>,
prefix: impl AsRef<str>,
icon_path: impl AsRef<Path>,
license_path: impl AsRef<Path>,
post_process: Option<impl Fn(&str) -> String>,
) -> Self {
let prefix = prefix.as_ref();
let module = module.as_ref();
let export = module.to_uppercase().replace([' ', '.'], "_");
let mut icons = Vec::new();
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::<Vec<_>>()
.join("\n");
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,
});
}
}
let hashmap_lines = icons
.into_iter()
.map(|icon| format!(" {line}", line = icon.line()))
.collect::<Vec<_>>()
.join(",\n");
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(),
};
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"));
self.icon_sets.push(IconSet {
module: module.to_string(),
export,
});
self
}
fn finalize(self) {
let modules = self
.icon_sets
.iter()
.map(|set| format!("#[rustfmt::skip]\npub mod {};", set.module))
.collect::<Vec<_>>()
.join("\n");
let reexports = self
.icon_sets
.iter()
.map(|set| format!("#[rustfmt::skip]\npub use {}::{};", set.module, set.export))
.collect::<Vec<_>>()
.join("\n");
let all_icon_sets = self
.icon_sets
.iter()
.map(|set| format!("&{}", set.export))
.collect::<Vec<_>>()
.join(", ");
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<str>) -> Option<&'static str> {{
let name = name.as_ref();
ALL_ICON_SETS.iter().find_map(|icon_set| icon_set.get(name))
}}
"###};
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");
}
}