css-modules 0.5.2

CSS Modules with a macro for convenience.
Documentation
//! The [CSS Modules] project defines CSS Modules as:
//!
//! > A **CSS Module** is a CSS file in which all class names and animation
//! names are scoped locally by default.
//!
//! This implementation is however currently immature and has not (as far as
//! I'm aware) been used in a real world situation. Currently only animation
//! and class names are locally scoped and the following work is in progress:
//!
//! - Inlining `url()` and `@import` statements
//! - Creating or integrating with a asset compilation tool tool (Rollup, Webpack, etc)
//!
//! ## Usage
//!
//! Add this crate as a build dependency and as a regular dependency:
//!
//! ```toml
//! [dependencies]
//! css-modules = "0.5"
//!
//! [build-dependencies]
//! css-modules = "0.5"
//! ```
//!
//! Create a build script (`build.rs`) in the root of your project:
//!
//! ```ignore
//! use css_modules::CssModules;
//!
//! fn main() {
//!     let mut css_modules = CssModules::default();
//!
//!     // Include all CSS files in the src dir:
//!     css_modules.add_modules("src/**/*.css").unwrap();
//!
//!     // Compile all modules and export CSS into one file:
//!     css_modules.compile("public/app.css");
//! }
//! ```
//!
//! And then finally, you can include CSS modules:
//!
//! ```ignore
//! #![feature(proc_macro_hygiene)]
//! use css_modules::include_css_module;
//!
//! let css = include_css_module!("example.css"); // relative path to your CSS
//! let myClass = css["original-class-name"]; // aliased class name
//! ```
//!
//! For more detailed examples, look in the `examples/` directory.
//!
//! [CSS Modules]: https://github.com/css-modules/css-modules

pub mod ast;
pub mod parser;

use crate::ast::Module;
use crate::parser::ParserResult;
use glob::glob;
use quote::quote;
use std::collections::HashMap;
use std::env;
use std::fmt;
use std::fs::File;
use std::io::Write;
use std::ops::Index;
use std::path::{Path, PathBuf};
use std::str::FromStr;
pub use css_modules_macros::*;

/// A tool for compiling CSS Modules.
#[derive(Debug)]
pub struct CssModules {
    ast: ast::Stylesheet,
}

impl Default for CssModules {
    fn default() -> Self {
        Self {
            ast: ast::Stylesheet::default(),
        }
    }
}

impl fmt::Display for CssModules {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        for (_, module) in &self.ast.modules {
            for child in &module.children {
                write!(formatter, "{}", child)?
            }
        }

        Ok(())
    }
}

impl CssModules {
    /// Get a list of added modules:
    pub fn modules(&self) -> Vec<&Module> {
        self.ast
            .modules
            .iter()
            .map(|(_, module)| module)
            .collect::<Vec<&Module>>()
    }

    /// Add a module to compile using the exact path to the CSS file.
    pub fn add_module<'m>(&mut self, path: &str) -> ParserResult<'m, &Module> {
        let path = PathBuf::from_str(path).unwrap().canonicalize().unwrap();

        self.ast.add_module(path)
    }

    /// Add any modules matching the specified glob pattern.
    pub fn add_modules<'m>(&mut self, pattern: &str) -> ParserResult<'m, ()> {
        for entry in glob(pattern).expect("Failed to read glob pattern") {
            self.ast.add_module(entry.unwrap())?;
        }

        Ok(())
    }

    /// Has a module been added at this exact path?
    pub fn has_module(self, path: &str) -> bool {
        let path = PathBuf::from_str(path).unwrap().canonicalize().unwrap();

        self.ast.has_module(path)
    }

    /// Remove the module at this exact path.
    pub fn remove_module(self, path: &str) {
        let path = PathBuf::from_str(path).unwrap().canonicalize().unwrap();

        self.ast.remove_module(path)
    }

    /// Compile all modules into the specified stylesheet.
    pub fn compile(&self, stylesheet_name: &str) {
        fn write(filename: &PathBuf, content: String) {
            use std::fs::create_dir_all;

            let dirname = filename.parent().unwrap();

            create_dir_all(&dirname).expect("could not create directory.");

            let mut file = File::create(&filename).expect("could not create file.");

            file.write_all(content.as_bytes())
                .expect("could not write to file.");
        }

        fn is_fresh(input: &PathBuf, output: &PathBuf) -> bool {
            if !output.is_file() {
                false
            } else {
                let input = input.metadata().unwrap();
                let output = output.metadata().unwrap();

                output.modified().unwrap() >= input.modified().unwrap()
            }
        }

        let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
        let manifest_dir = Path::new(&manifest_dir);
        let out_dir = env::var("OUT_DIR").unwrap();
        let out_dir = Path::new(&out_dir);
        let mut stylesheet = String::new();
        let mut re_render = false;

        for module in &self.modules() {
            if !is_fresh(&module.input_path, &module.output_path) {
                let module_name = module.input_path.file_name().unwrap().to_str().unwrap();
                let mut identifiers = Vec::new();

                for (_old, _new) in &module.identifiers {
                    identifiers.push(quote! {(#_old, #_new)});
                }

                let output = quote! {
                    CssModuleBuilder::new()
                        .with_identifiers(vec![#(#identifiers),*])
                        .with_module_name(#module_name)
                        .with_stylesheet_name(#stylesheet_name)
                        .finish()
                };

                write(&out_dir.join(&module.output_path), output.to_string());

                re_render = true;
            }

            for child in &module.children {
                stylesheet.push_str(&format!("{}", child));
            }
        }

        if re_render {
            let stylesheet_path = manifest_dir.join(&stylesheet_name);

            write(&stylesheet_path, stylesheet);
        }
    }
}

/// Used internally in the `include_css_module!("...")` macro to create modules.
#[derive(Debug, PartialEq)]
pub struct CssModuleBuilder<'b> {
    identifiers: Option<HashMap<&'b str, &'b str>>,
    module_name: Option<&'b str>,
    stylesheet_name: Option<&'b str>,
}

impl<'b> CssModuleBuilder<'b> {
    pub fn new() -> Self {
        Self {
            identifiers: None,
            module_name: None,
            stylesheet_name: None,
        }
    }

    pub fn with_identifiers(self, identifiers: Vec<(&'b str, &'b str)>) -> Self {
        Self {
            identifiers: Some(identifiers.into_iter().collect()),
            ..self
        }
    }

    pub fn with_module_name(self, module_name: &'b str) -> Self {
        Self {
            module_name: Some(module_name),
            ..self
        }
    }

    pub fn with_stylesheet_name(self, stylesheet_name: &'b str) -> Self {
        Self {
            stylesheet_name: Some(stylesheet_name),
            ..self
        }
    }

    pub fn finish(self) -> CssModule<'b> {
        if let (Some(identifiers), Some(module_name), Some(stylesheet_name)) =
            (self.identifiers, self.module_name, self.stylesheet_name)
        {
            CssModule {
                identifiers,
                module_name,
                stylesheet_name,
            }
        } else {
            panic!("Unable to initialize CssModule, not all parameters are set.");
        }
    }
}

/// Returned from the `include_css_module!("...")` macro.
///
/// Class aliases are available by index:
///
/// ```ignore
/// #![feature(proc_macro_hygiene)]
/// let css = include_css_module!("example.css");
///
/// println!("<p class='{}'>{}</p>", css["hello"], "Hello World");
/// // <p class='test__hello__0'>Hello World</p>
/// ```
#[derive(Debug, PartialEq)]
pub struct CssModule<'m> {
    pub identifiers: HashMap<&'m str, &'m str>,
    pub module_name: &'m str,
    pub stylesheet_name: &'m str,
}

impl<'m> Index<&'m str> for CssModule<'m> {
    type Output = &'m str;

    fn index<'b>(&self, identifier: &'b str) -> &&'m str {
        match self.identifiers.get(identifier) {
            Some(new_identifier) => &new_identifier,
            None => panic!(
                "Class `{}` was not found in {}",
                identifier, self.module_name
            ),
        }
    }
}