#![feature(default_field_values)]
use std::{collections::BTreeSet, fmt::Write as _, fs, path::Path};
use strum::IntoEnumIterator;
use tailwind_fuse::AsTailwindClass;
mod manifest;
fn main() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let dir = root.join("ts/uikit/src/generated");
fs::create_dir_all(&dir).expect("create generated dir");
for (component, exports) in manifest::manifest() {
let mut out = String::from("// AUTO-GENERATED by `cargo run -p ev_lib_gen` — do not edit.\n\n");
for (i, ts) in exports.iter().enumerate() {
if i > 0 {
out.push('\n');
}
ts.render(&mut out);
}
let path = dir.join(format!("{component}.ts"));
fs::write(&path, out).expect("write generated component file");
println!("wrote {}", path.display());
}
write_class_inventory(&root);
write_stylesheets(&root);
}
fn write_stylesheets(root: &Path) {
let dir = root.join("rust/classes/css");
fs::create_dir_all(&dir).expect("create css dir");
for name in ["tokens.css", "tokens-legacy.css"] {
let flat = flatten_css(root, name);
let path = dir.join(name);
fs::write(
&path,
format!("/* AUTO-GENERATED by `cargo run -p ev_lib_gen` from the repo-root {name} — do not edit. */\n{flat}"),
)
.expect("write stylesheet");
println!("wrote {}", path.display());
}
}
fn flatten_css(root: &Path, name: &str) -> String {
let src = fs::read_to_string(root.join(name)).unwrap_or_else(|e| panic!("read {name}: {e}"));
src.lines()
.map(|line| match line.trim().strip_prefix("@import \"./").and_then(|r| r.strip_suffix("\";")) {
Some(target) => flatten_css(root, target),
None => format!("{line}\n"),
})
.collect()
}
fn write_class_inventory(root: &Path) {
let mut tabled = BTreeSet::new();
for (_, exports) in manifest::manifest() {
for ts in &exports {
ts.collect_classes(&mut tabled);
}
}
let mut lines = tabled;
for entry in fs::read_dir(root.join("rust/src/uikit")).expect("read uikit dir") {
let path = entry.expect("dir entry").path();
if path.extension().is_none_or(|e| e != "rs") {
continue;
}
let src = fs::read_to_string(&path).expect("read component");
for (i, piece) in src.split('"').enumerate() {
if i % 2 == 1 && looks_like_classes(piece) {
lines.insert(piece.to_string());
}
}
}
let path = root.join("rust/classes/uikit-classes.txt");
let body: String = lines.into_iter().map(|l| format!("{l}\n")).collect();
fs::write(&path, format!("# AUTO-GENERATED by `cargo run -p ev_lib_gen` — do not edit.\n{body}")).expect("write class inventory");
println!("wrote {}", path.display());
}
enum Ts {
Const {
name: &'static str,
value: &'static str,
},
Table {
name: &'static str,
ty: &'static str,
entries: Vec<(String, String)>,
},
}
impl Ts {
fn render(&self, out: &mut String) {
match self {
Ts::Const { name, value } => writeln!(out, "export const {name} = {value:?};").unwrap(),
Ts::Table { name, ty, entries } => {
writeln!(out, "export const {name} = {{").unwrap();
for (key, class) in entries {
writeln!(out, " {key:?}: {class:?},").unwrap();
}
out.push_str("} as const;\n");
writeln!(out, "export type {ty} = keyof typeof {name};").unwrap();
}
}
}
fn collect_classes(&self, out: &mut BTreeSet<String>) {
match self {
Ts::Const { value, .. } => {
out.insert(value.to_string());
}
Ts::Table { entries, .. } => out.extend(entries.iter().map(|(_, class)| class.clone())),
}
}
}
fn table<T>(name: &'static str, ty: &'static str) -> Ts
where
T: IntoEnumIterator + AsRef<str> + AsTailwindClass, {
Ts::Table {
name,
ty,
entries: T::iter().map(|v| (v.as_ref().to_string(), v.as_class().to_string())).collect(),
}
}
fn looks_like_classes(s: &str) -> bool {
let s = s.trim();
!s.is_empty()
&& !s.contains([';', '<', '\n'])
&& !s.starts_with("--")
&& !s.starts_with('#')
&& s.split_whitespace()
.all(|t| t.chars().next().is_some_and(|c| c.is_ascii_alphanumeric() || c == '-' || c == '[') && (t.contains('-') || t.contains(':') || t.contains('[')))
}