css_modules/
lib.rs

1//! The [CSS Modules] project defines CSS Modules as:
2//!
3//! > A **CSS Module** is a CSS file in which all class names and animation
4//! names are scoped locally by default.
5//!
6//! This implementation is however currently immature and has not (as far as
7//! I'm aware) been used in a real world situation. Currently only animation
8//! and class names are locally scoped and the following work is in progress:
9//!
10//! - Inlining `url()` and `@import` statements
11//! - Creating or integrating with a asset compilation tool tool (Rollup, Webpack, etc)
12//!
13//! ## Usage
14//!
15//! Add this crate as a build dependency and as a regular dependency:
16//!
17//! ```toml
18//! [dependencies]
19//! css-modules = "0.5"
20//!
21//! [build-dependencies]
22//! css-modules = "0.5"
23//! ```
24//!
25//! Create a build script (`build.rs`) in the root of your project:
26//!
27//! ```ignore
28//! use css_modules::CssModules;
29//!
30//! fn main() {
31//!     let mut css_modules = CssModules::default();
32//!
33//!     // Include all CSS files in the src dir:
34//!     css_modules.add_modules("src/**/*.css").unwrap();
35//!
36//!     // Compile all modules and export CSS into one file:
37//!     css_modules.compile("public/app.css");
38//! }
39//! ```
40//!
41//! And then finally, you can include CSS modules:
42//!
43//! ```ignore
44//! #![feature(proc_macro_hygiene)]
45//! use css_modules::include_css_module;
46//!
47//! let css = include_css_module!("example.css"); // relative path to your CSS
48//! let myClass = css["original-class-name"]; // aliased class name
49//! ```
50//!
51//! For more detailed examples, look in the `examples/` directory.
52//!
53//! [CSS Modules]: https://github.com/css-modules/css-modules
54
55pub mod ast;
56pub mod parser;
57
58use crate::ast::Module;
59use crate::parser::ParserResult;
60use glob::glob;
61use quote::quote;
62use std::collections::HashMap;
63use std::env;
64use std::fmt;
65use std::fs::File;
66use std::io::Write;
67use std::ops::Index;
68use std::path::{Path, PathBuf};
69use std::str::FromStr;
70pub use css_modules_macros::*;
71
72/// A tool for compiling CSS Modules.
73#[derive(Debug)]
74pub struct CssModules {
75    ast: ast::Stylesheet,
76}
77
78impl Default for CssModules {
79    fn default() -> Self {
80        Self {
81            ast: ast::Stylesheet::default(),
82        }
83    }
84}
85
86impl fmt::Display for CssModules {
87    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
88        for (_, module) in &self.ast.modules {
89            for child in &module.children {
90                write!(formatter, "{}", child)?
91            }
92        }
93
94        Ok(())
95    }
96}
97
98impl CssModules {
99    /// Get a list of added modules:
100    pub fn modules(&self) -> Vec<&Module> {
101        self.ast
102            .modules
103            .iter()
104            .map(|(_, module)| module)
105            .collect::<Vec<&Module>>()
106    }
107
108    /// Add a module to compile using the exact path to the CSS file.
109    pub fn add_module<'m>(&mut self, path: &str) -> ParserResult<'m, &Module> {
110        let path = PathBuf::from_str(path).unwrap().canonicalize().unwrap();
111
112        self.ast.add_module(path)
113    }
114
115    /// Add any modules matching the specified glob pattern.
116    pub fn add_modules<'m>(&mut self, pattern: &str) -> ParserResult<'m, ()> {
117        for entry in glob(pattern).expect("Failed to read glob pattern") {
118            self.ast.add_module(entry.unwrap())?;
119        }
120
121        Ok(())
122    }
123
124    /// Has a module been added at this exact path?
125    pub fn has_module(self, path: &str) -> bool {
126        let path = PathBuf::from_str(path).unwrap().canonicalize().unwrap();
127
128        self.ast.has_module(path)
129    }
130
131    /// Remove the module at this exact path.
132    pub fn remove_module(self, path: &str) {
133        let path = PathBuf::from_str(path).unwrap().canonicalize().unwrap();
134
135        self.ast.remove_module(path)
136    }
137
138    /// Compile all modules into the specified stylesheet.
139    pub fn compile(&self, stylesheet_name: &str) {
140        fn write(filename: &PathBuf, content: String) {
141            use std::fs::create_dir_all;
142
143            let dirname = filename.parent().unwrap();
144
145            create_dir_all(&dirname).expect("could not create directory.");
146
147            let mut file = File::create(&filename).expect("could not create file.");
148
149            file.write_all(content.as_bytes())
150                .expect("could not write to file.");
151        }
152
153        fn is_fresh(input: &PathBuf, output: &PathBuf) -> bool {
154            if !output.is_file() {
155                false
156            } else {
157                let input = input.metadata().unwrap();
158                let output = output.metadata().unwrap();
159
160                output.modified().unwrap() >= input.modified().unwrap()
161            }
162        }
163
164        let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
165        let manifest_dir = Path::new(&manifest_dir);
166        let out_dir = env::var("OUT_DIR").unwrap();
167        let out_dir = Path::new(&out_dir);
168        let mut stylesheet = String::new();
169        let mut re_render = false;
170
171        for module in &self.modules() {
172            if !is_fresh(&module.input_path, &module.output_path) {
173                let module_name = module.input_path.file_name().unwrap().to_str().unwrap();
174                let mut identifiers = Vec::new();
175
176                for (_old, _new) in &module.identifiers {
177                    identifiers.push(quote! {(#_old, #_new)});
178                }
179
180                let output = quote! {
181                    CssModuleBuilder::new()
182                        .with_identifiers(vec![#(#identifiers),*])
183                        .with_module_name(#module_name)
184                        .with_stylesheet_name(#stylesheet_name)
185                        .finish()
186                };
187
188                write(&out_dir.join(&module.output_path), output.to_string());
189
190                re_render = true;
191            }
192
193            for child in &module.children {
194                stylesheet.push_str(&format!("{}", child));
195            }
196        }
197
198        if re_render {
199            let stylesheet_path = manifest_dir.join(&stylesheet_name);
200
201            write(&stylesheet_path, stylesheet);
202        }
203    }
204}
205
206/// Used internally in the `include_css_module!("...")` macro to create modules.
207#[derive(Debug, PartialEq)]
208pub struct CssModuleBuilder<'b> {
209    identifiers: Option<HashMap<&'b str, &'b str>>,
210    module_name: Option<&'b str>,
211    stylesheet_name: Option<&'b str>,
212}
213
214impl<'b> CssModuleBuilder<'b> {
215    pub fn new() -> Self {
216        Self {
217            identifiers: None,
218            module_name: None,
219            stylesheet_name: None,
220        }
221    }
222
223    pub fn with_identifiers(self, identifiers: Vec<(&'b str, &'b str)>) -> Self {
224        Self {
225            identifiers: Some(identifiers.into_iter().collect()),
226            ..self
227        }
228    }
229
230    pub fn with_module_name(self, module_name: &'b str) -> Self {
231        Self {
232            module_name: Some(module_name),
233            ..self
234        }
235    }
236
237    pub fn with_stylesheet_name(self, stylesheet_name: &'b str) -> Self {
238        Self {
239            stylesheet_name: Some(stylesheet_name),
240            ..self
241        }
242    }
243
244    pub fn finish(self) -> CssModule<'b> {
245        if let (Some(identifiers), Some(module_name), Some(stylesheet_name)) =
246            (self.identifiers, self.module_name, self.stylesheet_name)
247        {
248            CssModule {
249                identifiers,
250                module_name,
251                stylesheet_name,
252            }
253        } else {
254            panic!("Unable to initialize CssModule, not all parameters are set.");
255        }
256    }
257}
258
259/// Returned from the `include_css_module!("...")` macro.
260///
261/// Class aliases are available by index:
262///
263/// ```ignore
264/// #![feature(proc_macro_hygiene)]
265/// let css = include_css_module!("example.css");
266///
267/// println!("<p class='{}'>{}</p>", css["hello"], "Hello World");
268/// // <p class='test__hello__0'>Hello World</p>
269/// ```
270#[derive(Debug, PartialEq)]
271pub struct CssModule<'m> {
272    pub identifiers: HashMap<&'m str, &'m str>,
273    pub module_name: &'m str,
274    pub stylesheet_name: &'m str,
275}
276
277impl<'m> Index<&'m str> for CssModule<'m> {
278    type Output = &'m str;
279
280    fn index<'b>(&self, identifier: &'b str) -> &&'m str {
281        match self.identifiers.get(identifier) {
282            Some(new_identifier) => &new_identifier,
283            None => panic!(
284                "Class `{}` was not found in {}",
285                identifier, self.module_name
286            ),
287        }
288    }
289}