Skip to main content

iris_cssom/
css_modules.rs

1//! CSS Modules 处理器
2//!
3//! 实现 CSS Modules 作用域化类名生成,支持 `<style module>` 语法。
4//!
5//! ## 功能
6//!
7//! - 类名作用域化:`.button` -> `.button__hash123`
8//! - 生成类名映射:`{ "button": "button__hash123" }`
9//! - 支持 `:local()` 和 `:global()` 语法
10//!
11//! ## 使用示例
12//!
13//! ```vue
14//! <style module>
15//! .button {
16//!   color: red;
17//! }
18//! </style>
19//! ```
20//!
21//! 编译后:
22//! ```css
23//! .button__a1b2c3 {
24//!   color: red;
25//! }
26//! ```
27
28use regex::Regex;
29use std::collections::HashMap;
30use std::sync::LazyLock;
31
32/// CSS 类名选择器正则表达式
33static CLASS_SELECTOR_RE: LazyLock<Regex> =
34    LazyLock::new(|| Regex::new(r"\.([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap());
35
36/// :local() 伪类正则
37static LOCAL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":local\(([^)]+)\)").unwrap());
38
39/// :global() 伪类正则
40static GLOBAL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":global\(([^)]+)\)").unwrap());
41
42/// 生成短哈希(基于内容)
43pub fn generate_short_hash(content: &str) -> String {
44    use xxhash_rust::xxh3::xxh3_64;
45    let hash = xxh3_64(content.as_bytes());
46    // 取前 8 位十六进制字符
47    format!("{:08x}", hash & 0xFFFFFFFF)
48}
49
50/// 生成作用域化类名
51///
52/// # 参数
53///
54/// * `class_name` - 原始类名
55/// * `hash` - 哈希值
56///
57/// # 返回
58///
59/// 作用域化后的类名
60pub fn scope_class_name(class_name: &str, hash: &str) -> String {
61    format!("{}__{}", class_name, hash)
62}
63
64/// 转换 CSS 内容为作用域化版本
65///
66/// # 参数
67///
68/// * `css` - 原始 CSS 内容
69/// * `hash` - 哈希值
70///
71/// # 返回
72///
73/// 作用域化后的 CSS
74pub fn transform_css(css: &str, hash: &str) -> String {
75    let mut result = css.to_string();
76
77    // 处理 :global() - 移除 :global() 包装,保持类名不变
78    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    // 处理 :local() - 作用域化类名
93    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    // 处理普通类名选择器(自动作用域化)
109    result = CLASS_SELECTOR_RE
110        .replace_all(&result, |caps: &regex::Captures| {
111            let class_name = &caps[1];
112            // 跳过已经作用域化的类名(包含 __hash 后缀)
113            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
124/// 生成类名映射表
125///
126/// # 参数
127///
128/// * `css` - 原始 CSS 内容
129/// * `hash` - 哈希值
130///
131/// # 返回
132///
133/// 类名映射:原始类名 -> 作用域化类名(排除 :global() 中的类名)
134pub fn generate_mapping(css: &str, hash: &str) -> HashMap<String, String> {
135    let mut mapping = HashMap::new();
136
137    // 先找出所有 :global() 块的范围
138    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    // 提取所有类名,排除在 :global() 范围内的
146    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        // 检查是否在 :global() 范围内
151        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        // 相同内容生成相同哈希
175        assert_eq!(hash1, hash2);
176        // 不同内容生成不同哈希
177        assert_ne!(hash1, hash3);
178        // 哈希长度为 8
179        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        // :global() 应该被移除,保留类名
220        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        // :local() 应该被作用域化
236        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        // 模拟完整的 CSS Modules 处理流程
265        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        // 验证 CSS 被正确作用域化
279        assert!(scoped_css.contains(&format!(".button__{}", hash)));
280        assert!(scoped_css.contains(".external")); // global 不被作用域化
281
282        // 验证映射表
283        assert!(mapping.contains_key("button"));
284        assert_eq!(mapping.len(), 1); // 只有 .button,.external 是 global
285    }
286}