1use crate::parser::CssParser;
6use crate::scope::generate_class_name;
7use crate::style::register_style;
8use std::collections::HashMap;
9use std::path::Path;
10
11#[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 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 Self::extract_and_scope_classes(css, &module_id, &mut class_names);
28
29 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 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 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 let scoped = format!("{}-{}", module_id, original_class);
56 class_names.insert(original_class.to_string(), scoped);
57 }
58 }
59 }
60
61 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 for (original, scoped_name) in class_names {
73 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: ®ex::Captures| {
78 format!(".{}", scoped_name)
79 })
80 .to_string();
81 }
82 }
83
84 CssParser::parse(&scoped).unwrap_or(scoped)
86 }
87
88 pub fn register(&self) {
90 register_style(&self.module_id, &self.css);
91 }
92
93 pub fn class(&self, name: &str) -> Option<&String> {
95 self.class_names.get(name)
96 }
97
98 pub fn module_id(&self) -> &str {
100 &self.module_id
101 }
102
103 pub fn classes(&self) -> &HashMap<String, String> {
105 &self.class_names
106 }
107}
108
109pub struct CssModuleLoader;
111
112impl CssModuleLoader {
113 pub fn load<P: AsRef<Path>>(path: P) -> Result<CssModule, String> {
118 let path = path.as_ref();
119
120 let css_content = std::fs::read_to_string(path)
122 .map_err(|e| format!("Failed to read CSS file {}: {}", path.display(), e))?;
123
124 let module_name = path
126 .file_stem()
127 .and_then(|s| s.to_str())
128 .unwrap_or("module");
129
130 Ok(CssModule::new(module_name, &css_content))
132 }
133
134 pub fn from_str(name: &str, css: &str) -> CssModule {
136 CssModule::new(name, css)
137 }
138
139 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#[derive(Clone, Debug)]
172pub struct CssModuleClasses {
173 module: CssModule,
174}
175
176impl CssModuleClasses {
177 pub fn new(module: CssModule) -> Self {
179 Self { module }
180 }
181
182 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 pub fn module(&self) -> &CssModule {
192 &self.module
193 }
194}