css_modules/
ast.rs

1use crate::parser::{self, Error, ParserResult, Rule};
2use pest::iterators::{Pair, Pairs};
3use std::collections::HashMap;
4use std::fmt;
5use std::fs::File;
6use std::io::prelude::*;
7use std::path::PathBuf;
8
9pub type Identifiers = HashMap<String, String>;
10pub type Children = Vec<Child>;
11
12#[derive(Debug, PartialEq)]
13pub enum Child {
14    AtRule {
15        name: Option<String>,
16        rule: Option<String>,
17        children: Children,
18    },
19    Comment {
20        value: Option<String>,
21    },
22    Property {
23        name: Option<String>,
24        value: Option<String>,
25    },
26    SelectRule {
27        rule: Option<String>,
28        children: Children,
29    },
30}
31
32impl fmt::Display for Child {
33    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
34        match self {
35            Child::AtRule {
36                name,
37                rule,
38                children,
39            } => {
40                if let (Some(name), Some(rule)) = (name, rule) {
41                    if children.is_empty() {
42                        write!(formatter, "@{} {}; ", name, rule.trim())?;
43                    } else {
44                        write!(formatter, "@{} {} {{ ", name, rule.trim())?;
45
46                        for child in children {
47                            write!(formatter, "{}", child)?;
48                        }
49
50                        write!(formatter, "}}\n")?;
51                    }
52                } else if let Some(name) = name {
53                    if children.is_empty() {
54                        write!(formatter, "@{};", name)?;
55                    } else {
56                        write!(formatter, "@{} {{ ", name)?;
57
58                        for child in children {
59                            write!(formatter, "{}", child)?;
60                        }
61
62                        write!(formatter, "}}\n")?;
63                    }
64                }
65            }
66            Child::SelectRule { rule, children } => {
67                if children.is_empty() {
68                    write!(formatter, "")?;
69                } else if let Some(rule) = rule {
70                    write!(formatter, "{} {{ ", rule)?;
71
72                    for child in children {
73                        write!(formatter, "{}", child)?;
74                    }
75
76                    write!(formatter, "}}\n")?;
77                }
78            }
79            Child::Property { name, value } => {
80                if let (Some(name), Some(value)) = (name, value) {
81                    write!(formatter, "{}: {}; ", name, value)?;
82                } else if let Some(name) = name {
83                    write!(formatter, "{}:; ", name)?;
84                }
85            }
86            Child::Comment { value } => {
87                if let Some(value) = value {
88                    write!(formatter, "{}", value)?;
89                }
90            }
91        };
92
93        Ok(())
94    }
95}
96
97#[derive(Debug, PartialEq)]
98pub struct Context<'c> {
99    pub module: &'c mut Module,
100    pub name: &'c str,
101    pub path: &'c PathBuf,
102    pub stylesheet: &'c mut Stylesheet,
103}
104
105impl<'c> Context<'c> {
106    fn add_identifier(&mut self, identifier: String) -> String {
107        self.module
108            .identifiers
109            .entry(identifier.clone())
110            .or_insert(format!(
111                "{}__{}__{}",
112                &self.name, &identifier, &self.stylesheet.identifiers
113            ));
114
115        self.stylesheet.identifiers += 1;
116
117        self.module.identifiers.get(&identifier).unwrap().to_owned()
118    }
119}
120
121#[derive(Debug, PartialEq)]
122pub struct Module {
123    pub children: Children,
124    pub identifiers: Identifiers,
125    pub input_path: PathBuf,
126    pub output_path: PathBuf,
127}
128
129#[cfg(test)]
130impl Default for Module {
131    fn default() -> Self {
132        use std::str::FromStr;
133
134        let path = PathBuf::from_str(file!()).unwrap();
135
136        Self {
137            children: Vec::new(),
138            identifiers: HashMap::new(),
139            input_path: path.clone(),
140            output_path: path.clone().with_extension("css.rs"),
141        }
142    }
143}
144
145impl fmt::Display for Module {
146    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
147        // write!(formatter, "{}", value)
148        for child in &self.children {
149            write!(formatter, "{}", child)?;
150        }
151
152        Ok(())
153    }
154}
155
156impl<'m> Module {
157    pub fn new(
158        mut stylesheet: &mut Stylesheet,
159        path: PathBuf,
160        input: &'m str,
161    ) -> ParserResult<'m, Self> {
162        let pairs = parser::stylesheet(&input)?;
163        let mut module = Module {
164            children: Children::new(),
165            identifiers: Identifiers::new(),
166            input_path: path.clone(),
167            output_path: path.clone().with_extension("css.rs"),
168        };
169        let mut context = Context {
170            module: &mut module,
171            name: &path.file_stem().unwrap().to_str().unwrap(),
172            path: &path.parent().unwrap().to_path_buf(),
173            stylesheet: &mut stylesheet,
174        };
175
176        for pair in pairs {
177            let child = match pair.as_rule() {
178                Rule::comment => comment(pair),
179                Rule::atrule => atrule(&mut context, pair)?,
180                Rule::selectrule => selectrule(&mut context, pair)?,
181                Rule::EOI => None,
182                _ => return Err(Error::from(pair)),
183            };
184
185            if let Some(child) = child {
186                context.module.children.push(child);
187            }
188        }
189
190        Ok(module)
191    }
192}
193
194pub type Modules = HashMap<PathBuf, Module>;
195
196#[derive(Debug, PartialEq)]
197pub struct Stylesheet {
198    pub identifiers: u64,
199    pub modules: Modules,
200}
201
202impl Default for Stylesheet {
203    fn default() -> Self {
204        Self {
205            identifiers: 0,
206            modules: HashMap::new(),
207        }
208    }
209}
210
211impl Stylesheet {
212    pub fn add_module<'m>(&mut self, path: PathBuf) -> ParserResult<'m, &Module> {
213        let mut file = File::open(path.clone()).expect("file not found");
214        let mut input = String::new();
215
216        file.read_to_string(&mut input).unwrap();
217
218        let module = Module::new(self, path.clone(), &input)?;
219
220        Ok(self.modules.entry(path).or_insert(module))
221    }
222
223    #[cfg(test)]
224    fn add_test_module<'m>(&mut self, input: &str) -> ParserResult<'m, &Module> {
225        use std::str::FromStr;
226
227        let path = PathBuf::from_str(file!()).unwrap();
228
229        let module = Module::new(self, path.clone(), &input)?;
230
231        Ok(self.modules.entry(path).or_insert(module))
232    }
233
234    pub fn has_module(self, path: PathBuf) -> bool {
235        self.modules.contains_key(&path)
236    }
237
238    pub fn remove_module(mut self, path: PathBuf) {
239        self.modules.remove_entry(&path);
240    }
241}
242
243pub fn atrule<'t>(
244    mut context: &mut Context,
245    pair: Pair<'t, Rule>,
246) -> ParserResult<'t, Option<Child>> {
247    let mut name: Option<String> = None;
248    let mut rule: Option<String> = None;
249    let mut children = Vec::new();
250
251    for pair in pair.into_inner() {
252        let child = match pair.as_rule() {
253            Rule::identifier => {
254                name = Some(pair.as_str().into());
255
256                None
257            }
258            Rule::atrule_rule => {
259                if Some("keyframes".into()) == name {
260                    rule = Some(format!(
261                        "{}",
262                        &context.add_identifier(pair.as_str().trim().into())
263                    ));
264                } else if Some("import".into()) == name {
265                    let quotes: &[_] = &['"', '\''];
266                    let path = context
267                        .path
268                        .clone()
269                        .join(pair.as_str().trim_matches(quotes));
270                    let import = context.stylesheet.add_module(path)?;
271
272                    for (old, new) in import.identifiers.iter() {
273                        context
274                            .module
275                            .identifiers
276                            .entry(old.clone())
277                            .or_insert(new.clone());
278                    }
279
280                    return Ok(None);
281                } else {
282                    rule = Some(pair.as_str().into());
283                }
284
285                None
286            }
287            Rule::comment | Rule::line_comment => comment(pair),
288            Rule::property => property(&mut context, pair)?,
289            Rule::atrule => atrule(&mut context, pair)?,
290            Rule::selectrule => selectrule(&mut context, pair)?,
291            _ => return Err(Error::from(pair)),
292        };
293
294        if let Some(child) = child {
295            children.push(child);
296        }
297    }
298
299    Ok(Some(Child::AtRule {
300        name,
301        rule,
302        children,
303    }))
304}
305
306pub fn comment<'t>(pair: Pair<'t, Rule>) -> Option<Child> {
307    Some(Child::Comment {
308        value: Some(pair.as_str().into()),
309    })
310}
311
312pub fn property<'t>(
313    mut context: &mut Context,
314    pair: Pair<'t, Rule>,
315) -> ParserResult<'t, Option<Child>> {
316    let mut name: Option<String> = None;
317    let mut value: Option<String> = None;
318
319    for pair in pair.into_inner() {
320        match pair.as_rule() {
321            Rule::identifier => {
322                name = Some(pair.as_str().into());
323            }
324            Rule::property_value => {
325                if Some("animation".into()) == name || Some("animation-name".into()) == name {
326                    value = replace_identifiers(&mut context, parser::animation(pair.as_str())?)?;
327                } else {
328                    value = Some(pair.as_str().trim().into());
329                }
330            }
331            _ => return Err(Error::from(pair)),
332        }
333    }
334
335    Ok(Some(Child::Property { name, value }))
336}
337
338pub fn selectrule<'t>(
339    mut context: &mut Context,
340    pair: Pair<'t, Rule>,
341) -> ParserResult<'t, Option<Child>> {
342    let mut rule: Option<String> = None;
343    let mut children = Vec::new();
344
345    for pair in pair.into_inner() {
346        let child = match pair.as_rule() {
347            Rule::selectrule_rule => {
348                rule = replace_identifiers(&mut context, parser::selector(pair.as_str())?)?;
349
350                None
351            }
352            Rule::comment | Rule::line_comment => comment(pair),
353            Rule::property => property(&mut context, pair)?,
354            Rule::atrule => atrule(&mut context, pair)?,
355            Rule::selectrule => selectrule(&mut context, pair)?,
356            _ => return Err(Error::from(pair)),
357        };
358
359        if let Some(child) = child {
360            children.push(child);
361        }
362    }
363
364    Ok(Some(Child::SelectRule { rule, children }))
365}
366
367pub fn replace_identifiers<'t>(
368    context: &mut Context,
369    pairs: Pairs<Rule>,
370) -> ParserResult<'t, Option<String>> {
371    let mut result = String::new();
372
373    for pair in pairs {
374        match pair.as_rule() {
375            Rule::identifier => {
376                result.push_str(&format!(
377                    "{}",
378                    &context.add_identifier(pair.as_str().trim().into())
379                ));
380            }
381            Rule::selector_class => {
382                result.push_str(&format!(
383                    ".{}",
384                    &context.add_identifier(pair.as_str()[1..].trim().into())
385                ));
386            }
387            _ => {
388                result.push_str(pair.as_str());
389            }
390        }
391    }
392
393    result = result.trim().into();
394
395    if result.is_empty() {
396        Ok(None)
397    } else {
398        Ok(Some(result))
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    #[test]
407    fn empty_stylesheet_parses() {
408        assert_eq!(
409            Stylesheet::default(),
410            Stylesheet {
411                identifiers: 0,
412                modules: HashMap::new(),
413            }
414        )
415    }
416
417    #[test]
418    fn empty_select_rule_parses() {
419        assert_eq!(
420            Stylesheet::default().add_test_module(".foobar {}").unwrap(),
421            &Module {
422                children: vec![Child::SelectRule {
423                    children: Vec::new(),
424                    rule: Some(".ast__foobar__0".into()),
425                }],
426                identifiers: vec![("foobar".into(), "ast__foobar__0".into())]
427                    .into_iter()
428                    .collect(),
429                ..Module::default()
430            }
431        )
432    }
433
434    #[test]
435    fn select_rule_with_property_parses() {
436        assert_eq!(
437            Stylesheet::default()
438                .add_test_module(".foobar { color: red; }")
439                .unwrap(),
440            &Module {
441                children: vec![Child::SelectRule {
442                    rule: Some(".ast__foobar__0".into()),
443                    children: vec![Child::Property {
444                        name: Some("color".into()),
445                        value: Some("red".into())
446                    }],
447                }],
448                identifiers: vec![("foobar".into(), "ast__foobar__0".into())]
449                    .into_iter()
450                    .collect(),
451                ..Module::default()
452            }
453        )
454    }
455
456    #[test]
457    fn empty_at_rule_parses() {
458        assert_eq!(
459            Stylesheet::default()
460                .add_test_module("@keyframes foobar;")
461                .unwrap(),
462            &Module {
463                children: vec![Child::AtRule {
464                    name: Some("keyframes".into()),
465                    children: Vec::new(),
466                    rule: Some("ast__foobar__0".into()),
467                }],
468                identifiers: vec![("foobar".into(), "ast__foobar__0".into())]
469                    .into_iter()
470                    .collect(),
471                ..Module::default()
472            }
473        )
474    }
475
476    #[test]
477    fn unclosed_block_is_an_error() {
478        assert!(Stylesheet::default().add_test_module("p {").is_err());
479    }
480
481    #[test]
482    fn format_empty_module() {
483        let mut stylesheet = Stylesheet::default();
484        let module = stylesheet.add_test_module("").unwrap();
485
486        assert_eq!(format!("{}", module), String::new());
487    }
488
489    #[test]
490    fn format_selectrule_with_property() {
491        let mut stylesheet = Stylesheet::default();
492        let module = stylesheet
493            .add_test_module("p.foobar  {  color :  #fff ;  }")
494            .unwrap();
495
496        assert_eq!(
497            format!("{}", module),
498            String::from("p.ast__foobar__0 { color: #fff; }\n")
499        );
500    }
501}