use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scheme {
Light,
Dark,
}
impl Default for Scheme {
fn default() -> Self {
Scheme::Light
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TokenValue {
Color(u32),
F32(f32),
U32(u32),
U8(u8),
I32(i32),
Str(String),
}
impl TokenValue {
pub fn value_type(&self) -> u8 {
match self {
TokenValue::Color(_) => crate::value_type::COLOR,
TokenValue::F32(_) => crate::value_type::F32,
TokenValue::U32(_) => crate::value_type::U32,
TokenValue::U8(_) => crate::value_type::U8,
TokenValue::I32(_) => crate::value_type::I32,
TokenValue::Str(_) => crate::value_type::STRING,
}
}
pub fn from_wire(value_type: u8, value: u32) -> Option<Self> {
match value_type {
crate::value_type::COLOR => Some(TokenValue::Color(value)),
crate::value_type::F32 => Some(TokenValue::F32(f32::from_bits(value))),
crate::value_type::U32 => Some(TokenValue::U32(value)),
crate::value_type::U8 => Some(TokenValue::U8((value & 0xFF) as u8)),
crate::value_type::I32 => Some(TokenValue::I32(value as i32)),
_ => None,
}
}
pub fn as_f32(&self) -> Option<f32> {
match self {
TokenValue::F32(v) => Some(*v),
TokenValue::U32(v) => Some(*v as f32),
TokenValue::U8(v) => Some(f32::from(*v)),
TokenValue::I32(v) => Some(*v as f32),
_ => None,
}
}
pub fn to_bits(&self) -> Option<u32> {
match self {
TokenValue::Color(v) => Some(*v),
TokenValue::F32(v) => Some(v.to_bits()),
TokenValue::U32(v) => Some(*v),
TokenValue::U8(v) => Some(u32::from(*v)),
TokenValue::I32(v) => Some(*v as u32),
TokenValue::Str(_) => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct TokenTables<'a> {
pub overrides: &'a BTreeMap<String, TokenValue>,
pub dark_overrides: &'a BTreeMap<String, TokenValue>,
pub defaults: &'a BTreeMap<String, TokenValue>,
pub dark_defaults: &'a BTreeMap<String, TokenValue>,
}
pub fn resolve(
path: &str,
scheme: Scheme,
tables: &TokenTables<'_>,
) -> Option<TokenValue> {
if let Some(rest) = path.strip_prefix("space.") {
if let Ok(n) = rest.parse::<f32>() {
if let Some(base) = resolve("space.base", scheme, tables) {
if let Some(base) = base.as_f32() {
return Some(TokenValue::F32(base * n));
}
}
}
}
let chain = parent_chain(path);
if scheme == Scheme::Dark {
for &cand in &chain {
if let Some(v) = tables.dark_overrides.get(cand) {
return Some(v.clone());
}
}
}
for &cand in &chain {
if let Some(v) = tables.overrides.get(cand) {
return Some(v.clone());
}
}
if scheme == Scheme::Dark {
for &cand in &chain {
if let Some(v) = tables.dark_defaults.get(cand) {
return Some(v.clone());
}
}
}
for &cand in &chain {
if let Some(v) = tables.defaults.get(cand) {
return Some(v.clone());
}
}
None
}
fn parent_chain(path: &str) -> Vec<&str> {
let mut chain = Vec::new();
chain.push(path);
let mut cur = path;
while let Some(idx) = cur.rfind('.') {
cur = &cur[..idx];
chain.push(cur);
}
chain.push("");
chain
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
fn map(pairs: &[(&str, TokenValue)]) -> BTreeMap<String, TokenValue> {
pairs
.iter()
.map(|(k, v)| ((*k).to_string(), v.clone()))
.collect()
}
fn tables(
overrides: &[(&str, TokenValue)],
dark_overrides: &[(&str, TokenValue)],
defaults: &[(&str, TokenValue)],
dark_defaults: &[(&str, TokenValue)],
) -> (BTreeMap<String, TokenValue>, BTreeMap<String, TokenValue>, BTreeMap<String, TokenValue>, BTreeMap<String, TokenValue>) {
(map(overrides), map(dark_overrides), map(defaults), map(dark_defaults))
}
fn resolve_with(
overrides: &[(&str, TokenValue)],
dark_overrides: &[(&str, TokenValue)],
defaults: &[(&str, TokenValue)],
path: &str,
scheme: Scheme,
) -> Option<TokenValue> {
let (o, d, de, dde) = tables(overrides, dark_overrides, defaults, &[]);
let t = TokenTables {
overrides: &o,
dark_overrides: &d,
defaults: &de,
dark_defaults: &dde,
};
resolve(path, scheme, &t)
}
#[test]
fn conformance_resolution_table() {
let (o, d, de, dde) = tables(
&[
("color.primary", TokenValue::Color(0xFF2563EB)),
("space.base", TokenValue::F32(4.0)),
],
&[("color.primary", TokenValue::Color(0xFF60A5FA))],
&[("font.body.size", TokenValue::F32(16.0)), ("space.base", TokenValue::F32(4.0))],
&[],
);
let t = TokenTables {
overrides: &o,
dark_overrides: &d,
defaults: &de,
dark_defaults: &dde,
};
assert_eq!(
resolve("color.primary", Scheme::Dark, &t),
Some(TokenValue::Color(0xFF60A5FA)),
"dark.color.primary override wins (dark layer)"
);
assert_eq!(
resolve("color.primary", Scheme::Light, &t),
Some(TokenValue::Color(0xFF2563EB)),
"dark layer skipped; base override wins"
);
assert_eq!(
resolve("color.primary", Scheme::Dark, &TokenTables {
overrides: &o,
dark_overrides: &BTreeMap::new(),
defaults: &de,
dark_defaults: &dde,
}),
Some(TokenValue::Color(0xFF2563EB)),
"base override falls through when the dark layer is empty"
);
assert_eq!(
resolve("font.body.size", Scheme::Light, &t),
Some(TokenValue::F32(16.0)),
"no override; defaults are renderer-owned"
);
assert_eq!(
resolve_with(&[("font.body", TokenValue::F32(18.0))], &[], &[], "font.body.size", Scheme::Light),
Some(TokenValue::F32(18.0)),
"parent-path fallback"
);
assert_eq!(
resolve("space.2", Scheme::Light, &t),
Some(TokenValue::F32(8.0)),
"generative family: space.base (4.0) × 2"
);
assert_eq!(
resolve("space.0.5", Scheme::Light, &t),
Some(TokenValue::F32(2.0)),
"generative family: fractional leaf"
);
assert_eq!(
resolve_with(&[], &[], &[("space.base", TokenValue::F32(4.0))], "space.2", Scheme::Light),
Some(TokenValue::F32(8.0)),
"generative base falls back to renderer defaults"
);
assert_eq!(
resolve("missing.token", Scheme::Light, &t),
None,
"no override, no default, no fallback -> None"
);
}
#[test]
fn parent_fallback_walks_ancestors_and_root() {
assert_eq!(parent_chain("color.primary"), ["color.primary", "color", ""]);
assert_eq!(
parent_chain("font.body.size"),
["font.body.size", "font.body", "font", ""]
);
}
#[test]
fn dark_layer_takes_precedence_over_base() {
let (o, d, de, dde) = tables(
&[("color.accent", TokenValue::Color(0xFF0000FF))],
&[("color.accent", TokenValue::Color(0xFFFF0000))],
&[],
&[],
);
let t = TokenTables {
overrides: &o,
dark_overrides: &d,
defaults: &de,
dark_defaults: &dde,
};
assert_eq!(resolve("color.accent", Scheme::Dark, &t), Some(TokenValue::Color(0xFFFF0000)));
assert_eq!(resolve("color.accent", Scheme::Light, &t), Some(TokenValue::Color(0xFF0000FF)));
}
}