sunbeam_ir/
parse.rs

1use crate::class_dictionary::{ClassDictionary, ClassDictionaryError};
2use crate::color::{ColorName, CssColor};
3use crate::font::FontName;
4use crate::{Classes, ConfigLookup, SunbeamConfig};
5use syn::parse::{Parse, ParseStream};
6use syn::LitStr;
7
8/// When validating the classes in a proc macro we don't have access to our SunbeamConfig
9/// (unless we made our proc macro non-deterministic and added an environment variable that can
10/// be used to lookup a SunbeamConfig file.. which we don't want to do), so we
11/// can't validate whether or not things like colors or screen sizes are defined in the config.
12/// Instead, when we run into things that need to be looked up in config we just assume that
13/// they're there. Then later during the end user's build script they'll use their Sunbeam config
14/// to do the full validation.
15/// This AlwaysSucceedsConfigLookup is used during procedural macro parsing.
16struct AlwaysSucceedsConfigLookup {
17    color: CssColor,
18    font_families: Vec<String>,
19}
20impl ConfigLookup for AlwaysSucceedsConfigLookup {
21    fn get_color(&self, _color_name: &ColorName) -> Option<&CssColor> {
22        Some(&self.color)
23    }
24
25    fn get_font(&self, _font_name: &FontName) -> Option<&Vec<String>> {
26        Some(&self.font_families)
27    }
28}
29
30impl Classes {
31    /// Parse classes from a string.
32    pub fn parse_str(
33        classes: &str,
34        config: &SunbeamConfig,
35    ) -> Result<Self, Vec<ClassDictionaryError>> {
36        Self::impl_parse_str(&classes, config)
37    }
38}
39
40impl Parse for Classes {
41    fn parse(input: ParseStream) -> syn::Result<Self> {
42        let classes_lit_str: LitStr = input.parse()?;
43        let classes: String = classes_lit_str.value();
44
45        let config_lookup = AlwaysSucceedsConfigLookup {
46            color: CssColor::new("some-color".to_string()).unwrap(),
47            font_families: vec![],
48        };
49
50        Self::impl_parse_str(&classes, &config_lookup).map_err(|errors| {
51            let mut errors = errors.into_iter();
52
53            let mut combined = syn::Error::new(classes_lit_str.span(), errors.next().unwrap());
54
55            while let Some(err) = errors.next() {
56                combined.combine(syn::Error::new(classes_lit_str.span(), err));
57            }
58
59            combined
60        })
61    }
62}
63
64impl Classes {
65    fn impl_parse_str(
66        classes: &str,
67        config_lookup: &dyn ConfigLookup,
68    ) -> Result<Classes, Vec<ClassDictionaryError>> {
69        let classes_split = classes.trim().split_whitespace();
70
71        let class_dict = ClassDictionary::new();
72
73        let mut classes = vec![];
74        let mut errors = vec![];
75
76        for class in classes_split {
77            match class_dict.get_class_definition(class, config_lookup) {
78                Ok(class_definition) => {
79                    classes.push(class_definition);
80                }
81                Err(err) => {
82                    errors.push(err);
83                }
84            };
85        }
86
87        if errors.len() > 0 {
88            return Err(errors);
89        }
90
91        Ok(Classes { classes })
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    /// Verify that we can parse a multi line CSS class name string.
100    #[test]
101    fn parse_multi_line_string() {
102        let css = r#"
103ml5
104mb6
105"#;
106        let config_lookup = always_succeeds_config();
107
108        let classes = Classes::impl_parse_str(css, &config_lookup).unwrap();
109        assert_eq!(classes.len(), 2);
110    }
111
112    fn always_succeeds_config() -> AlwaysSucceedsConfigLookup {
113        AlwaysSucceedsConfigLookup {
114            color: CssColor::new("some-color".to_string()).unwrap(),
115            font_families: vec![],
116        }
117    }
118}