use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::token::TokenKind;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Theme {
pub text_color: Option<String>,
pub background_color: Option<String>,
pub line_number_color: Option<String>,
pub line_number_background_color: Option<String>,
pub text_styles: BTreeMap<String, TokenStyle>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct TokenStyle {
pub text_color: Option<String>,
pub background_color: Option<String>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
}
impl Theme {
pub fn from_json(bytes: &[u8]) -> Result<Self, Error> {
serde_json::from_slice(bytes).map_err(|e| Error::Parse(e.to_string()))
}
pub fn to_json(&self) -> Result<String, Error> {
let mut buf = Vec::new();
let indent = b" ";
let formatter = serde_json::ser::PrettyFormatter::with_indent(indent);
let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
self.serialize(&mut ser)
.map_err(|e| Error::Parse(e.to_string()))?;
String::from_utf8(buf).map_err(|e| Error::Parse(e.to_string()))
}
#[must_use]
pub fn style_for(&self, kind: TokenKind) -> Option<&TokenStyle> {
self.text_styles.get(kind.style_key())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
Parse(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Parse(msg) => write!(f, "invalid theme: {msg}"),
}
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrips_a_minimal_theme() {
let json = r##"{
"text-color": null,
"background-color": null,
"line-number-color": "#aaaaaa",
"line-number-background-color": null,
"text-styles": {
"Keyword": {
"text-color": "#007020",
"background-color": null,
"bold": true,
"italic": false,
"underline": false
}
}
}"##;
let theme = Theme::from_json(json.as_bytes()).expect("parse");
assert_eq!(theme.to_json().expect("serialize"), json);
}
#[test]
fn resolves_style_by_kind() {
let json = r##"{
"text-color": null,
"background-color": null,
"line-number-color": null,
"line-number-background-color": null,
"text-styles": {
"Keyword": {
"text-color": "#007020",
"background-color": null,
"bold": true,
"italic": false,
"underline": false
}
}
}"##;
let theme = Theme::from_json(json.as_bytes()).expect("parse");
assert_eq!(
theme
.style_for(TokenKind::Keyword)
.expect("keyword")
.text_color,
Some("#007020".to_string())
);
assert!(theme.style_for(TokenKind::Comment).is_none());
}
}