use std::collections::HashMap;
use ratatui::style::Color as RColor;
use crate::box_model::{BorderStyle, BoxEdges, Length};
use crate::color::Color;
use crate::error::{CssError, Result};
use crate::media::{MediaContext, MediaQuery};
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
Color(Color),
Length(Length),
BoxEdges(BoxEdges),
BorderStyle(BorderStyle),
Var { name: String },
}
impl From<Color> for Token {
fn from(c: Color) -> Self {
Token::Color(c)
}
}
impl From<Length> for Token {
fn from(l: Length) -> Self {
Token::Length(l)
}
}
impl From<BoxEdges> for Token {
fn from(e: BoxEdges) -> Self {
Token::BoxEdges(e)
}
}
impl From<BorderStyle> for Token {
fn from(b: BorderStyle) -> Self {
Token::BorderStyle(b)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TokenKind {
Color,
Length,
BoxEdges,
BorderStyle,
}
impl TokenKind {
fn compatible_with(self, tok: &Token) -> bool {
match tok {
Token::Var { .. } => true,
Token::Color(_) => self == TokenKind::Color,
Token::Length(Length::Cells(_)) => {
self == TokenKind::Length || self == TokenKind::BoxEdges
}
Token::Length(_) => self == TokenKind::Length,
Token::BoxEdges(_) => self == TokenKind::BoxEdges,
Token::BorderStyle(_) => self == TokenKind::BorderStyle,
}
}
}
impl From<&str> for Token {
fn from(s: &str) -> Self {
Token::Color(Color::from(s))
}
}
impl From<String> for Token {
fn from(s: String) -> Self {
Token::from(s.as_str())
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ThemeTokens {
vars: HashMap<String, Token>,
media_vars: Vec<(MediaQuery, HashMap<String, Token>)>,
}
impl ThemeTokens {
pub fn new() -> Self {
Self::default()
}
pub fn set<T: Into<Token>>(mut self, name: impl Into<String>, value: T) -> Self {
self.vars.insert(name.into(), value.into());
self
}
pub fn insert<T: Into<Token>>(&mut self, name: impl Into<String>, value: T) {
self.vars.insert(name.into(), value.into());
}
pub fn insert_media<T: Into<Token>>(
&mut self,
query: MediaQuery,
name: impl Into<String>,
value: T,
) {
let key = name.into();
for (q, map) in &mut self.media_vars {
if q == &query {
map.insert(key.clone(), value.into());
return;
}
}
let mut map = HashMap::new();
map.insert(key, value.into());
self.media_vars.push((query, map));
}
pub fn set_media<T: Into<Token>>(
mut self,
query: MediaQuery,
name: impl Into<String>,
value: T,
) -> Self {
self.insert_media(query, name, value);
self
}
pub fn get(&self, name: &str) -> Option<&Token> {
self.vars.get(name)
}
pub fn is_defined(&self, name: &str) -> bool {
if self.vars.contains_key(name) {
return true;
}
self.media_vars
.iter()
.any(|(_, map)| map.contains_key(name))
}
pub fn get_color(&self, name: &str) -> Option<&Color> {
let mut cur = name;
for _ in 0..32 {
match self.vars.get(cur)? {
Token::Color(c) => return Some(c),
Token::Var { name: next } => cur = next,
_ => return None,
}
}
None
}
pub fn get_length(&self, name: &str) -> Option<&Length> {
let mut cur = name;
for _ in 0..32 {
match self.vars.get(cur)? {
Token::Length(l) => return Some(l),
Token::Var { name: next } => cur = next,
_ => return None,
}
}
None
}
pub fn get_box_edges(&self, name: &str) -> Option<&BoxEdges> {
let mut cur = name;
for _ in 0..32 {
match self.vars.get(cur)? {
Token::BoxEdges(e) => return Some(e),
Token::Var { name: next } => cur = next,
_ => return None,
}
}
None
}
pub fn get_border_style(&self, name: &str) -> Option<&BorderStyle> {
let mut cur = name;
for _ in 0..32 {
match self.vars.get(cur)? {
Token::BorderStyle(b) => return Some(b),
Token::Var { name: next } => cur = next,
_ => return None,
}
}
None
}
pub fn get_color_with(&self, name: &str, media: &MediaContext) -> Option<Color> {
self.resolve_color_with(name, media, 0)
}
pub fn get_length_with(&self, name: &str, media: &MediaContext) -> Option<Length> {
self.resolve_length_with(name, media, 0)
}
pub fn get_box_edges_with(&self, name: &str, media: &MediaContext) -> Option<BoxEdges> {
self.resolve_box_edges_with(name, media, 0)
}
pub fn get_border_style_with(&self, name: &str, media: &MediaContext) -> Option<BorderStyle> {
self.resolve_border_style_with(name, media, 0)
}
fn resolve_color_with(&self, name: &str, media: &MediaContext, depth: u8) -> Option<Color> {
if depth > 32 {
return None;
}
if let Some(map) = self.best_media_map(name, media, TokenKind::Color) {
match map.get(name).expect("best_media_map guarantees map[name] present") {
Token::Color(c) => return Some(c.clone()),
Token::Var { name: next } => {
return self.resolve_color_with(next, media, depth + 1);
}
_ => return None,
}
}
match self.vars.get(name)? {
Token::Color(c) => Some(c.clone()),
Token::Var { name: next } => self.resolve_color_with(next, media, depth + 1),
_ => None,
}
}
fn resolve_length_with(&self, name: &str, media: &MediaContext, depth: u8) -> Option<Length> {
if depth > 32 {
return None;
}
if let Some(map) = self.best_media_map(name, media, TokenKind::Length) {
match map.get(name).expect("best_media_map guarantees map[name] present") {
Token::Length(l) => return Some(l.clone()),
Token::Var { name: next } => {
return self.resolve_length_with(next, media, depth + 1);
}
_ => return None,
}
}
match self.vars.get(name)? {
Token::Length(l) => Some(l.clone()),
Token::Var { name: next } => self.resolve_length_with(next, media, depth + 1),
_ => None,
}
}
fn resolve_box_edges_with(
&self,
name: &str,
media: &MediaContext,
depth: u8,
) -> Option<BoxEdges> {
if depth > 32 {
return None;
}
if let Some(map) = self.best_media_map(name, media, TokenKind::BoxEdges) {
return match map.get(name).expect("best_media_map guarantees map[name] present") {
Token::BoxEdges(e) => Some(*e),
Token::Length(Length::Cells(n)) => Some(BoxEdges::uniform(*n)),
Token::Var { name: next } => {
self.resolve_box_edges_with(next, media, depth + 1)
}
_ => None,
};
}
match self.vars.get(name)? {
Token::BoxEdges(e) => Some(*e),
Token::Length(Length::Cells(n)) => Some(BoxEdges::uniform(*n)),
Token::Var { name: next } => self.resolve_box_edges_with(next, media, depth + 1),
_ => None,
}
}
fn resolve_border_style_with(
&self,
name: &str,
media: &MediaContext,
depth: u8,
) -> Option<BorderStyle> {
if depth > 32 {
return None;
}
if let Some(map) = self.best_media_map(name, media, TokenKind::BorderStyle) {
match map.get(name).expect("best_media_map guarantees map[name] present") {
Token::BorderStyle(b) => return Some(*b),
Token::Var { name: next } => {
return self.resolve_border_style_with(next, media, depth + 1);
}
_ => return None,
}
}
match self.vars.get(name)? {
Token::BorderStyle(b) => Some(*b),
Token::Var { name: next } => self.resolve_border_style_with(next, media, depth + 1),
_ => None,
}
}
fn best_media_map(
&self,
name: &str,
media: &MediaContext,
kind: TokenKind,
) -> Option<&HashMap<String, Token>> {
let mut best: Option<(&HashMap<String, Token>, usize)> = None;
for (query, map) in &self.media_vars {
let spec = match query.matching_specificity(media) {
Some(s) => s,
None => continue,
};
let tok = match map.get(name) {
Some(t) => t,
None => continue,
};
if !kind.compatible_with(tok) {
continue;
}
match best {
Some((_, cur_spec)) if cur_spec > spec => {}
_ => best = Some((map, spec)),
}
}
best.map(|(map, _)| map)
}
pub fn merge(&mut self, other: &ThemeTokens) {
for (k, v) in &other.vars {
self.vars.insert(k.clone(), v.clone());
}
for (q, map) in &other.media_vars {
self.media_vars.push((q.clone(), map.clone()));
}
}
pub fn is_empty(&self) -> bool {
self.vars.is_empty()
}
pub fn len(&self) -> usize {
self.vars.len()
}
}
pub fn resolve_strict(color: &Color, tokens: &ThemeTokens) -> Result<RColor> {
resolve_strict_with_media(color, tokens, &MediaContext::default())
}
pub fn resolve(color: &Color, tokens: &ThemeTokens) -> RColor {
resolve_with_media(color, tokens, &MediaContext::default())
}
pub fn resolve_strict_with_media(
color: &Color,
tokens: &ThemeTokens,
media: &MediaContext,
) -> Result<RColor> {
resolve_inner(color, tokens, media, 0)
}
pub fn resolve_with_media(color: &Color, tokens: &ThemeTokens, media: &MediaContext) -> RColor {
resolve_strict_with_media(color, tokens, media).unwrap_or(RColor::Reset)
}
fn resolve_inner(
color: &Color,
tokens: &ThemeTokens,
media: &MediaContext,
depth: u8,
) -> Result<RColor> {
if depth > 32 {
return Err(CssError::circular_variable(
"var() reference chain too deep (depth > 32)",
));
}
match color {
Color::Literal(c) => Ok(*c),
Color::Reset => Ok(RColor::Reset),
Color::Inherit => Ok(RColor::Reset),
Color::Var { name, fallback } => match tokens.get_color_with(name, media) {
Some(referent) => resolve_inner(&referent, tokens, media, depth + 1),
None => match fallback {
Some(fb) => resolve_inner(fb, tokens, media, depth + 1),
None => Err(CssError::undefined_variable(name.clone())),
},
},
}
}
pub fn resolve_length_strict(length: &Length, tokens: &ThemeTokens) -> Result<Length> {
resolve_length_strict_with_media(length, tokens, &MediaContext::default())
}
pub fn resolve_length(length: &Length, tokens: &ThemeTokens) -> Length {
resolve_length_with_media(length, tokens, &MediaContext::default())
}
pub fn resolve_length_strict_with_media(
length: &Length,
tokens: &ThemeTokens,
media: &MediaContext,
) -> Result<Length> {
resolve_length_inner(length, tokens, media, 0)
}
pub fn resolve_length_with_media(
length: &Length,
tokens: &ThemeTokens,
media: &MediaContext,
) -> Length {
resolve_length_strict_with_media(length, tokens, media).unwrap_or(Length::Auto)
}
fn resolve_length_inner(
length: &Length,
tokens: &ThemeTokens,
media: &MediaContext,
depth: u8,
) -> Result<Length> {
if depth > 32 {
return Err(CssError::circular_variable(
"var() reference chain too deep (depth > 32)",
));
}
match length {
Length::Auto | Length::Cells(_) | Length::Percent(_) | Length::Min(_) | Length::Max(_) => {
Ok(length.clone())
}
Length::Var { name, fallback } => match tokens.get_length_with(name, media) {
Some(referent) => resolve_length_inner(&referent, tokens, media, depth + 1),
None => match fallback {
Some(fb) => resolve_length_inner(fb, tokens, media, depth + 1),
None => Err(CssError::undefined_variable(name.clone())),
},
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_simple_var() {
let tokens = ThemeTokens::new().set("accent", Color::literal(RColor::Blue));
let c = Color::var("accent");
assert_eq!(resolve_strict(&c, &tokens).unwrap(), RColor::Blue);
}
#[test]
fn resolves_chain() {
let tokens = ThemeTokens::new()
.set("accent", Color::var("blue"))
.set("blue", Color::literal(RColor::Blue));
assert_eq!(resolve_strict(&Color::var("accent"), &tokens).unwrap(), RColor::Blue);
}
#[test]
fn uses_fallback() {
let tokens = ThemeTokens::new();
let c = Color::Var { name: "missing".into(), fallback: Some(Box::new(Color::literal(RColor::Red))) };
assert_eq!(resolve_strict(&c, &tokens).unwrap(), RColor::Red);
}
#[test]
fn undefined_is_error_strict_but_reset_lenient() {
let tokens = ThemeTokens::new();
assert!(resolve_strict(&Color::var("nope"), &tokens).is_err());
assert_eq!(resolve(&Color::var("nope"), &tokens), RColor::Reset);
}
#[test]
fn token_table_holds_length() {
let tokens = ThemeTokens::new().set("w", Length::Cells(22));
assert_eq!(tokens.get_length("w"), Some(&Length::Cells(22)));
assert_eq!(tokens.get_color("w"), None);
let tokens = ThemeTokens::new().set("c", Color::literal(RColor::Blue));
assert_eq!(tokens.get_color("c"), Some(&Color::literal(RColor::Blue)));
assert_eq!(tokens.get_length("c"), None);
}
#[test]
fn length_var_resolves_strict() {
let tokens = ThemeTokens::new().set("w", Length::Cells(22));
assert_eq!(
resolve_length_strict(&Length::Var { name: "w".into(), fallback: None }, &tokens).unwrap(),
Length::Cells(22)
);
}
#[test]
fn length_var_chain() {
let tokens = ThemeTokens::new()
.set("w", Length::Var { name: "w2".into(), fallback: None })
.set("w2", Length::Cells(10));
assert_eq!(
resolve_length_strict(&Length::Var { name: "w".into(), fallback: None }, &tokens).unwrap(),
Length::Cells(10)
);
}
#[test]
fn length_var_undefined_degrades_to_auto_lenient() {
let tokens = ThemeTokens::new();
assert!(resolve_length_strict(&Length::Var { name: "nope".into(), fallback: None }, &tokens).is_err());
assert_eq!(
resolve_length(&Length::Var { name: "nope".into(), fallback: None }, &tokens),
Length::Auto
);
}
#[test]
fn length_var_mistype_degrades_to_auto_lenient() {
let tokens = ThemeTokens::new().set("c", Color::literal(RColor::Blue));
assert_eq!(
resolve_length(&Length::Var { name: "c".into(), fallback: None }, &tokens),
Length::Auto
);
}
#[test]
fn length_var_undefined_uses_fallback() {
let tokens = ThemeTokens::new();
let l = Length::Var {
name: "missing".into(),
fallback: Some(Box::new(Length::Cells(7))),
};
assert_eq!(resolve_length_strict(&l, &tokens).unwrap(), Length::Cells(7));
assert_eq!(resolve_length(&l, &tokens), Length::Cells(7));
}
fn mq(s: &str) -> MediaQuery {
MediaQuery::parse(s).unwrap()
}
fn ctx(cols: u16) -> MediaContext {
MediaContext {
cols,
..Default::default()
}
}
#[test]
fn get_color_with_uses_media_override_when_matching() {
let tokens = ThemeTokens::new()
.set("accent", Color::literal(RColor::Red))
.set_media(
mq("(min-width: 80)"),
"accent",
Color::literal(RColor::Blue),
);
assert_eq!(
tokens.get_color_with("accent", &ctx(100)),
Some(Color::literal(RColor::Blue))
);
assert_eq!(
tokens.get_color_with("accent", &ctx(60)),
Some(Color::literal(RColor::Red))
);
assert_eq!(
tokens.get_color("accent"),
Some(&Color::literal(RColor::Red))
);
}
#[test]
fn get_color_with_falls_back_when_override_is_for_a_different_name() {
let tokens = ThemeTokens::new()
.set("accent", Color::literal(RColor::Red))
.set_media(
mq("(min-width: 80)"),
"other",
Color::literal(RColor::Green),
);
assert_eq!(
tokens.get_color_with("accent", &ctx(100)),
Some(Color::literal(RColor::Red)),
"override for --other must not shadow --accent"
);
}
#[test]
fn get_color_with_last_matching_override_wins() {
let tokens = ThemeTokens::new()
.set("accent", Color::literal(RColor::Red))
.set_media(mq("(min-width: 50)"), "accent", Color::literal(RColor::Green))
.set_media(mq("(min-width: 80)"), "accent", Color::literal(RColor::Blue));
assert_eq!(
tokens.get_color_with("accent", &ctx(100)),
Some(Color::literal(RColor::Blue)),
"last-matching media override wins by source order"
);
assert_eq!(
tokens.get_color_with("accent", &ctx(60)),
Some(Color::literal(RColor::Green))
);
}
#[test]
fn get_color_with_chains_through_media_var() {
let tokens = ThemeTokens::new().set_media(
mq("(min-width: 80)"),
"x",
Token::Var { name: "y".to_string() },
);
let tokens = tokens.set_media(
mq("(min-width: 80)"),
"y",
Color::literal(RColor::Magenta),
);
assert_eq!(
tokens.get_color_with("x", &ctx(100)),
Some(Color::literal(RColor::Magenta)),
"media-gated var() chain resolves through both media entries"
);
assert_eq!(tokens.get_color_with("x", &ctx(40)), None);
}
#[test]
fn get_length_with_uses_media_override() {
let tokens = ThemeTokens::new()
.set("w", Length::Cells(5))
.set_media(mq("(min-width: 80)"), "w", Length::Cells(50));
assert_eq!(tokens.get_length_with("w", &ctx(100)), Some(Length::Cells(50)));
assert_eq!(tokens.get_length_with("w", &ctx(40)), Some(Length::Cells(5)));
assert_eq!(tokens.get_length("w"), Some(&Length::Cells(5)));
}
#[test]
fn insert_media_accumulates_same_query_into_one_map() {
let q = mq("(min-width: 80)");
let mut tokens = ThemeTokens::new();
tokens.insert_media(q.clone(), "a", Color::literal(RColor::Red));
tokens.insert_media(q.clone(), "b", Color::literal(RColor::Green));
assert_eq!(tokens.get_color_with("a", &ctx(100)), Some(Color::literal(RColor::Red)));
assert_eq!(tokens.get_color_with("b", &ctx(100)), Some(Color::literal(RColor::Green)));
tokens.insert_media(q, "a", Color::literal(RColor::Blue));
assert_eq!(tokens.get_color_with("a", &ctx(100)), Some(Color::literal(RColor::Blue)));
}
#[test]
fn is_defined_checks_default_and_all_media_maps() {
let mut tokens = ThemeTokens::new();
tokens.insert("default_only", Color::literal(RColor::Red));
tokens.insert_media(mq("(min-width: 80)"), "media_only", Color::literal(RColor::Red));
assert!(tokens.is_defined("default_only"));
assert!(tokens.is_defined("media_only"));
assert!(!tokens.is_defined("neither"));
}
#[test]
fn resolve_with_media_gates_var_against_context() {
let tokens = ThemeTokens::new()
.set("accent", Color::literal(RColor::Red))
.set_media(mq("(min-width: 80)"), "accent", Color::literal(RColor::Blue));
assert_eq!(resolve(&Color::var("accent"), &tokens), RColor::Red);
assert_eq!(
resolve_with_media(&Color::var("accent"), &tokens, &ctx(100)),
RColor::Blue
);
assert_eq!(
resolve_with_media(&Color::var("accent"), &tokens, &ctx(40)),
RColor::Red
);
}
#[test]
fn resolve_length_with_media_gates_var_against_context() {
let tokens = ThemeTokens::new()
.set("w", Length::Cells(5))
.set_media(mq("(min-width: 80)"), "w", Length::Cells(50));
assert_eq!(
resolve_length_with_media(&Length::Var { name: "w".into(), fallback: None }, &tokens, &ctx(100)),
Length::Cells(50)
);
assert_eq!(
resolve_length_with_media(&Length::Var { name: "w".into(), fallback: None }, &tokens, &ctx(40)),
Length::Cells(5)
);
}
#[test]
fn merge_merges_media_vars_too() {
let other = ThemeTokens::new()
.set("a", Color::literal(RColor::Red))
.set_media(mq("(min-width: 80)"), "a", Color::literal(RColor::Blue));
let mut mine = ThemeTokens::new();
mine.merge(&other);
assert_eq!(mine.get_color("a"), Some(&Color::literal(RColor::Red)));
assert_eq!(mine.get_color_with("a", &ctx(100)), Some(Color::literal(RColor::Blue)));
}
#[test]
fn get_color_with_picks_more_specific_override() {
let tokens = ThemeTokens::new()
.set_media(mq("(min-width: 80)"), "x", Color::literal(RColor::Red))
.set_media(mq("(min-width: 80) and (color)"), "x", Color::literal(RColor::Blue));
assert_eq!(
tokens.get_color_with("x", &ctx(100)),
Some(Color::literal(RColor::Blue)),
"the 2-condition override is more specific and wins"
);
}
#[test]
fn get_color_with_specificity_tie_falls_back_to_source_order() {
let tokens = ThemeTokens::new()
.set_media(mq("(min-width: 50)"), "x", Color::literal(RColor::Red))
.set_media(mq("(min-width: 80)"), "x", Color::literal(RColor::Blue));
assert_eq!(
tokens.get_color_with("x", &ctx(100)),
Some(Color::literal(RColor::Blue)),
"equal specificity → later source-order wins"
);
}
#[test]
fn get_color_with_less_specific_does_not_override_more_specific() {
let tokens = ThemeTokens::new()
.set_media(mq("(min-width: 80) and (color)"), "x", Color::literal(RColor::Blue))
.set_media(mq("(min-width: 80)"), "x", Color::literal(RColor::Red));
assert_eq!(
tokens.get_color_with("x", &ctx(100)),
Some(Color::literal(RColor::Blue)),
"more-specific wins regardless of source position"
);
}
#[test]
fn get_color_with_single_override_unchanged() {
let tokens = ThemeTokens::new()
.set("x", Color::literal(RColor::Red))
.set_media(mq("(min-width: 80)"), "x", Color::literal(RColor::Blue));
assert_eq!(tokens.get_color_with("x", &ctx(100)), Some(Color::literal(RColor::Blue)));
assert_eq!(tokens.get_color_with("x", &ctx(40)), Some(Color::literal(RColor::Red)));
}
#[test]
fn get_color_with_no_override_falls_back_to_default() {
let tokens = ThemeTokens::new().set("x", Color::literal(RColor::Red));
assert_eq!(tokens.get_color_with("x", &ctx(100)), Some(Color::literal(RColor::Red)));
assert_eq!(tokens.get_color_with("x", &ctx(40)), Some(Color::literal(RColor::Red)));
}
#[test]
fn get_color_with_specificity_var_chain() {
let tokens = ThemeTokens::new()
.set_media(mq("(min-width: 80)"), "x", Color::literal(RColor::Red))
.set_media(mq("(min-width: 80)"), "y", Color::literal(RColor::Magenta))
.set_media(mq("(min-width: 80) and (color)"), "x", Token::Var { name: "y".into() });
assert_eq!(
tokens.get_color_with("x", &ctx(100)),
Some(Color::literal(RColor::Magenta)),
"more-specific var() chain resolves through the same media context"
);
}
#[test]
fn get_length_with_picks_more_specific_override() {
let tokens = ThemeTokens::new()
.set_media(mq("(min-width: 80)"), "w", Length::Cells(5))
.set_media(mq("(min-width: 80) and (color)"), "w", Length::Cells(50));
assert_eq!(
tokens.get_length_with("w", &ctx(100)),
Some(Length::Cells(50)),
"more-specific length override wins"
);
}
#[test]
fn get_color_with_more_specific_skipped_when_it_binds_different_name() {
let tokens = ThemeTokens::new()
.set_media(mq("(min-width: 80)"), "x", Color::literal(RColor::Red))
.set_media(mq("(min-width: 80) and (color)"), "other", Color::literal(RColor::Blue));
assert_eq!(
tokens.get_color_with("x", &ctx(100)),
Some(Color::literal(RColor::Red)),
"a more-specific query that does not bind `x` does not shadow `x`"
);
assert_eq!(
tokens.get_color_with("other", &ctx(100)),
Some(Color::literal(RColor::Blue))
);
}
#[test]
fn get_box_edges_resolves_named_token() {
let tokens = ThemeTokens::new().set("pad", BoxEdges::uniform(2));
assert_eq!(tokens.get_box_edges("pad"), Some(&BoxEdges::uniform(2)));
assert_eq!(
tokens.get_box_edges_with("pad", &MediaContext::default()),
Some(BoxEdges::uniform(2))
);
}
#[test]
fn get_box_edges_follows_var_chain() {
let edges = BoxEdges { top: 1, right: 2, bottom: 3, left: 4 };
let tokens = ThemeTokens::new()
.set("pad", Token::Var { name: "pad2".into() })
.set("pad2", edges);
assert_eq!(tokens.get_box_edges("pad"), Some(&edges));
}
#[test]
fn get_border_style_resolves_named_token() {
let tokens = ThemeTokens::new().set("bs", BorderStyle::Rounded);
assert_eq!(tokens.get_border_style("bs"), Some(&BorderStyle::Rounded));
assert_eq!(
tokens.get_border_style_with("bs", &MediaContext::default()),
Some(BorderStyle::Rounded)
);
}
#[test]
fn get_border_style_follows_var_chain() {
let tokens = ThemeTokens::new()
.set("bs", Token::Var { name: "bs2".into() })
.set("bs2", BorderStyle::Double);
assert_eq!(tokens.get_border_style("bs"), Some(&BorderStyle::Double));
}
#[test]
fn get_box_edges_with_media_specificity_override() {
let tokens = ThemeTokens::new()
.set_media(
mq("(min-width: 80)"),
"pad",
BoxEdges::uniform(1),
)
.set_media(
mq("(min-width: 80) and (color)"),
"pad",
BoxEdges::uniform(2),
);
assert_eq!(
tokens.get_box_edges_with("pad", &ctx(100)),
Some(BoxEdges::uniform(2)),
"more-specific media override wins for box-edges"
);
}
#[test]
fn get_border_style_with_media_override() {
let tokens = ThemeTokens::new()
.set("bs", BorderStyle::Single)
.set_media(mq("(min-width: 80)"), "bs", BorderStyle::Rounded);
assert_eq!(
tokens.get_border_style_with("bs", &ctx(100)),
Some(BorderStyle::Rounded)
);
assert_eq!(
tokens.get_border_style_with("bs", &ctx(40)),
Some(BorderStyle::Single)
);
}
#[test]
fn box_edges_token_is_not_a_color() {
let tokens = ThemeTokens::new().set("pad", BoxEdges::uniform(1));
assert_eq!(tokens.get_color("pad"), None);
}
#[test]
fn border_style_token_is_not_a_length() {
let tokens = ThemeTokens::new().set("bs", BorderStyle::Rounded);
assert_eq!(tokens.get_length("bs"), None);
}
}