1use ishou_tokens::{Rgb, TokenSet};
17
18fn role(t: &TokenSet, name: &str) -> String {
22 let key = t
23 .roles
24 .pairs()
25 .into_iter()
26 .find(|(r, _)| *r == name)
27 .map(|(_, k)| k)
28 .unwrap_or("background");
29 palette(t, key)
30}
31
32fn palette(t: &TokenSet, key: &str) -> String {
34 t.color
35 .get(key)
36 .map(|c: Rgb| c.hex())
37 .unwrap_or_else(|| "#000000".to_string())
38}
39
40#[must_use]
42pub fn render(t: &TokenSet) -> String {
43 let pairs: [(&str, String); 34] = [
46 ("primary", role(t, "primary")),
47 ("on-primary", role(t, "background")),
48 ("primary-container", role(t, "structural")),
49 ("on-primary-container", role(t, "text")),
50 ("secondary", role(t, "structural")),
51 ("on-secondary", role(t, "background")),
52 ("secondary-container", role(t, "surface-elevated")),
53 ("on-secondary-container", role(t, "text")),
54 ("tertiary", role(t, "accent")),
55 ("on-tertiary", role(t, "background")),
56 ("tertiary-container", role(t, "surface-elevated")),
57 ("on-tertiary-container", role(t, "text")),
58 ("error", role(t, "error")),
59 ("on-error", role(t, "background")),
60 ("error-container", role(t, "surface-elevated")),
61 ("on-error-container", role(t, "text")),
62 ("background", role(t, "background")),
63 ("on-background", role(t, "text")),
64 ("surface", role(t, "surface")),
65 ("on-surface", role(t, "text")),
66 ("surface-variant", role(t, "surface-elevated")),
67 ("on-surface-variant", role(t, "text-muted")),
68 ("outline", role(t, "text-dim")),
69 ("outline-variant", role(t, "surface-elevated")),
70 ("inverse-surface", role(t, "text")),
71 ("inverse-on-surface", role(t, "background")),
72 ("inverse-primary", role(t, "structural")),
73 ("surface-dim", role(t, "background")),
74 ("surface-bright", role(t, "text-dim")),
75 ("surface-container-lowest", palette(t, "ink")),
76 ("surface-container-low", role(t, "background")),
77 ("surface-container", role(t, "surface")),
78 ("surface-container-high", role(t, "surface-elevated")),
79 ("surface-container-highest", role(t, "text-dim")),
80 ];
81
82 let mut out =
83 String::from("/* ishou — Material Design 3 system colors (generated; do not edit) */\n\n");
84 out.push_str(":root {\n");
85 for (suffix, hex) in &pairs {
86 out.push_str(&format!(" --md-sys-color-{suffix}: {hex};\n"));
87 }
88 out.push_str("}\n");
89 out
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 const MD3_SYSTEM_COLORS: [&str; 34] = [
98 "primary", "on-primary", "primary-container", "on-primary-container",
99 "secondary", "on-secondary", "secondary-container", "on-secondary-container",
100 "tertiary", "on-tertiary", "tertiary-container", "on-tertiary-container",
101 "error", "on-error", "error-container", "on-error-container",
102 "background", "on-background", "surface", "on-surface",
103 "surface-variant", "on-surface-variant", "outline", "outline-variant",
104 "inverse-surface", "inverse-on-surface", "inverse-primary",
105 "surface-dim", "surface-bright", "surface-container-lowest",
106 "surface-container-low", "surface-container", "surface-container-high",
107 "surface-container-highest",
108 ];
109
110 #[test]
111 fn emits_every_md3_system_color_pleme_mui_consumes() {
112 let out = render(&TokenSet::pleme());
113 for prop in MD3_SYSTEM_COLORS {
114 let needle = format!("--md-sys-color-{prop}:");
115 assert!(out.contains(&needle), "missing MD3 color: {prop}");
116 }
117 }
118
119 #[test]
120 fn every_role_resolves_to_a_real_token_hex() {
121 let out = render(&TokenSet::pleme());
122 assert!(
124 !out.contains("#000000"),
125 "an MD3 role failed to resolve from ishou tokens"
126 );
127 assert_eq!(out.matches('#').count(), 34, "expected 34 resolved hex values");
129 }
130
131 #[test]
132 fn is_deterministic() {
133 assert_eq!(render(&TokenSet::pleme()), render(&TokenSet::pleme()));
134 }
135
136 #[test]
137 fn steel_theme_renders_the_metallic_surface() {
138 let out = render(&TokenSet::steel());
139 assert!(
141 out.contains("--md-sys-color-primary: #5E8CC4"),
142 "steel primary should be blued-steel"
143 );
144 assert!(out.contains("--md-sys-color-background: #0B0E12"));
146 assert!(!out.contains("#000000"), "steel md3 should fully resolve");
148 }
149}