use std::collections::HashSet;
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
pub fn generate(assets_dir: impl AsRef<Path>) {
generate_with(assets_dir, "rosace::asset::Asset");
}
pub fn generate_with(assets_dir: impl AsRef<Path>, asset_type_path: &str) {
let dir = assets_dir.as_ref();
println!("cargo:rerun-if-changed={}", dir.display());
let out = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR set by cargo"))
.join("rosace_assets.rs");
let mut body = String::new();
if dir.is_dir() {
emit_dir(dir, dir, asset_type_path, 0, &mut body);
}
fs::write(&out, body).expect("write generated assets module");
}
fn emit_dir(root: &Path, dir: &Path, ty: &str, depth: usize, body: &mut String) {
let indent = " ".repeat(depth);
let mut entries: Vec<PathBuf> = match fs::read_dir(dir) {
Ok(rd) => rd.flatten().map(|e| e.path()).collect(),
Err(_) => return,
};
entries.sort();
let mut used_consts: HashSet<String> = HashSet::new();
let mut used_mods: HashSet<String> = HashSet::new();
for path in &entries {
let file_name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n,
None => continue,
};
if file_name.starts_with('.') {
continue;
}
if path.is_dir() {
let mod_name = unique(&mod_ident(file_name), &mut used_mods);
let _ = writeln!(body, "{indent}#[allow(non_snake_case)]");
let _ = writeln!(body, "{indent}pub mod {mod_name} {{");
emit_dir(root, path, ty, depth + 1, body);
let _ = writeln!(body, "{indent}}}");
} else {
let rel = path
.strip_prefix(root)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/");
let const_name = unique(&const_ident(file_name), &mut used_consts);
let _ = writeln!(
body,
"{indent}/// `{rel}`\n{indent}pub const {const_name}: {ty} = {ty}::new(\"{rel}\");"
);
}
}
}
fn const_ident(file_name: &str) -> String {
let stem = file_name.rsplit_once('.').map(|(s, _)| s).unwrap_or(file_name);
sanitize(stem, true)
}
fn mod_ident(dir_name: &str) -> String {
sanitize(dir_name, false)
}
fn sanitize(s: &str, upper: bool) -> String {
let mut out = String::new();
for ch in s.chars() {
if ch.is_ascii_alphanumeric() {
out.push(if upper { ch.to_ascii_uppercase() } else { ch.to_ascii_lowercase() });
} else {
out.push('_');
}
}
if out.is_empty() {
out.push('_');
}
if out.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
out.insert(0, '_');
}
out
}
fn unique(base: &str, used: &mut HashSet<String>) -> String {
if used.insert(base.to_string()) {
return base.to_string();
}
let mut n = 2;
loop {
let candidate = format!("{base}_{n}");
if used.insert(candidate.clone()) {
return candidate;
}
n += 1;
}
}