css_in_rust_next/style/
ast.rs

1// Copyright © 2020 Lukas Wagner
2
3/// A scope represents a media query or all content not in a media query.
4///
5/// As an example:
6/// ```css
7/// /* BEGIN Scope */
8/// .wrapper {
9///     width: 100vw;
10/// }
11/// /* END Scope */
12/// /* BEGIN Scope */
13/// @media only screen and (min-width: 1000px) {
14///     .wrapper {
15///         width: 1000px;
16///     }
17/// }
18/// /* END Scope */
19/// ```
20#[derive(Debug, Clone)]
21pub(crate) struct Scope {
22    pub(crate) condition: Option<String>,
23    pub(crate) stylesets: Vec<ScopeContent>,
24}
25
26impl ToCss for Scope {
27    fn to_css(&self, class_name: String) -> String {
28        let stylesets = self.stylesets.clone();
29        let stylesets_css = stylesets
30            .into_iter()
31            .map(|styleset| match styleset {
32                ScopeContent::Block(block) => block.to_css(class_name.clone()),
33                ScopeContent::Rule(rule) => rule.to_css(class_name.clone()),
34                // ScopeContent::Scope(scope) => scope.to_css(class_name.clone()),
35            })
36            .fold(String::new(), |acc, css_part| {
37                format!("{}{}\n", acc, css_part)
38            });
39        match &self.condition {
40            Some(condition) => format!("{} {{\n{}}}", condition, stylesets_css),
41            None => stylesets_css.trim().to_string(),
42        }
43    }
44}
45
46/// Everything that can reside inside a scope.
47#[derive(Debug, Clone)]
48pub(crate) enum ScopeContent {
49    Block(Block),
50    Rule(Rule),
51    // e.g. media rules nested in support rules and vice versa
52    // Scope(Scope),
53}
54
55/// A block is a set of css properties that apply to elements that
56/// match the condition.
57///
58/// E.g.:
59/// ```css
60/// .inner {
61///     color: red;
62/// }
63/// ```
64#[derive(Debug, Clone)]
65pub(crate) struct Block {
66    pub(crate) condition: Option<String>,
67    pub(crate) style_attributes: Vec<StyleAttribute>,
68}
69
70impl ToCss for Block {
71    fn to_css(&self, class_name: String) -> String {
72        let condition = match &self.condition {
73            Some(condition) => format!(" {}", condition),
74            None => String::new(),
75        };
76        let style_property_css = self
77            .style_attributes
78            .clone()
79            .into_iter()
80            .map(|style_property| style_property.to_css(class_name.clone()))
81            .fold(String::new(), |acc, css_part| {
82                format!("{}\n{}", acc, css_part)
83            });
84        if condition.contains('&') {
85            format!(
86                "{} {{{}\n}}",
87                condition.replace('&', format!(".{}", class_name).as_str()),
88                style_property_css
89            )
90        } else {
91            format!(".{}{} {{{}\n}}", class_name, condition, style_property_css)
92        }
93    }
94}
95
96/// A simple CSS proprerty in the form of a key value pair.
97///
98/// E.g.: `color: red`
99#[derive(Debug, Clone)]
100pub(crate) struct StyleAttribute {
101    pub(crate) key: String,
102    pub(crate) value: String,
103}
104
105impl ToCss for StyleAttribute {
106    fn to_css(&self, _: String) -> String {
107        format!("{}: {};", self.key, self.value)
108    }
109}
110
111/// A rule is everything that does not contain any properties.
112///
113/// An example would be `@keyframes`
114#[derive(Debug, Clone)]
115pub(crate) struct Rule {
116    pub(crate) condition: String,
117    pub(crate) content: Vec<RuleContent>,
118}
119
120impl ToCss for Rule {
121    fn to_css(&self, class_name: String) -> String {
122        format!(
123            "{} {{\n{}\n}}",
124            self.condition,
125            self.content
126                .iter()
127                .map(|rc| rc.to_css(class_name.clone()))
128                .collect::<Vec<String>>()
129                .concat()
130        )
131    }
132}
133
134/// Everything that can be inside a rule.
135#[derive(Debug, Clone)]
136pub(crate) enum RuleContent {
137    String(String),
138    CurlyBraces(Vec<RuleContent>),
139}
140
141impl ToCss for RuleContent {
142    fn to_css(&self, class_name: String) -> String {
143        match self {
144            RuleContent::String(s) => s.to_string(),
145            RuleContent::CurlyBraces(content) => format!(
146                "{{{}}}",
147                content
148                    .iter()
149                    .map(|rc| rc.to_css(class_name.clone()))
150                    .collect::<Vec<String>>()
151                    .concat()
152            ),
153        }
154    }
155}
156
157/// Structs implementing this trait should be able to turn into
158/// a part of a CSS style sheet.
159pub trait ToCss {
160    fn to_css(&self, class_name: String) -> String;
161}
162
163#[cfg(all(test, target_arch = "wasm32"))]
164mod tests {
165    use super::{Block, Rule, Scope, ScopeContent, StyleAttribute, ToCss};
166    use wasm_bindgen_test::*;
167
168    #[wasm_bindgen_test]
169    fn test_scope_building_without_condition() {
170        let test_block = Scope {
171            condition: None,
172            stylesets: vec![
173                ScopeContent::Block(Block {
174                    condition: None,
175                    style_attributes: vec![StyleAttribute {
176                        key: String::from("width"),
177                        value: String::from("100vw"),
178                    }],
179                }),
180                ScopeContent::Block(Block {
181                    condition: Some(String::from(".inner")),
182                    style_attributes: vec![StyleAttribute {
183                        key: String::from("background-color"),
184                        value: String::from("red"),
185                    }],
186                }),
187                ScopeContent::Rule(Rule {
188                    condition: String::from("@keyframes move"),
189                    content: String::from(
190                        r#"from {
191width: 100px;
192}
193to {
194width: 200px;
195}"#,
196                    ),
197                }),
198            ],
199        };
200        assert_eq!(
201            test_block.to_css(String::from("test")),
202            r#".test {
203width: 100vw;
204}
205.test .inner {
206background-color: red;
207}
208@keyframes move {
209from {
210width: 100px;
211}
212to {
213width: 200px;
214}
215}"#
216        );
217    }
218
219    #[wasm_bindgen_test]
220    fn test_scope_building_with_condition() {
221        let test_block = Scope {
222            condition: Some(String::from("@media only screen and (min-width: 1000px)")),
223            stylesets: vec![
224                ScopeContent::Block(Block {
225                    condition: None,
226                    style_attributes: vec![StyleAttribute {
227                        key: String::from("width"),
228                        value: String::from("100vw"),
229                    }],
230                }),
231                ScopeContent::Block(Block {
232                    condition: Some(String::from(".inner")),
233                    style_attributes: vec![StyleAttribute {
234                        key: String::from("background-color"),
235                        value: String::from("red"),
236                    }],
237                }),
238                ScopeContent::Rule(Rule {
239                    condition: String::from("@keyframes move"),
240                    content: String::from(
241                        r#"from {
242width: 100px;
243}
244to {
245width: 200px;
246}"#,
247                    ),
248                }),
249            ],
250        };
251        assert_eq!(
252            test_block.to_css(String::from("test")),
253            r#"@media only screen and (min-width: 1000px) {
254.test {
255width: 100vw;
256}
257.test .inner {
258background-color: red;
259}
260@keyframes move {
261from {
262width: 100px;
263}
264to {
265width: 200px;
266}
267}
268}"#
269        );
270    }
271}