textiler_core/theme/
parsing.rs

1//! Allows loading of themes from json files
2
3use std::io;
4use std::io::Read;
5
6use indexmap::IndexMap;
7use serde::Deserialize;
8
9use crate::theme::gradient::Gradient;
10use crate::theme::palette::Palette;
11use crate::theme::sx::SxValue;
12use crate::theme::typography::TypographyLevel;
13use crate::theme::{Color, Theme, PALETTE_SELECTOR_REGEX};
14use crate::utils::bounded_float::BoundedFloat;
15use crate::{sx, Sx};
16
17/// Parses a theme from a reader
18pub fn from_reader<R: Read>(reader: R) -> Result<Theme, io::Error> {
19    let json: ThemeJson = serde_json::from_reader(reader)?;
20    Ok(from_theme_json(json))
21}
22
23pub fn from_str(reader: &str) -> Result<Theme, io::Error> {
24    let json: ThemeJson = serde_json::from_str(reader)?;
25    Ok(from_theme_json(json))
26}
27
28fn adjust_color(color: Color, theme: &Theme) -> Color {
29    match color {
30        Color::Var { var, fallback } if PALETTE_SELECTOR_REGEX.is_match(&var) => {
31            let captures = PALETTE_SELECTOR_REGEX
32                .captures(&var)
33                .expect("must pass here");
34            let palette = &captures["palette"];
35            let selector = &captures["selector"];
36
37            Color::Var {
38                var: theme.palette_var(palette, selector),
39                fallback,
40            }
41        }
42        color => color,
43    }
44}
45fn from_theme_json(json: ThemeJson) -> Theme {
46    trace!("json: {:#?}", json);
47    let mut theme = json
48        .prefix
49        .map(|prefix| Theme::with_prefix(prefix))
50        .unwrap_or_else(Theme::new);
51
52    if let Some(typography_scale) = json.typography {
53        for (level, scale) in typography_scale.levels {
54            theme.typography_mut().insert(level, scale.to_sx().into());
55        }
56    }
57
58    for (palette_name, def) in json.palettes {
59        let mut palette = Palette::new();
60        if let Some(GradientJson {
61            points: gradient,
62            mode,
63        }) = def.gradient
64        {
65            use GradientMode::*;
66            let gradient: Gradient = match mode {
67                None => gradient,
68                Some(Hsl) => gradient
69                    .into_iter()
70                    .map(|(pt, c)| (pt, c.to_hsla_color().expect("could not convert to hsla")))
71                    .collect(),
72                Some(Rgb) => gradient
73                    .into_iter()
74                    .map(|(pt, c)| (pt, c.to_rgba_color().expect("could not convert to rgba")))
75                    .collect(),
76            };
77            for i in 0..=10 {
78                let as_float = BoundedFloat::new(i as f32 / 10.0).expect("must be valid");
79                palette.insert_constant(&format!("{:03}", i * 10), gradient.get(as_float));
80            }
81        }
82        if let Some(selectors) = def.selectors {
83            for (selector, color) in selectors {
84                match color {
85                    SelectorJson::Const(c) => {
86                        let c = adjust_color(c, &theme);
87                        palette.insert_constant(&selector, c);
88                    }
89                    SelectorJson::DarkLight { dark, light } => {
90                        let dark = adjust_color(dark, &theme);
91                        let light = adjust_color(light, &theme);
92                        palette.insert_by_mode(&selector, dark, light);
93                    }
94                }
95            }
96        }
97
98        theme.insert_palette(palette_name, palette);
99    }
100    theme
101}
102
103#[derive(Debug, Deserialize)]
104struct ThemeJson {
105    prefix: Option<String>,
106    palettes: IndexMap<String, PaletteJson>,
107    typography: Option<TypographyScaleJson>,
108}
109
110#[derive(Debug, Deserialize)]
111#[serde(rename_all = "lowercase")]
112enum GradientMode {
113    Hsl,
114    Rgb,
115}
116
117#[derive(Debug, Deserialize)]
118struct PaletteJson {
119    gradient: Option<GradientJson>,
120    selectors: Option<IndexMap<String, SelectorJson>>,
121}
122
123#[derive(Debug, Deserialize)]
124struct GradientJson {
125    points: Gradient,
126    mode: Option<GradientMode>,
127}
128
129#[derive(Debug, Deserialize)]
130#[serde(untagged)]
131enum SelectorJson {
132    Const(Color),
133    DarkLight { dark: Color, light: Color },
134}
135
136#[derive(Debug, Deserialize)]
137#[serde(transparent)]
138struct TypographyScaleJson {
139    levels: IndexMap<TypographyLevel, SxJson>,
140}
141
142#[derive(Debug, Deserialize)]
143#[serde(transparent)]
144struct SxJson {
145    mapping: IndexMap<String, SxJsonValue>,
146}
147
148impl SxJson {
149    fn to_sx(&self) -> Sx {
150        let mut sx = sx! {};
151        for (key, value) in &self.mapping {
152            let sx_value = match value {
153                SxJsonValue::String(lit) => SxValue::CssLiteral(lit.clone()),
154                SxJsonValue::Nested(nested) => SxValue::Nested(nested.to_sx()),
155                SxJsonValue::Boolean(b) => SxValue::CssLiteral(b.to_string()),
156                SxJsonValue::Int(i) => SxValue::Integer(*i),
157                SxJsonValue::Float(f) => SxValue::Float(*f),
158            };
159            sx.insert(key.to_owned(), sx_value);
160        }
161        sx
162    }
163}
164
165#[derive(Debug, Deserialize)]
166#[serde(untagged)]
167enum SxJsonValue {
168    String(String),
169    Boolean(bool),
170    Int(i32),
171    Float(f32),
172    Nested(SxJson),
173}
174
175#[cfg(test)]
176mod tests {
177    use crate::theme::parsing::from_str;
178
179    #[test]
180    fn parse_theme_json() {
181        let json = include_str!("./theme.json");
182        let parsed = from_str(json).expect("could not parse");
183
184        println!("parsed: {:#?}", parsed);
185    }
186}