use std::fmt;
use std::fs::File;
use std::path::Path;
use serde::de::{MapAccess, Visitor};
use serde::{Deserialize, Deserializer, de};
use crate::error::ZaloResult;
use crate::themes::compiled::CompiledTheme;
#[derive(Debug, Clone, Deserialize, Default)]
pub struct TokenColorSettings {
pub foreground: Option<String>,
pub background: Option<String>,
#[serde(rename = "fontStyle")]
pub font_style: Option<String>,
}
impl TokenColorSettings {
pub fn foreground(&self) -> Option<&str> {
self.foreground.as_deref().filter(|s| *s != "inherit")
}
pub fn background(&self) -> Option<&str> {
self.background.as_deref().filter(|s| *s != "inherit")
}
}
fn deserialize_string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
use std::fmt;
struct ScopeVisitor;
impl<'de> Visitor<'de> for ScopeVisitor {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or array of strings")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value.split(',').map(|s| s.trim().to_string()).collect())
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut vec = Vec::new();
while let Some(item) = seq.next_element::<String>()? {
vec.push(item);
}
Ok(vec)
}
}
deserializer.deserialize_any(ScopeVisitor)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Colors {
pub foreground: String,
pub background: String,
pub highlight_background: Option<String>,
pub line_number_foreground: Option<String>,
}
impl<'de> Deserialize<'de> for Colors {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ColorsVisitor;
impl<'de> Visitor<'de> for ColorsVisitor {
type Value = Colors;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("struct Colors")
}
fn visit_map<V>(self, mut map: V) -> Result<Colors, V::Error>
where
V: MapAccess<'de>,
{
let mut foreground = None;
let mut background = None;
let mut highlight_background = None;
let mut line_number_foreground = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"foreground" | "editor.foreground" => {
if foreground.is_none() {
foreground = Some(map.next_value()?);
} else {
let _: de::IgnoredAny = map.next_value()?;
}
}
"background" | "editor.background" => {
if background.is_none() {
background = Some(map.next_value()?);
} else {
let _: de::IgnoredAny = map.next_value()?;
}
}
"editor.lineHighlightBackground" => {
highlight_background = Some(map.next_value()?);
}
"editorLineNumber.foreground" => {
line_number_foreground = Some(map.next_value()?);
}
_ => {
let _: de::IgnoredAny = map.next_value()?;
}
}
}
let foreground = foreground
.ok_or_else(|| de::Error::missing_field("foreground or editor.foreground"))?;
let background = background
.ok_or_else(|| de::Error::missing_field("background or editor.background"))?;
Ok(Colors {
foreground,
background,
highlight_background,
line_number_foreground,
})
}
}
deserializer.deserialize_map(ColorsVisitor)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct TokenColorRule {
#[serde(deserialize_with = "deserialize_string_or_vec", default)]
pub scope: Vec<String>,
#[serde(default)]
pub settings: TokenColorSettings,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RawTheme {
pub name: String,
#[serde(rename = "type")]
pub kind: Option<String>,
pub colors: Colors,
#[serde(rename = "tokenColors")]
pub token_colors: Vec<TokenColorRule>,
}
impl RawTheme {
pub fn load_from_file<P: AsRef<Path>>(path: P) -> ZaloResult<Self> {
let file = File::open(path)?;
let theme = serde_json::from_reader(file)?;
Ok(theme)
}
pub fn compile(self) -> ZaloResult<CompiledTheme> {
CompiledTheme::from_raw_theme(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_handle_all_kinds_of_scope() {
let theme = RawTheme::load_from_file("src/fixtures/themes/all_scope_styles.json").unwrap();
assert_eq!(theme.name, "test");
assert_eq!(theme.token_colors.len(), 5);
let expected_scopes = [
vec![],
vec!["comment", "markup.quote.markdown"],
vec![
"variable.language.this",
"variable.language.self",
"variable.language.super",
],
vec!["string > source", "string embedded"],
vec!["string > source", "string embedded"],
];
let expected_foregrounds = [
Some("#D5CED9"), Some("#A0A1A7cc"), Some("#d699b6"), Some("#383A42"), Some("#383A42"), ];
for (i, (expected_scope, expected_fg)) in expected_scopes
.iter()
.zip(expected_foregrounds.iter())
.enumerate()
{
let rule = &theme.token_colors[i];
assert_eq!(
rule.scope.len(),
expected_scope.len(),
"Rule {} scope count mismatch",
i
);
assert_eq!(
rule.scope, *expected_scope,
"Rule {} scope content mismatch",
i
);
assert_eq!(
rule.settings.foreground(),
*expected_fg,
"Rule {} foreground color mismatch",
i
);
}
let compiled_theme = theme.compile().expect("Failed to compile test theme");
assert_eq!(compiled_theme.name, "test");
}
}