use serde::{Deserialize, Serialize};
use crate::error::{Error, ZaloResult};
use crate::themes::Color;
use crate::themes::font_style::FontStyle;
use crate::themes::raw::{RawTheme, TokenColorSettings};
use crate::themes::selector::{ThemeSelector, parse_selector};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Specificity {
scope_depth: u32,
parent_count: u32,
}
impl Specificity {
fn calculate(selector: &ThemeSelector) -> Self {
let target_scope_string = selector.target_scope.build_string();
let scope_depth = if target_scope_string.is_empty() {
0
} else {
(target_scope_string.matches('.').count() + 1) as u32
};
let parent_count = selector.parent_scopes.len() as u32;
Specificity {
scope_depth,
parent_count,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
pub struct Style {
pub foreground: Color,
pub background: Color,
pub font_style: FontStyle,
}
impl Default for Style {
fn default() -> Style {
Style {
foreground: Color::BLACK,
background: Color::WHITE,
font_style: FontStyle::default(),
}
}
}
impl Style {
pub(crate) fn has_decorations(&self) -> bool {
self.font_style.contains(FontStyle::UNDERLINE)
|| self.font_style.contains(FontStyle::STRIKETHROUGH)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct StyleModifier {
pub foreground: Option<Color>,
pub background: Option<Color>,
pub font_style: Option<FontStyle>,
}
impl TryFrom<TokenColorSettings> for StyleModifier {
type Error = Error;
fn try_from(settings: TokenColorSettings) -> ZaloResult<Self> {
let foreground = if let Some(s) = settings.foreground() {
Some(Color::from_hex(s)?)
} else {
None
};
let background = if let Some(s) = settings.background() {
Some(Color::from_hex(s)?)
} else {
None
};
let font_style = settings.font_style.map(|s| FontStyle::from_theme_str(&s));
Ok(Self {
foreground,
background,
font_style,
})
}
}
impl StyleModifier {
pub fn apply_to(&self, base: &Style) -> Style {
Style {
foreground: self.foreground.unwrap_or(base.foreground),
background: self.background.unwrap_or(base.background),
font_style: self.font_style.unwrap_or(base.font_style),
}
}
pub fn has_properties(&self) -> bool {
self.foreground.is_some() || self.background.is_some() || self.font_style.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ThemeType {
Light,
#[default]
Dark,
}
impl ThemeType {
pub fn from_theme_str(s: &str) -> ThemeType {
if s.eq_ignore_ascii_case("light") {
ThemeType::Light
} else {
ThemeType::Dark
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompiledThemeRule {
pub selector: ThemeSelector,
pub style_modifier: StyleModifier,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompiledTheme {
pub name: String,
pub(crate) theme_type: ThemeType,
pub default_style: Style,
pub highlight_background_color: Option<Color>,
pub line_number_foreground: Option<Color>,
pub(crate) rules: Vec<CompiledThemeRule>,
}
impl CompiledTheme {
pub(crate) fn from_raw_theme(raw_theme: RawTheme) -> ZaloResult<Self> {
let theme_type = raw_theme
.kind
.map(|s| ThemeType::from_theme_str(&s))
.unwrap_or_default();
let foreground = Color::from_hex(&raw_theme.colors.foreground)?;
let background = Color::from_hex(&raw_theme.colors.background)?;
let highlight_background_color = if let Some(bg) = raw_theme.colors.highlight_background {
Some(Color::from_hex(&bg)?)
} else {
None
};
let line_number_foreground = if let Some(fg) = raw_theme.colors.line_number_foreground {
Some(Color::from_hex(&fg)?)
} else {
None
};
let mut default_style = Style {
foreground,
background,
font_style: FontStyle::default(),
};
let mut rules_with_specificity = Vec::new();
for token_rule in raw_theme.token_colors {
if token_rule.scope.is_empty() {
if let Some(fg) = token_rule.settings.foreground() {
default_style.foreground = Color::from_hex(fg)?;
}
if let Some(bg) = token_rule.settings.background() {
default_style.background = Color::from_hex(bg)?;
}
continue;
}
let mut selectors = Vec::new();
for scope_pattern in &token_rule.scope {
if let Some(selector) = parse_selector(scope_pattern) {
selectors.push(selector);
} else {
#[cfg(feature = "debug")]
log::warn!(
"Failed to parse theme selector: '{scope_pattern}' in theme {}",
raw_theme.name
);
}
}
if !selectors.is_empty() {
let style_modifier = StyleModifier::try_from(token_rule.settings.clone())?;
for selector in selectors {
let specificity = Specificity::calculate(&selector);
rules_with_specificity.push((
CompiledThemeRule {
selector,
style_modifier,
},
specificity,
));
}
}
}
rules_with_specificity.sort_by(|a, b| a.1.cmp(&b.1));
let rules = rules_with_specificity
.into_iter()
.map(|(rule, _)| rule)
.collect();
Ok(CompiledTheme {
name: raw_theme.name,
theme_type,
default_style,
highlight_background_color,
line_number_foreground,
rules,
})
}
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
#[test]
fn can_load_and_compile_all_shiki_themes() {
let entries = fs::read_dir("grammars-themes/packages/tm-themes/themes")
.expect("Failed to read grammars directory");
for entry in entries {
let entry = entry.expect("Failed to read directory entry");
let path = entry.path();
println!("{:?}", path);
RawTheme::load_from_file(&path)
.unwrap()
.compile()
.unwrap_or_else(|_| panic!("Failed to compile theme: {path:?}"));
}
}
#[test]
fn can_load_default_from_token_colors() {
let theme = RawTheme::load_from_file("src/fixtures/themes/all_scope_styles.json").unwrap();
let compiled = CompiledTheme::from_raw_theme(theme).unwrap();
assert_eq!(compiled.default_style.background.as_hex(), "#23262E");
assert_eq!(compiled.default_style.foreground.as_hex(), "#D5CED9");
}
}