use std::{
collections::{BTreeMap, BTreeSet},
env, fs,
path::{Path, PathBuf},
};
use serde::Deserialize;
#[derive(Deserialize)]
struct GlyphRecord {
char: String,
code: String,
}
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing manifest dir"));
let glyphs_path = manifest_dir.join("glyphs.json");
println!("cargo:rerun-if-changed={}", glyphs_path.display());
let glyphs = load_glyphs(&glyphs_path);
validate_glyphs(&glyphs);
let generated = render_lookup_source(&glyphs);
let out_path = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR")).join("icons.rs");
fs::write(out_path, generated).expect("failed to write generated icons source");
}
fn load_glyphs(path: &Path) -> BTreeMap<String, GlyphRecord> {
let contents = fs::read_to_string(path).expect("failed to read glyphs.json");
let json: serde_json::Value =
serde_json::from_str(&contents).expect("failed to parse glyphs.json");
let object = json
.as_object()
.expect("glyphs.json must contain a top-level object");
object
.iter()
.filter_map(|(name, value)| {
let parsed = serde_json::from_value::<GlyphRecord>(value.clone()).ok()?;
Some((name.clone(), parsed))
})
.collect()
}
fn validate_glyphs(glyphs: &BTreeMap<String, GlyphRecord>) {
let mut normalized_names = BTreeMap::<String, BTreeSet<String>>::new();
for (name, glyph) in glyphs {
let codepoint = parse_codepoint(name, &glyph.code);
let expected_char = char::from_u32(codepoint)
.unwrap_or_else(|| panic!("invalid Unicode scalar value for {name}: 0x{codepoint:X}"));
let actual_char = parse_glyph_char(name, &glyph.char);
assert_eq!(
actual_char, expected_char,
"glyph snapshot mismatch for {name}: char {:?} does not match code {}",
glyph.char, glyph.code
);
let canonical_name = name.replace('_', "-");
let alias_name = format!("nf-{canonical_name}");
record_normalized_name(&mut normalized_names, name, &canonical_name);
record_normalized_name(&mut normalized_names, name, &alias_name);
}
for (normalized, originals) in normalized_names {
if originals.len() > 1 {
panic!(
"normalized glyph name collision for {normalized}: {}",
originals.into_iter().collect::<Vec<_>>().join(", ")
);
}
}
}
fn parse_codepoint(name: &str, code: &str) -> u32 {
u32::from_str_radix(code, 16).unwrap_or_else(|_| panic!("invalid codepoint for {name}: {code}"))
}
fn parse_glyph_char(name: &str, glyph: &str) -> char {
let mut chars = glyph.chars();
let ch = chars
.next()
.unwrap_or_else(|| panic!("missing glyph character for {name}"));
assert!(
chars.next().is_none(),
"glyph character for {name} must contain exactly one Unicode scalar value"
);
ch
}
fn record_normalized_name(
normalized_names: &mut BTreeMap<String, BTreeSet<String>>,
source_name: &str,
candidate: &str,
) {
let normalized = normalize_icon_name(candidate);
normalized_names
.entry(normalized)
.or_default()
.insert(source_name.to_string());
}
fn normalize_icon_name(raw: &str) -> String {
let mut normalized = String::with_capacity(raw.len());
let mut last_was_separator = false;
for ch in raw.trim().chars() {
if matches!(ch, ' ' | '_' | '-') {
if !normalized.is_empty() && !last_was_separator {
normalized.push('-');
}
last_was_separator = true;
continue;
}
normalized.extend(ch.to_lowercase());
last_was_separator = false;
}
while normalized.ends_with('-') {
normalized.pop();
}
normalized
}
fn render_lookup_source(glyphs: &BTreeMap<String, GlyphRecord>) -> String {
let mut output =
String::from("fn lookup_icon(name: &str) -> Option<u32> {\n match name {\n");
for (name, glyph) in glyphs {
let hyphen_name = name.replace('_', "-");
let alias_name = format!("nf-{hyphen_name}");
let codepoint = parse_codepoint(name, &glyph.code);
output.push_str(&format!(
" {:?} => Some(0x{:x}),\n",
hyphen_name, codepoint
));
output.push_str(&format!(
" {:?} => Some(0x{:x}),\n",
alias_name, codepoint
));
}
output.push_str(" _ => None,\n }\n}\n");
output
}