rustyle_css/
modules.rs

1//! CSS Modules support
2//!
3//! Provides support for external CSS files and CSS module compilation.
4
5use crate::parser::CssParser;
6use crate::scope::generate_class_name;
7use crate::style::register_style;
8use std::collections::HashMap;
9use std::path::Path;
10
11/// CSS Module definition with scoped class names
12#[derive(Clone, Debug)]
13pub struct CssModule {
14    pub name: String,
15    pub css: String,
16    pub class_names: HashMap<String, String>,
17    pub module_id: String,
18}
19
20impl CssModule {
21    /// Create a new CSS module from CSS string
22    pub fn new(name: &str, css: &str) -> Self {
23        let module_id = generate_class_name(&format!("{}{}", name, css));
24        let mut class_names = HashMap::new();
25
26        // Extract class names from CSS and generate scoped versions
27        Self::extract_and_scope_classes(css, &module_id, &mut class_names);
28
29        // Process and scope the CSS
30        let scoped_css = Self::scope_css_module(css, &module_id, &class_names);
31
32        Self {
33            name: name.to_string(),
34            css: scoped_css,
35            class_names,
36            module_id,
37        }
38    }
39
40    /// Extract class names from CSS and generate scoped versions
41    fn extract_and_scope_classes(
42        css: &str,
43        module_id: &str,
44        class_names: &mut HashMap<String, String>,
45    ) {
46        use regex::Regex;
47
48        // Match class selectors: .className
49        let class_re = Regex::new(r"\.([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap();
50
51        for cap in class_re.captures_iter(css) {
52            let original_class = &cap[1];
53            if !class_names.contains_key(original_class) {
54                // Generate scoped class name: module_id-original_class
55                let scoped = format!("{}-{}", module_id, original_class);
56                class_names.insert(original_class.to_string(), scoped);
57            }
58        }
59    }
60
61    /// Scope CSS module by replacing class names with scoped versions
62    fn scope_css_module(
63        css: &str,
64        _module_id: &str,
65        class_names: &HashMap<String, String>,
66    ) -> String {
67        use regex::Regex;
68
69        let mut scoped = css.to_string();
70
71        // Replace all class selectors with scoped versions
72        for (original, scoped_name) in class_names {
73            // Match .original (but not already scoped)
74            let pattern = format!(r"\.({})(?![a-zA-Z0-9_-])", regex::escape(original));
75            if let Ok(re) = Regex::new(&pattern) {
76                scoped = re
77                    .replace_all(&scoped, |_caps: &regex::Captures| {
78                        format!(".{}", scoped_name)
79                    })
80                    .to_string();
81            }
82        }
83
84        // Process CSS through parser for validation and optimization
85        CssParser::parse(&scoped).unwrap_or(scoped)
86    }
87
88    /// Register the module styles
89    pub fn register(&self) {
90        register_style(&self.module_id, &self.css);
91    }
92
93    /// Get a scoped class name from the module
94    pub fn class(&self, name: &str) -> Option<&String> {
95        self.class_names.get(name)
96    }
97
98    /// Get the module ID
99    pub fn module_id(&self) -> &str {
100        &self.module_id
101    }
102
103    /// Get all class names
104    pub fn classes(&self) -> &HashMap<String, String> {
105        &self.class_names
106    }
107}
108
109/// CSS Module loader for file system support
110pub struct CssModuleLoader;
111
112impl CssModuleLoader {
113    /// Load a CSS module from a file path
114    ///
115    /// Reads the CSS file from disk, parses it, and generates scoped class names.
116    /// The path should be relative to the crate root or an absolute path.
117    pub fn load<P: AsRef<Path>>(path: P) -> Result<CssModule, String> {
118        let path = path.as_ref();
119
120        // Try to read the file
121        let css_content = std::fs::read_to_string(path)
122            .map_err(|e| format!("Failed to read CSS file {}: {}", path.display(), e))?;
123
124        // Use the file name (without extension) as the module name
125        let module_name = path
126            .file_stem()
127            .and_then(|s| s.to_str())
128            .unwrap_or("module");
129
130        // Create module from content
131        Ok(CssModule::new(module_name, &css_content))
132    }
133
134    /// Load a CSS module from a string
135    pub fn from_str(name: &str, css: &str) -> CssModule {
136        CssModule::new(name, css)
137    }
138
139    /// Load multiple CSS modules from a directory
140    pub fn load_directory<P: AsRef<Path>>(dir: P) -> Result<Vec<CssModule>, String> {
141        let dir = dir.as_ref();
142        let mut modules = Vec::new();
143
144        let entries = std::fs::read_dir(dir)
145            .map_err(|e| format!("Failed to read directory {}: {}", dir.display(), e))?;
146
147        for entry in entries {
148            let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
149            let path = entry.path();
150
151            if path.extension().and_then(|s| s.to_str()) == Some("css") {
152                match Self::load(&path) {
153                    Ok(module) => modules.push(module),
154                    Err(e) => eprintln!(
155                        "Warning: Failed to load CSS module {}: {}",
156                        path.display(),
157                        e
158                    ),
159                }
160            }
161        }
162
163        Ok(modules)
164    }
165}
166
167/// Type-safe CSS module class accessor
168///
169/// This struct is generated by the `css_module!` macro to provide
170/// type-safe access to CSS module class names.
171#[derive(Clone, Debug)]
172pub struct CssModuleClasses {
173    module: CssModule,
174}
175
176impl CssModuleClasses {
177    /// Create a new CSS module classes accessor
178    pub fn new(module: CssModule) -> Self {
179        Self { module }
180    }
181
182    /// Get a class name by its original name
183    pub fn get(&self, name: &str) -> String {
184        self.module
185            .class(name)
186            .cloned()
187            .unwrap_or_else(|| format!("{}-{}", self.module.module_id(), name))
188    }
189
190    /// Get the module
191    pub fn module(&self) -> &CssModule {
192        &self.module
193    }
194}