use std::collections::HashMap;
use crate::color::OpalineColor;
use crate::error::OpalineError;
#[cfg(feature = "gradients")]
use crate::gradient::Gradient;
use crate::schema::{StyleDef, ThemeFile};
use crate::style::OpalineStyle;
#[derive(Debug, Clone)]
pub struct ResolvedTheme {
pub palette: HashMap<String, OpalineColor>,
pub tokens: HashMap<String, OpalineColor>,
pub styles: HashMap<String, OpalineStyle>,
#[cfg(feature = "gradients")]
pub gradients: HashMap<String, Gradient>,
}
pub fn resolve(theme_file: &ThemeFile) -> Result<ResolvedTheme, OpalineError> {
let palette = resolve_palette(&theme_file.palette)?;
let tokens = resolve_tokens(&theme_file.tokens, &palette)?;
let styles = resolve_styles(&theme_file.styles, &palette, &tokens)?;
#[cfg(feature = "gradients")]
let gradients = resolve_gradients(&theme_file.gradients, &palette, &tokens)?;
Ok(ResolvedTheme {
palette,
tokens,
styles,
#[cfg(feature = "gradients")]
gradients,
})
}
fn resolve_palette(
raw: &HashMap<String, String>,
) -> Result<HashMap<String, OpalineColor>, OpalineError> {
let mut palette = HashMap::with_capacity(raw.len());
for (name, hex) in raw {
let color = OpalineColor::from_hex(hex).map_err(|source| OpalineError::InvalidColor {
token: name.clone(),
source,
})?;
palette.insert(name.clone(), color);
}
Ok(palette)
}
fn resolve_tokens(
raw: &HashMap<String, String>,
palette: &HashMap<String, OpalineColor>,
) -> Result<HashMap<String, OpalineColor>, OpalineError> {
let mut resolved: HashMap<String, OpalineColor> = HashMap::with_capacity(raw.len());
for name in raw.keys() {
if !resolved.contains_key(name) {
let mut chain = Vec::new();
resolve_token(name, raw, palette, &mut resolved, &mut chain)?;
}
}
Ok(resolved)
}
fn resolve_token(
name: &str,
raw: &HashMap<String, String>,
palette: &HashMap<String, OpalineColor>,
resolved: &mut HashMap<String, OpalineColor>,
chain: &mut Vec<String>,
) -> Result<OpalineColor, OpalineError> {
if let Some(&color) = resolved.get(name) {
return Ok(color);
}
if chain.iter().any(|entry| entry == name) {
chain.push(name.to_string()); return Err(OpalineError::CircularReference {
token: name.to_string(),
chain: chain.clone(),
});
}
chain.push(name.to_string());
let Some(value) = raw.get(name) else {
return Err(OpalineError::UnresolvedToken {
token: name.to_string(),
reference: name.to_string(),
});
};
let color = if value.starts_with('#') {
OpalineColor::from_hex(value).map_err(|source| OpalineError::InvalidColor {
token: name.to_string(),
source,
})?
} else if let Some(&palette_color) = palette.get(value.as_str()) {
palette_color
} else if raw.contains_key(value.as_str()) {
resolve_token(value, raw, palette, resolved, chain)?
} else {
return Err(OpalineError::UnresolvedToken {
token: name.to_string(),
reference: value.clone(),
});
};
resolved.insert(name.to_string(), color);
Ok(color)
}
fn resolve_named_color_ref(
reference: &str,
palette: &HashMap<String, OpalineColor>,
tokens: &HashMap<String, OpalineColor>,
) -> Option<OpalineColor> {
tokens
.get(reference)
.copied()
.or_else(|| palette.get(reference).copied())
}
fn resolve_styles(
raw: &HashMap<String, StyleDef>,
palette: &HashMap<String, OpalineColor>,
tokens: &HashMap<String, OpalineColor>,
) -> Result<HashMap<String, OpalineStyle>, OpalineError> {
let mut styles = HashMap::with_capacity(raw.len());
for (name, def) in raw {
let fg = def.fg.as_ref().map(|r| {
if r.starts_with('#') {
OpalineColor::from_hex(r).map_err(|source| OpalineError::InvalidColor {
token: format!("{name}.fg"),
source,
})
} else {
resolve_named_color_ref(r, palette, tokens).ok_or_else(|| {
OpalineError::UnresolvedToken {
token: format!("{name}.fg"),
reference: r.clone(),
}
})
}
});
let fg = fg.transpose()?;
let bg = def.bg.as_ref().map(|r| {
if r.starts_with('#') {
OpalineColor::from_hex(r).map_err(|source| OpalineError::InvalidColor {
token: format!("{name}.bg"),
source,
})
} else {
resolve_named_color_ref(r, palette, tokens).ok_or_else(|| {
OpalineError::UnresolvedToken {
token: format!("{name}.bg"),
reference: r.clone(),
}
})
}
});
let bg = bg.transpose()?;
styles.insert(
name.clone(),
OpalineStyle {
fg,
bg,
bold: def.bold,
dim: def.dim,
italic: def.italic,
underline: def.underline,
slow_blink: def.slow_blink,
rapid_blink: def.rapid_blink,
reversed: def.reversed,
hidden: def.hidden,
crossed_out: def.crossed_out,
},
);
}
Ok(styles)
}
#[cfg(feature = "gradients")]
fn resolve_gradients(
raw: &HashMap<String, Vec<String>>,
palette: &HashMap<String, OpalineColor>,
tokens: &HashMap<String, OpalineColor>,
) -> Result<HashMap<String, Gradient>, OpalineError> {
let mut gradients = HashMap::with_capacity(raw.len());
for (name, stops) in raw {
if stops.is_empty() {
return Err(OpalineError::EmptyGradient);
}
let mut colors = Vec::with_capacity(stops.len());
for (i, stop) in stops.iter().enumerate() {
let color = if stop.starts_with('#') {
OpalineColor::from_hex(stop).map_err(|source| OpalineError::InvalidColor {
token: format!("{name}[{i}]"),
source,
})?
} else {
resolve_named_color_ref(stop, palette, tokens).ok_or_else(|| {
OpalineError::UnresolvedToken {
token: format!("{name}[{i}]"),
reference: stop.clone(),
}
})?
};
colors.push(color);
}
gradients.insert(name.clone(), Gradient::new(colors));
}
Ok(gradients)
}