sunbeam_build/
lib.rs

1//! Parse css!(...) macro invocations and generate a CSS stylesheet.
2
3#![deny(missing_docs)]
4
5use std::path::Path;
6
7use syn::parse::{Parse, ParseStream};
8use syn::{Expr, ImplItem, Item, LitStr, Macro, Stmt};
9
10pub use sunbeam_ir::Classes;
11pub use sunbeam_ir::SunbeamConfig;
12
13pub use self::files_in_dir::files_in_dir_recursive_ending_with;
14
15mod files_in_dir;
16
17/// An error while parsing css!(..) calls.
18#[derive(Debug, thiserror::Error)]
19pub enum ParseCssError {
20    // TODO
21}
22
23/// Parse the classes found in css!(...) macro calls inside of the given Rust files.
24// We first parse the file into a syn::File which we then iterate through in order to find macro
25// calls. This approach avoids accidentally treating comments as macro calls.
26pub fn parse_rust_files(
27    rust_source_files: impl IntoIterator<Item = impl AsRef<Path>>,
28    config: &SunbeamConfig,
29) -> Result<Classes, ParseCssError> {
30    let mut all_classes = Classes::new();
31
32    for path in rust_source_files {
33        let path = path.as_ref();
34        let file_contents = std::fs::read_to_string(path).unwrap();
35
36        let file: syn::File = syn::parse_str(&file_contents).unwrap();
37
38        for item in file.items {
39            maybe_extend_classes(&mut all_classes, parse_item(item, config));
40        }
41    }
42
43    Ok(all_classes)
44}
45
46fn maybe_extend_classes(all_classes: &mut Classes, newly_parsed: Option<Classes>) {
47    if let Some(classes) = newly_parsed {
48        all_classes.extend(classes);
49    }
50}
51
52fn parse_item(item: Item, config: &SunbeamConfig) -> Option<Classes> {
53    let mut classes = Classes::new();
54
55    match item {
56        Item::Const(item_const) => parse_expression(*item_const.expr, config),
57        Item::Fn(item_fn) => {
58            parse_statements(&mut classes, config, item_fn.block.stmts);
59            Some(classes)
60        }
61        Item::Macro(item_macro) => {
62            let tokens = item_macro.mac.tokens;
63            let inner_macros = syn::parse2::<MacrosInsideMacro>(tokens).unwrap();
64            maybe_extend_classes(&mut classes, Some(inner_macros.into_classes(config)));
65
66            Some(classes)
67        }
68        Item::Impl(item_impl) => {
69            for impl_item in item_impl.items {
70                match impl_item {
71                    ImplItem::Const(impl_item_const) => {
72                        if let Some(c) = parse_expression(impl_item_const.expr, config) {
73                            classes.extend(c);
74                        }
75                    }
76                    ImplItem::Method(impl_item_method) => {
77                        parse_statements(&mut classes, config, impl_item_method.block.stmts);
78                    }
79                    _ => {}
80                }
81            }
82
83            Some(classes)
84        }
85        _ => None,
86    }
87}
88
89fn parse_expressions(
90    expressions: impl IntoIterator<Item = Expr>,
91    config: &SunbeamConfig,
92) -> Classes {
93    let mut classes = Classes::new();
94
95    for expr in expressions.into_iter() {
96        if let Some(c) = parse_expression(expr, config) {
97            classes.extend(c);
98        }
99    }
100
101    classes
102}
103
104fn parse_expression(expr: Expr, config: &SunbeamConfig) -> Option<Classes> {
105    match expr {
106        Expr::Call(tokens) => {
107            let classes = parse_expressions(tokens.args, config);
108            Some(classes)
109        }
110        Expr::MethodCall(tokens) => {
111            let mut classes = parse_expressions(tokens.args, config);
112            if let Some(receiver) = parse_expression(*tokens.receiver, config) {
113                classes.extend(receiver);
114            }
115            Some(classes)
116        }
117        Expr::Macro(macro_expr) => parse_macro(macro_expr.mac, config),
118        Expr::Array(expr_array) => {
119            let classes = parse_expressions(expr_array.elems, config);
120            Some(classes)
121        }
122        Expr::If(expr_if) => {
123            let mut classes = Classes::new();
124            parse_statements(&mut classes, config, expr_if.then_branch.stmts);
125
126            if let Some((_, else_branch)) = expr_if.else_branch {
127                if let Some(c) = parse_expression(*else_branch, config) {
128                    classes.extend(c);
129                }
130            }
131
132            Some(classes)
133        }
134        Expr::Block(expr_block) => {
135            let mut classes = Classes::new();
136            parse_statements(&mut classes, config, expr_block.block.stmts);
137
138            Some(classes)
139        }
140        Expr::Closure(expr_closure) => parse_expression(*expr_closure.body, config),
141        Expr::AssignOp(assign_op) => parse_expression(*assign_op.right, config),
142        Expr::Reference(expr_ref) => parse_expression(*expr_ref.expr, config),
143        _ => None,
144    }
145}
146
147fn parse_statements(classes: &mut Classes, config: &SunbeamConfig, stmts: Vec<Stmt>) {
148    for statement in stmts {
149        match statement {
150            Stmt::Local(local) => {
151                if let Some(init) = local.init {
152                    maybe_extend_classes(classes, parse_expression(*init.1, config));
153                }
154            }
155            Stmt::Item(item) => {
156                maybe_extend_classes(classes, parse_item(item, config));
157            }
158            Stmt::Expr(expr) => {
159                maybe_extend_classes(classes, parse_expression(expr, config));
160            }
161            Stmt::Semi(expr, _) => {
162                maybe_extend_classes(classes, parse_expression(expr, config));
163            }
164        }
165    }
166}
167
168fn parse_macro(mac: Macro, config: &SunbeamConfig) -> Option<Classes> {
169    let last_segment = mac.path.segments.last()?;
170    if last_segment.ident.to_string() == "css" {
171        let classes = mac.tokens;
172        let classes: LitStr = syn::parse2(classes).unwrap();
173        let classes = Classes::parse_str(&classes.value(), config).unwrap();
174
175        Some(classes)
176    } else {
177        let tokens = mac.tokens;
178        let inner_macros = syn::parse2::<MacrosInsideMacro>(tokens).unwrap();
179        Some(inner_macros.into_classes(config))
180    }
181}
182
183// Used to find macro calls inside of a macro.
184// For example, the css!(...) in `html! { <div class=css!(...)></div> }`
185struct MacrosInsideMacro {
186    inner_macros: Vec<Macro>,
187}
188impl MacrosInsideMacro {
189    fn into_classes(self, config: &SunbeamConfig) -> Classes {
190        let mut inner_classes = Classes::new();
191
192        for mac in self.inner_macros {
193            if let Some(classes) = parse_macro(mac, config) {
194                maybe_extend_classes(&mut inner_classes, Some(classes));
195                continue;
196            }
197        }
198
199        inner_classes
200    }
201}
202impl Parse for MacrosInsideMacro {
203    fn parse(input: ParseStream) -> syn::Result<Self> {
204        let mut inner_macros = vec![];
205
206        while !input.is_empty() {
207            let fork = input.fork();
208
209            if let Ok(mac) = fork.parse::<Macro>() {
210                inner_macros.push(mac);
211                input.parse::<Macro>().unwrap();
212            }
213
214            advance_input_by_one_token(input);
215        }
216
217        Ok(MacrosInsideMacro { inner_macros })
218    }
219}
220
221// Start: token1 token2 token3
222// End: token2 token3
223fn advance_input_by_one_token(input: ParseStream) {
224    input
225        .step(|cursor| {
226            let rest = *cursor;
227
228            if let Some((_tt, next)) = rest.token_tree() {
229                Ok(((), next))
230            } else {
231                Ok(((), rest))
232            }
233        })
234        .unwrap();
235}
236
237#[cfg(test)]
238mod tests {
239    use sunbeam_ir::{Modifiers, RetrievedClassDefinition};
240
241    use super::*;
242
243    /// Verify that we we can parse out the css!(...) from a Rust file.
244    /// Our Rust file uses the css!(...) macro in many different kinds of places
245    /// (such as inside of an impl block.. or nested inside another macro) so that we can confirm
246    /// that we are properly parsing out css!(...) calls.
247    #[test]
248    fn parse_css_macro_calls_from_rust_file() {
249        let files = ["src/fixtures/valid-css-classes.rs"];
250
251        let parsed = parse_rust_files(files, &SunbeamConfig::default()).unwrap();
252
253        for expected in [
254            RetrievedClassDefinition::new(
255                "ml1".to_string(),
256                Margin::Left(1).to_css_definition(),
257                Modifiers::default(),
258            ),
259            RetrievedClassDefinition::new(
260                "mr2".to_string(),
261                Margin::Right(2).to_css_definition(),
262                Modifiers::default(),
263            ),
264            RetrievedClassDefinition::new(
265                "mt3".to_string(),
266                Margin::Top(3).to_css_definition(),
267                Modifiers::default(),
268            ),
269            RetrievedClassDefinition::new(
270                "mb4".to_string(),
271                Margin::Bottom(4).to_css_definition(),
272                Modifiers::default(),
273            ),
274            RetrievedClassDefinition::new(
275                "mb5".to_string(),
276                Margin::Bottom(5).to_css_definition(),
277                Modifiers::default(),
278            ),
279            RetrievedClassDefinition::new(
280                "mb6".to_string(),
281                Margin::Bottom(6).to_css_definition(),
282                Modifiers::default(),
283            ),
284            RetrievedClassDefinition::new(
285                "mb7".to_string(),
286                Margin::Bottom(7).to_css_definition(),
287                Modifiers::default(),
288            ),
289            RetrievedClassDefinition::new(
290                "mb8".to_string(),
291                Margin::Bottom(8).to_css_definition(),
292                Modifiers::default(),
293            ),
294            RetrievedClassDefinition::new(
295                "mb9".to_string(),
296                Margin::Bottom(9).to_css_definition(),
297                Modifiers::default(),
298            ),
299            RetrievedClassDefinition::new(
300                "mb10".to_string(),
301                Margin::Bottom(10).to_css_definition(),
302                Modifiers::default(),
303            ),
304            RetrievedClassDefinition::new(
305                "mb11".to_string(),
306                Margin::Bottom(11).to_css_definition(),
307                Modifiers::default(),
308            ),
309            RetrievedClassDefinition::new(
310                "mb12".to_string(),
311                Margin::Bottom(12).to_css_definition(),
312                Modifiers::default(),
313            ),
314            RetrievedClassDefinition::new(
315                "mb13".to_string(),
316                Margin::Bottom(13).to_css_definition(),
317                Modifiers::default(),
318            ),
319            RetrievedClassDefinition::new(
320                "ml14".to_string(),
321                Margin::Left(14).to_css_definition(),
322                Modifiers::default(),
323            ),
324            RetrievedClassDefinition::new(
325                "mt15".to_string(),
326                Margin::Top(15).to_css_definition(),
327                Modifiers::default(),
328            ),
329            RetrievedClassDefinition::new(
330                "mt16".to_string(),
331                Margin::Top(16).to_css_definition(),
332                Modifiers::default(),
333            ),
334            RetrievedClassDefinition::new(
335                "mt17".to_string(),
336                Margin::Top(17).to_css_definition(),
337                Modifiers::default(),
338            ),
339            RetrievedClassDefinition::new(
340                "mb18".to_string(),
341                Margin::Bottom(18).to_css_definition(),
342                Modifiers::default(),
343            ),
344            RetrievedClassDefinition::new(
345                "ml19".to_string(),
346                Margin::Left(19).to_css_definition(),
347                Modifiers::default(),
348            ),
349            RetrievedClassDefinition::new(
350                "mt20".to_string(),
351                Margin::Top(20).to_css_definition(),
352                Modifiers::default(),
353            ),
354            RetrievedClassDefinition::new(
355                "mb21".to_string(),
356                Margin::Bottom(21).to_css_definition(),
357                Modifiers::default(),
358            ),
359            RetrievedClassDefinition::new(
360                "mr22".to_string(),
361                Margin::Right(22).to_css_definition(),
362                Modifiers::default(),
363            ),
364            RetrievedClassDefinition::new(
365                "ml23".to_string(),
366                Margin::Left(23).to_css_definition(),
367                Modifiers::default(),
368            ),
369        ] {
370            assert!(
371                parsed.contains(&expected),
372                "{:#?} should have been parsed.",
373                expected
374            );
375        }
376    }
377
378    enum Margin {
379        Top(u32),
380        Left(u32),
381        Bottom(u32),
382        Right(u32),
383    }
384    impl Margin {
385        fn to_css_definition(&self) -> String {
386            let (letter, suffix, px) = match self {
387                Margin::Top(px) => ("t", "top", px),
388                Margin::Left(px) => ("l", "left", px),
389                Margin::Bottom(px) => ("b", "bottom", px),
390                Margin::Right(px) => ("r", "right", px),
391            };
392
393            format!(
394                r#".m{letter}{px} {{
395    margin-{suffix}: {px}px;
396}}"#,
397            )
398        }
399    }
400}