textiler_core/theme/
parsing.rs1use 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
17pub 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}