iris_cssom/
css_modules.rs1use regex::Regex;
29use std::collections::HashMap;
30use std::sync::LazyLock;
31
32static CLASS_SELECTOR_RE: LazyLock<Regex> =
34 LazyLock::new(|| Regex::new(r"\.([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap());
35
36static LOCAL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":local\(([^)]+)\)").unwrap());
38
39static GLOBAL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":global\(([^)]+)\)").unwrap());
41
42pub fn generate_short_hash(content: &str) -> String {
44 use xxhash_rust::xxh3::xxh3_64;
45 let hash = xxh3_64(content.as_bytes());
46 format!("{:08x}", hash & 0xFFFFFFFF)
48}
49
50pub fn scope_class_name(class_name: &str, hash: &str) -> String {
61 format!("{}__{}", class_name, hash)
62}
63
64pub fn transform_css(css: &str, hash: &str) -> String {
75 let mut result = css.to_string();
76
77 loop {
79 if let Some(mat) = GLOBAL_RE.find(&result) {
80 let content = &mat.as_str()[8..mat.as_str().len() - 1];
81 result = format!(
82 "{}{}{}",
83 &result[..mat.start()],
84 content,
85 &result[mat.end()..]
86 );
87 } else {
88 break;
89 }
90 }
91
92 loop {
94 if let Some(mat) = LOCAL_RE.find(&result) {
95 let content = &mat.as_str()[7..mat.as_str().len() - 1];
96 let scoped = scope_class_name(content.trim(), hash);
97 result = format!(
98 "{}.{}{}",
99 &result[..mat.start()],
100 scoped,
101 &result[mat.end()..]
102 );
103 } else {
104 break;
105 }
106 }
107
108 result = CLASS_SELECTOR_RE
110 .replace_all(&result, |caps: ®ex::Captures| {
111 let class_name = &caps[1];
112 if class_name.contains("__") {
114 return format!(".{}", class_name);
115 }
116 let scoped = scope_class_name(class_name, hash);
117 format!(".{}", scoped)
118 })
119 .to_string();
120
121 result
122}
123
124pub fn generate_mapping(css: &str, hash: &str) -> HashMap<String, String> {
135 let mut mapping = HashMap::new();
136
137 let mut global_ranges = Vec::new();
139 for cap in GLOBAL_RE.captures_iter(css) {
140 if let Some(m) = cap.get(0) {
141 global_ranges.push((m.start(), m.end()));
142 }
143 }
144
145 for cap in CLASS_SELECTOR_RE.captures_iter(css) {
147 let class_name = cap[1].to_string();
148 let match_start = cap.get(0).unwrap().start();
149
150 let in_global = global_ranges
152 .iter()
153 .any(|&(start, end)| match_start >= start && match_start < end);
154
155 if !in_global {
156 let scoped = scope_class_name(&class_name, hash);
157 mapping.insert(class_name, scoped);
158 }
159 }
160
161 mapping
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_generate_short_hash() {
170 let hash1 = generate_short_hash(".button { color: red; }");
171 let hash2 = generate_short_hash(".button { color: red; }");
172 let hash3 = generate_short_hash(".button { color: blue; }");
173
174 assert_eq!(hash1, hash2);
176 assert_ne!(hash1, hash3);
178 assert_eq!(hash1.len(), 8);
180 }
181
182 #[test]
183 fn test_scope_class_name() {
184 let scoped = scope_class_name("button", "a1b2c3d4");
185 assert_eq!(scoped, "button__a1b2c3d4");
186 }
187
188 #[test]
189 fn test_transform_css_basic() {
190 let css = r#"
191 .button {
192 color: red;
193 }
194 .container {
195 padding: 10px;
196 }
197 "#;
198
199 let hash = "test123";
200 let result = transform_css(css, hash);
201
202 assert!(result.contains(".button__test123"));
203 assert!(result.contains(".container__test123"));
204 assert!(!result.contains(".button {"));
205 assert!(!result.contains(".container {"));
206 }
207
208 #[test]
209 fn test_transform_css_global() {
210 let css = r#"
211 :global(.global-class) {
212 color: red;
213 }
214 "#;
215
216 let hash = "test123";
217 let result = transform_css(css, hash);
218
219 assert!(result.contains(".global-class"));
221 assert!(!result.contains(":global"));
222 }
223
224 #[test]
225 fn test_transform_css_local() {
226 let css = r#"
227 :local(.local-class) {
228 color: red;
229 }
230 "#;
231
232 let hash = "test123";
233 let result = transform_css(css, hash);
234
235 assert!(result.contains(".local-class__test123"));
237 assert!(!result.contains(":local"));
238 }
239
240 #[test]
241 fn test_generate_mapping() {
242 let css = r#"
243 .button {
244 color: red;
245 }
246 .container {
247 padding: 10px;
248 }
249 "#;
250
251 let hash = "test123";
252 let mapping = generate_mapping(css, hash);
253
254 assert_eq!(mapping.get("button"), Some(&"button__test123".to_string()));
255 assert_eq!(
256 mapping.get("container"),
257 Some(&"container__test123".to_string())
258 );
259 assert_eq!(mapping.len(), 2);
260 }
261
262 #[test]
263 fn test_css_modules_integration() {
264 let css = r#"
266 .button {
267 color: red;
268 }
269 :global(.external) {
270 font-size: 14px;
271 }
272 "#;
273
274 let hash = generate_short_hash(css);
275 let scoped_css = transform_css(css, &hash);
276 let mapping = generate_mapping(css, &hash);
277
278 assert!(scoped_css.contains(&format!(".button__{}", hash)));
280 assert!(scoped_css.contains(".external")); assert!(mapping.contains_key("button"));
284 assert_eq!(mapping.len(), 1); }
286}