use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
#[derive(Debug, Clone)]
pub struct CssModules {
class_map: HashMap<String, String>,
rewritten_css: String,
}
impl CssModules {
pub fn process(css: &str, scope: &str) -> Self {
let mut class_map = HashMap::new();
let mut result = String::new();
let chars: Vec<char> = css.chars().collect();
let mut pos = 0;
while pos < chars.len() {
if chars[pos] == '.' {
pos += 1;
let mut class_name = String::new();
while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_' || chars[pos] == '-') {
class_name.push(chars[pos]);
pos += 1;
}
if !class_name.is_empty() {
let hashed = class_map.entry(class_name.clone())
.or_insert_with(|| hash_class_name(&class_name, scope));
result.push('.');
result.push_str(hashed);
continue;
}
result.push('.');
} else {
result.push(chars[pos]);
pos += 1;
}
}
CssModules { class_map, rewritten_css: result }
}
pub fn map_class(&self, class: &str) -> Option<&str> {
self.class_map.get(class).map(|s| s.as_str())
}
pub fn rewrite_html(&self, html: &str) -> String {
let mut result = html.to_string();
for (original, hashed) in &self.class_map {
result = result.replace(&format!("class=\"{}\"", original), &format!("class=\"{}\"", hashed));
result = result.replace(&format!("class='{}'", original), &format!("class='{}'", hashed));
}
result
}
pub fn rewritten_css(&self) -> &str { &self.rewritten_css }
pub fn class_map(&self) -> &HashMap<String, String> { &self.class_map }
}
fn hash_class_name(name: &str, scope: &str) -> String {
let mut hasher = DefaultHasher::new();
scope.hash(&mut hasher);
name.hash(&mut hasher);
format!("{}_{}", name, to_base62(hasher.finish()))
}
fn to_base62(mut n: u64) -> String {
const CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
if n == 0 { return "0".to_string(); }
let mut result = Vec::new();
while n > 0 {
result.push(CHARS[(n % 62) as usize]);
n /= 62;
}
result.reverse();
String::from_utf8(result).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
let m = CssModules::process(".btn { color: red; }", "Btn");
assert!(m.rewritten_css().contains(".btn_"));
assert!(m.rewritten_css().contains("color: red"));
}
#[test]
fn test_map_class() {
let m = CssModules::process(".header { }", "H");
assert!(m.map_class("header").unwrap().starts_with("header_"));
}
#[test]
fn test_rewrite_html() {
let m = CssModules::process(".card { }", "C");
let html = r#"<div class="card">x</div>"#;
assert!(m.rewrite_html(html).contains("card_"));
}
#[test]
fn test_scope_isolation() {
let a = CssModules::process(".btn { }", "A");
let b = CssModules::process(".btn { }", "B");
assert_ne!(a.map_class("btn"), b.map_class("btn"));
}
}