1#![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#[derive(Debug, thiserror::Error)]
19pub enum ParseCssError {
20 }
22
23pub 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
183struct 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
221fn 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 #[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}