iris_cssom/
css_modules.rs1use std::collections::HashMap;
7use std::hash::{DefaultHasher, Hash, Hasher};
8
9#[derive(Debug, Clone)]
11pub struct CssModules {
12 class_map: HashMap<String, String>,
13 rewritten_css: String,
14}
15
16impl CssModules {
17 pub fn process(css: &str, scope: &str) -> Self {
22 let mut class_map = HashMap::new();
23 let mut result = String::new();
24 let chars: Vec<char> = css.chars().collect();
25 let mut pos = 0;
26
27 while pos < chars.len() {
28 if chars[pos] == '.' {
29 pos += 1;
30 let mut class_name = String::new();
31 while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_' || chars[pos] == '-') {
32 class_name.push(chars[pos]);
33 pos += 1;
34 }
35 if !class_name.is_empty() {
36 let hashed = class_map.entry(class_name.clone())
37 .or_insert_with(|| hash_class_name(&class_name, scope));
38 result.push('.');
39 result.push_str(hashed);
40 continue;
41 }
42 result.push('.');
43 } else {
44 result.push(chars[pos]);
45 pos += 1;
46 }
47 }
48
49 CssModules { class_map, rewritten_css: result }
50 }
51
52 pub fn map_class(&self, class: &str) -> Option<&str> {
53 self.class_map.get(class).map(|s| s.as_str())
54 }
55
56 pub fn rewrite_html(&self, html: &str) -> String {
57 let mut result = html.to_string();
58 for (original, hashed) in &self.class_map {
59 result = result.replace(&format!("class=\"{}\"", original), &format!("class=\"{}\"", hashed));
60 result = result.replace(&format!("class='{}'", original), &format!("class='{}'", hashed));
61 }
62 result
63 }
64
65 pub fn rewritten_css(&self) -> &str { &self.rewritten_css }
66 pub fn class_map(&self) -> &HashMap<String, String> { &self.class_map }
67}
68
69fn hash_class_name(name: &str, scope: &str) -> String {
70 let mut hasher = DefaultHasher::new();
71 scope.hash(&mut hasher);
72 name.hash(&mut hasher);
73 format!("{}_{}", name, to_base62(hasher.finish()))
74}
75
76fn to_base62(mut n: u64) -> String {
77 const CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
78 if n == 0 { return "0".to_string(); }
79 let mut result = Vec::new();
80 while n > 0 {
81 result.push(CHARS[(n % 62) as usize]);
82 n /= 62;
83 }
84 result.reverse();
85 String::from_utf8(result).unwrap_or_default()
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_basic() {
94 let m = CssModules::process(".btn { color: red; }", "Btn");
95 assert!(m.rewritten_css().contains(".btn_"));
96 assert!(m.rewritten_css().contains("color: red"));
97 }
98
99 #[test]
100 fn test_map_class() {
101 let m = CssModules::process(".header { }", "H");
102 assert!(m.map_class("header").unwrap().starts_with("header_"));
103 }
104
105 #[test]
106 fn test_rewrite_html() {
107 let m = CssModules::process(".card { }", "C");
108 let html = r#"<div class="card">x</div>"#;
109 assert!(m.rewrite_html(html).contains("card_"));
110 }
111
112 #[test]
113 fn test_scope_isolation() {
114 let a = CssModules::process(".btn { }", "A");
115 let b = CssModules::process(".btn { }", "B");
116 assert_ne!(a.map_class("btn"), b.map_class("btn"));
117 }
118}