Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
127 lines
4.3 KiB
Rust
127 lines
4.3 KiB
Rust
//! Tokens → Tailwind config (TypeScript) + CSS custom-property emission.
|
|
//! Emits stable, deterministically-ordered output (BTreeMap input guarantees).
|
|
|
|
use crate::parse::Tokens;
|
|
use std::fmt::Write as _;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
pub fn tailwind_config(t: &Tokens, out: &Path) -> Result<(), String> {
|
|
let mut s = String::new();
|
|
s.push_str("// GENERATED by tokens-sync — do not edit by hand.\n");
|
|
s.push_str("import type { Config } from 'tailwindcss';\n\n");
|
|
s.push_str("const config: Config = {\n");
|
|
s.push_str(" content: [],\n");
|
|
s.push_str(" theme: {\n extend: {\n");
|
|
|
|
emit_category(&mut s, "colors", &t.colors);
|
|
emit_category(&mut s, "fontFamily", &t.fonts);
|
|
emit_category(&mut s, "spacing", &t.spacing);
|
|
emit_category(&mut s, "borderRadius", &t.radius);
|
|
|
|
s.push_str(" },\n },\n plugins: [],\n};\n\nexport default config;\n");
|
|
|
|
fs::write(out, s).map_err(|e| format!("write {}: {e}", out.display()))
|
|
}
|
|
|
|
pub fn css_vars(t: &Tokens, out: &Path) -> Result<(), String> {
|
|
let mut s = String::new();
|
|
s.push_str("/* GENERATED by tokens-sync — do not edit by hand. */\n:root {\n");
|
|
|
|
for (k, v) in &t.colors {
|
|
let _ = writeln!(s, " --color-{}: {};", css_ident(k), v);
|
|
}
|
|
for (k, v) in &t.fonts {
|
|
let _ = writeln!(s, " --font-{}: {};", css_ident(k), v);
|
|
}
|
|
for (k, v) in &t.spacing {
|
|
let _ = writeln!(s, " --space-{}: {};", css_ident(k), v);
|
|
}
|
|
for (k, v) in &t.radius {
|
|
let _ = writeln!(s, " --radius-{}: {};", css_ident(k), v);
|
|
}
|
|
|
|
s.push_str("}\n");
|
|
|
|
fs::write(out, s).map_err(|e| format!("write {}: {e}", out.display()))
|
|
}
|
|
|
|
fn emit_category(
|
|
s: &mut String,
|
|
key: &str,
|
|
map: &std::collections::BTreeMap<String, String>,
|
|
) {
|
|
if map.is_empty() {
|
|
return;
|
|
}
|
|
let _ = writeln!(s, " {key}: {{");
|
|
for (k, v) in map {
|
|
// Escape JS single quotes inside the value string.
|
|
let escaped = v.replace('\'', "\\'");
|
|
let _ = writeln!(s, " '{k}': '{escaped}',");
|
|
}
|
|
s.push_str(" },\n");
|
|
}
|
|
|
|
fn css_ident(raw: &str) -> String {
|
|
raw.chars()
|
|
.map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '-' })
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::collections::BTreeMap;
|
|
|
|
fn sample_tokens() -> Tokens {
|
|
let mut colors = BTreeMap::new();
|
|
colors.insert("primary".into(), "oklch(0.6 0.2 250)".into());
|
|
colors.insert("surface".into(), "oklch(0.99 0 0)".into());
|
|
let mut fonts = BTreeMap::new();
|
|
fonts.insert("body".into(), "Inter, sans-serif".into());
|
|
let mut spacing = BTreeMap::new();
|
|
spacing.insert("md".into(), "1rem".into());
|
|
let mut radius = BTreeMap::new();
|
|
radius.insert("card".into(), "0.75rem".into());
|
|
Tokens { colors, fonts, spacing, radius }
|
|
}
|
|
|
|
#[test]
|
|
fn tailwind_emits_extend_categories() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let out = dir.path().join("tw.ts");
|
|
tailwind_config(&sample_tokens(), &out).unwrap();
|
|
let text = fs::read_to_string(&out).unwrap();
|
|
assert!(text.contains("colors: {"));
|
|
assert!(text.contains("'primary': 'oklch(0.6 0.2 250)'"));
|
|
assert!(text.contains("fontFamily: {"));
|
|
assert!(text.contains("borderRadius: {"));
|
|
}
|
|
|
|
#[test]
|
|
fn css_emits_vars_under_root() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let out = dir.path().join("tokens.css");
|
|
css_vars(&sample_tokens(), &out).unwrap();
|
|
let text = fs::read_to_string(&out).unwrap();
|
|
assert!(text.contains(":root {"));
|
|
assert!(text.contains("--color-primary: oklch(0.6 0.2 250);"));
|
|
assert!(text.contains("--font-body: Inter, sans-serif;"));
|
|
assert!(text.contains("--space-md: 1rem;"));
|
|
assert!(text.contains("--radius-card: 0.75rem;"));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_tokens_still_emit_valid_shells() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let t = Tokens::default();
|
|
let tw = dir.path().join("tw.ts");
|
|
let css = dir.path().join("c.css");
|
|
tailwind_config(&t, &tw).unwrap();
|
|
css_vars(&t, &css).unwrap();
|
|
let css_text = fs::read_to_string(&css).unwrap();
|
|
assert!(css_text.contains(":root {"));
|
|
assert!(css_text.trim_end().ends_with('}'));
|
|
}
|
|
}
|