1use alloc::collections::BTreeMap;
13use alloc::string::String;
14use alloc::vec::Vec;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Scheme {
20 Light,
21 Dark,
22}
23
24impl Default for Scheme {
25 fn default() -> Self {
26 Scheme::Light
27 }
28}
29
30#[derive(Debug, Clone, PartialEq)]
33pub enum TokenValue {
34 Color(u32),
35 F32(f32),
36 U32(u32),
37 U8(u8),
38 I32(i32),
39 Str(String),
40}
41
42impl TokenValue {
43 pub fn value_type(&self) -> u8 {
45 match self {
46 TokenValue::Color(_) => crate::value_type::COLOR,
47 TokenValue::F32(_) => crate::value_type::F32,
48 TokenValue::U32(_) => crate::value_type::U32,
49 TokenValue::U8(_) => crate::value_type::U8,
50 TokenValue::I32(_) => crate::value_type::I32,
51 TokenValue::Str(_) => crate::value_type::STRING,
52 }
53 }
54
55 pub fn from_wire(value_type: u8, value: u32) -> Option<Self> {
59 match value_type {
60 crate::value_type::COLOR => Some(TokenValue::Color(value)),
61 crate::value_type::F32 => Some(TokenValue::F32(f32::from_bits(value))),
62 crate::value_type::U32 => Some(TokenValue::U32(value)),
63 crate::value_type::U8 => Some(TokenValue::U8((value & 0xFF) as u8)),
64 crate::value_type::I32 => Some(TokenValue::I32(value as i32)),
65 _ => None,
66 }
67 }
68
69 pub fn as_f32(&self) -> Option<f32> {
71 match self {
72 TokenValue::F32(v) => Some(*v),
73 TokenValue::U32(v) => Some(*v as f32),
74 TokenValue::U8(v) => Some(f32::from(*v)),
75 TokenValue::I32(v) => Some(*v as f32),
76 _ => None,
77 }
78 }
79
80 pub fn to_bits(&self) -> Option<u32> {
83 match self {
84 TokenValue::Color(v) => Some(*v),
85 TokenValue::F32(v) => Some(v.to_bits()),
86 TokenValue::U32(v) => Some(*v),
87 TokenValue::U8(v) => Some(u32::from(*v)),
88 TokenValue::I32(v) => Some(*v as u32),
89 TokenValue::Str(_) => None,
90 }
91 }
92}
93
94pub const TIER1_TOKENS: &[&str] = &[
99 "color.primary",
101 "color.secondary",
102 "color.background",
103 "color.surface",
104 "color.text.primary",
105 "color.text.secondary",
106 "color.text.onPrimary",
107 "color.border",
108 "color.accent",
109 "font.body.size",
111 "font.body.weight",
112 "font.body.family",
113 "space.base",
115 "radius.xs",
116 "radius.sm",
117 "radius.md",
118 "radius.lg",
119 "radius.xl",
120 "radius.full",
121 "border.width.thin",
122 "control.background",
124 "control.foreground",
125 "control.border",
126 "control.border.width",
127 "control.radius",
128 "control.padding.horizontal",
129 "control.padding.vertical",
130 "control.height",
131 "control.font.size",
132 "control.font.weight",
133 "control.accent",
134 "button.background",
136 "button.foreground",
137 "button.border",
138 "button.radius",
139 "button.height",
140 "button.font.size",
141 "button.font.weight",
142 "button.accent",
143 "input.background",
145 "input.foreground",
146 "input.placeholder",
147 "input.border",
148 "input.radius",
149 "input.font.size",
150 "toggle.accent",
152 "toggle.track.off",
153 "toggle.track.on",
154 "toggle.thumb",
155 "slider.accent",
156 "slider.track",
157 "slider.thumb",
158 "progress.track",
159 "progress.fill",
160 "gauge.track",
161 "gauge.fill",
162];
163
164#[derive(Debug, Clone, Copy)]
170pub struct TokenTables<'a> {
171 pub overrides: &'a BTreeMap<String, TokenValue>,
172 pub dark_overrides: &'a BTreeMap<String, TokenValue>,
173 pub defaults: &'a BTreeMap<String, TokenValue>,
174 pub dark_defaults: &'a BTreeMap<String, TokenValue>,
175}
176
177pub fn resolve(
187 path: &str,
188 scheme: Scheme,
189 tables: &TokenTables<'_>,
190) -> Option<TokenValue> {
191 if let Some(rest) = path.strip_prefix("space.") {
193 if let Ok(n) = rest.parse::<f32>() {
194 if let Some(base) = resolve("space.base", scheme, tables) {
195 if let Some(base) = base.as_f32() {
196 return Some(TokenValue::F32(base * n));
197 }
198 }
199 }
200 }
201
202 let chain = parent_chain(path);
203
204 if scheme == Scheme::Dark {
207 for &cand in &chain {
208 if let Some(v) = tables.dark_overrides.get(cand) {
209 return Some(v.clone());
210 }
211 }
212 }
213 for &cand in &chain {
214 if let Some(v) = tables.overrides.get(cand) {
215 return Some(v.clone());
216 }
217 }
218
219 if scheme == Scheme::Dark {
221 for &cand in &chain {
222 if let Some(v) = tables.dark_defaults.get(cand) {
223 return Some(v.clone());
224 }
225 }
226 }
227 for &cand in &chain {
228 if let Some(v) = tables.defaults.get(cand) {
229 return Some(v.clone());
230 }
231 }
232 None
233}
234
235pub fn resolve_ref(
242 path: &str,
243 scheme: Scheme,
244 tables: &TokenTables<'_>,
245) -> Option<TokenValue> {
246 match path.strip_prefix("dark.") {
247 Some(bare) => resolve(bare, Scheme::Dark, tables),
248 None => resolve(path, scheme, tables),
249 }
250}
251
252fn parent_chain(path: &str) -> Vec<&str> {
255 let mut chain = Vec::new();
256 chain.push(path);
257 let mut cur = path;
258 while let Some(idx) = cur.rfind('.') {
259 cur = &cur[..idx];
260 chain.push(cur);
261 }
262 chain.push("");
263 chain
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use alloc::string::ToString;
270
271 fn map(pairs: &[(&str, TokenValue)]) -> BTreeMap<String, TokenValue> {
272 pairs
273 .iter()
274 .map(|(k, v)| ((*k).to_string(), v.clone()))
275 .collect()
276 }
277
278 fn tables(
279 overrides: &[(&str, TokenValue)],
280 dark_overrides: &[(&str, TokenValue)],
281 defaults: &[(&str, TokenValue)],
282 dark_defaults: &[(&str, TokenValue)],
283 ) -> (BTreeMap<String, TokenValue>, BTreeMap<String, TokenValue>, BTreeMap<String, TokenValue>, BTreeMap<String, TokenValue>) {
284 (map(overrides), map(dark_overrides), map(defaults), map(dark_defaults))
285 }
286
287 fn resolve_with(
288 overrides: &[(&str, TokenValue)],
289 dark_overrides: &[(&str, TokenValue)],
290 defaults: &[(&str, TokenValue)],
291 path: &str,
292 scheme: Scheme,
293 ) -> Option<TokenValue> {
294 let (o, d, de, dde) = tables(overrides, dark_overrides, defaults, &[]);
295 let t = TokenTables {
296 overrides: &o,
297 dark_overrides: &d,
298 defaults: &de,
299 dark_defaults: &dde,
300 };
301 resolve(path, scheme, &t)
302 }
303
304 #[test]
306 fn conformance_resolution_table() {
307 let (o, d, de, dde) = tables(
308 &[
309 ("color.primary", TokenValue::Color(0xFF2563EB)),
310 ("space.base", TokenValue::F32(4.0)),
311 ],
312 &[("color.primary", TokenValue::Color(0xFF60A5FA))],
313 &[("font.body.size", TokenValue::F32(16.0)), ("space.base", TokenValue::F32(4.0))],
314 &[],
315 );
316 let t = TokenTables {
317 overrides: &o,
318 dark_overrides: &d,
319 defaults: &de,
320 dark_defaults: &dde,
321 };
322
323 assert_eq!(
324 resolve("color.primary", Scheme::Dark, &t),
325 Some(TokenValue::Color(0xFF60A5FA)),
326 "dark.color.primary override wins (dark layer)"
327 );
328 assert_eq!(
329 resolve("color.primary", Scheme::Light, &t),
330 Some(TokenValue::Color(0xFF2563EB)),
331 "dark layer skipped; base override wins"
332 );
333 assert_eq!(
334 resolve("color.primary", Scheme::Dark, &TokenTables {
335 overrides: &o,
336 dark_overrides: &BTreeMap::new(),
337 defaults: &de,
338 dark_defaults: &dde,
339 }),
340 Some(TokenValue::Color(0xFF2563EB)),
341 "base override falls through when the dark layer is empty"
342 );
343 assert_eq!(
344 resolve("font.body.size", Scheme::Light, &t),
345 Some(TokenValue::F32(16.0)),
346 "no override; defaults are renderer-owned"
347 );
348 assert_eq!(
349 resolve_with(&[("font.body", TokenValue::F32(18.0))], &[], &[], "font.body.size", Scheme::Light),
350 Some(TokenValue::F32(18.0)),
351 "parent-path fallback"
352 );
353 assert_eq!(
354 resolve("space.2", Scheme::Light, &t),
355 Some(TokenValue::F32(8.0)),
356 "generative family: space.base (4.0) × 2"
357 );
358 assert_eq!(
359 resolve("space.0.5", Scheme::Light, &t),
360 Some(TokenValue::F32(2.0)),
361 "generative family: fractional leaf"
362 );
363 assert_eq!(
364 resolve_with(&[], &[], &[("space.base", TokenValue::F32(4.0))], "space.2", Scheme::Light),
365 Some(TokenValue::F32(8.0)),
366 "generative base falls back to renderer defaults"
367 );
368 assert_eq!(
369 resolve("missing.token", Scheme::Light, &t),
370 None,
371 "no override, no default, no fallback -> None"
372 );
373 }
374
375 #[test]
376 fn parent_fallback_walks_ancestors_and_root() {
377 assert_eq!(parent_chain("color.primary"), ["color.primary", "color", ""]);
378 assert_eq!(
379 parent_chain("font.body.size"),
380 ["font.body.size", "font.body", "font", ""]
381 );
382 }
383
384 #[test]
385 fn explicit_dark_reference_forces_the_dark_layer() {
386 let (o, d, de, dde) = tables(
389 &[("color.primary", TokenValue::Color(0xFF_2563EB))],
390 &[("color.primary", TokenValue::Color(0xFF_F59E0B))],
391 &[],
392 &[],
393 );
394 let t = TokenTables {
395 overrides: &o,
396 dark_overrides: &d,
397 defaults: &de,
398 dark_defaults: &dde,
399 };
400 assert_eq!(
401 resolve_ref("dark.color.primary", Scheme::Light, &t),
402 Some(TokenValue::Color(0xFF_F59E0B)),
403 "explicit dark reference ignores the light scheme"
404 );
405 assert_eq!(
406 resolve_ref("dark.color.primary", Scheme::Dark, &t),
407 Some(TokenValue::Color(0xFF_F59E0B))
408 );
409 assert_eq!(
410 resolve_ref("color.primary", Scheme::Light, &t),
411 Some(TokenValue::Color(0xFF_2563EB))
412 );
413 assert_eq!(
414 resolve_ref("color.primary", Scheme::Dark, &t),
415 Some(TokenValue::Color(0xFF_F59E0B))
416 );
417 }
418
419 #[test]
420 fn explicit_dark_reference_falls_back_to_dark_defaults() {
421 let (o, d, de, dde) = tables(
422 &[],
423 &[],
424 &[("color.primary", TokenValue::Color(0xFF_2563EB))],
425 &[("color.primary", TokenValue::Color(0xFF_111827))],
426 );
427 let t = TokenTables {
428 overrides: &o,
429 dark_overrides: &d,
430 defaults: &de,
431 dark_defaults: &dde,
432 };
433 assert_eq!(
434 resolve_ref("dark.color.primary", Scheme::Light, &t),
435 Some(TokenValue::Color(0xFF_111827)),
436 "dark defaults, not light defaults"
437 );
438 }
439
440 #[test]
441 fn dark_layer_takes_precedence_over_base() {
442 let (o, d, de, dde) = tables(
444 &[("color.accent", TokenValue::Color(0xFF0000FF))],
445 &[("color.accent", TokenValue::Color(0xFFFF0000))],
446 &[],
447 &[],
448 );
449 let t = TokenTables {
450 overrides: &o,
451 dark_overrides: &d,
452 defaults: &de,
453 dark_defaults: &dde,
454 };
455 assert_eq!(resolve("color.accent", Scheme::Dark, &t), Some(TokenValue::Color(0xFFFF0000)));
456 assert_eq!(resolve("color.accent", Scheme::Light, &t), Some(TokenValue::Color(0xFF0000FF)));
457 }
458}