use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use serde_json::Value;
use sha2::{Digest, Sha256};
fn main() {
let manifest_dir = PathBuf::from(
env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo for build scripts"),
);
let workspace_root = find_workspace_root(&manifest_dir).unwrap_or_else(|| {
panic!(
"could not locate the DOMicile workspace root by walking up from {}",
manifest_dir.display()
)
});
println!("cargo:rerun-if-changed={}", workspace_root.join("Cargo.toml").display());
let tokens_json_path = workspace_root.join("tokens").join("tokens.json");
println!("cargo:rerun-if-changed={}", tokens_json_path.display());
let tokens_bytes = fs::read(&tokens_json_path)
.unwrap_or_else(|e| panic!("read tokens/tokens.json ({:?}): {}", tokens_json_path, e));
let tokens: Value = serde_json::from_slice(&tokens_bytes)
.expect("tokens/tokens.json parses as JSON");
let mut out = String::new();
out.push_str("// Generated by build.rs from tokens/tokens.json -- do not edit by hand.\n\n");
if let Some(color) = tokens.get("color") {
if let Some(primary) = color.get("primary") {
if let Some(gradient) = primary.get("gradient").and_then(|v| v.as_array()) {
let stops: Vec<String> = gradient.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
let stop_count = stops.len();
out.push_str(&format!(
"pub const COLOR_PRIMARY_GRADIENT: &[&str; {stop_count}] = &[{}];\n",
stops.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(", ")
));
}
if let Some(angle) = primary.get("angle").and_then(|v| v.as_str()) {
out.push_str(&format!("pub const COLOR_PRIMARY_ANGLE: &str = \"{angle}\";\n"));
}
}
if let Some(secondary) = color.get("secondary") {
for (name, hex) in secondary.as_object().into_iter().flatten() {
if let Some(hex_str) = hex.as_str() {
let ident = to_const_ident(&format!("secondary_{name}"));
out.push_str(&format!("pub const {ident}: &str = \"{hex_str}\";\n"));
}
}
}
if let Some(accent) = color.get("accent") {
for (name, hex) in accent.as_object().into_iter().flatten() {
if let Some(hex_str) = hex.as_str() {
let ident = to_const_ident(&format!("accent_{name}"));
out.push_str(&format!("pub const {ident}: &str = \"{hex_str}\";\n"));
}
}
}
if let Some(text) = color.get("text") {
for (name, hex) in text.as_object().into_iter().flatten() {
if let Some(hex_str) = hex.as_str() {
let ident = to_const_ident(&format!("text_{name}"));
out.push_str(&format!("pub const {ident}: &str = \"{hex_str}\";\n"));
}
}
}
if let Some(surface) = color.get("surface") {
for (name, hex) in surface.as_object().into_iter().flatten() {
if let Some(hex_str) = hex.as_str() {
let ident = to_const_ident(&format!("surface_{name}"));
out.push_str(&format!("pub const {ident}: &str = \"{hex_str}\";\n"));
}
}
}
}
if let Some(radius) = tokens.get("radius") {
emit_px_consts(&mut out, "radius", radius);
}
if let Some(space) = tokens.get("space") {
emit_px_consts(&mut out, "space", space);
}
if let Some(border) = tokens.get("border") {
if let Some(obj) = border.as_object() {
for (name, val) in obj {
let ident = to_const_ident(&format!("border_{name}"));
if val.is_string() {
out.push_str(&format!("pub const {ident}: &str = \"{}\";\n", val.as_str().unwrap()));
}
}
}
}
if let Some(shadow) = tokens.get("shadow") {
if let Some(obj) = shadow.as_object() {
for (name, val) in obj {
let ident = to_const_ident(&format!("shadow_{name}"));
if val.is_string() {
out.push_str(&format!("pub const {ident}: &str = \"{}\";\n", val.as_str().unwrap()));
}
}
}
}
let mut hasher = Sha256::new();
hasher.update(&tokens_bytes);
let sha = format!("{:x}", hasher.finalize());
out.push_str(&format!("pub const TOKENS_JSON_SHA256: &str = \"{sha}\";\n"));
let out_dir: PathBuf = env::var_os("OUT_DIR").expect("OUT_DIR set").into();
let gen_dir = out_dir.join("generated");
fs::create_dir_all(&gen_dir).expect("mkdir OUT_DIR/generated");
fs::write(gen_dir.join("tokens.rs"), out).expect("write tokens.rs");
}
fn emit_px_consts(out: &mut String, group: &str, value: &Value) {
if let Some(obj) = value.as_object() {
for (name, val) in obj {
let parsed: Option<f32> = val.as_f64().map(|n| n as f32)
.or_else(|| val.as_str().and_then(parse_px_str));
if let Some(px) = parsed {
let ident = to_const_ident(&format!("{group}_{name}"));
out.push_str(&format!("pub const {ident}: f32 = {px:.1};\n"));
}
}
}
}
fn parse_px_str(s: &str) -> Option<f32> {
let trimmed = s.trim();
let numeric = trimmed.trim_end_matches("px").trim();
numeric.parse::<f32>().ok()
}
fn to_const_ident(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for ch in input.chars() {
if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch.is_ascii_digit() {
out.push(ch.to_ascii_uppercase());
} else if ch == '_' || ch == '-' {
out.push('_');
}
}
out
}
fn find_workspace_root(start: &Path) -> Option<PathBuf> {
let mut current = start.parent().map(PathBuf::from);
while let Some(dir) = current {
let manifest = dir.join("Cargo.toml");
if manifest.is_file() && has_workspace_table(&manifest) {
return Some(dir);
}
current = dir.parent().map(PathBuf::from);
}
None
}
fn has_workspace_table(manifest: &Path) -> bool {
fs::read_to_string(manifest)
.map(|s| s.contains("[workspace]"))
.unwrap_or(false)
}