use regex::Regex;
use std::collections::HashMap;
use std::sync::LazyLock;
static CLASS_SELECTOR_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\.([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap());
static LOCAL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":local\(([^)]+)\)").unwrap());
static GLOBAL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":global\(([^)]+)\)").unwrap());
pub fn generate_short_hash(content: &str) -> String {
use xxhash_rust::xxh3::xxh3_64;
let hash = xxh3_64(content.as_bytes());
format!("{:08x}", hash & 0xFFFFFFFF)
}
pub fn scope_class_name(class_name: &str, hash: &str) -> String {
format!("{}__{}", class_name, hash)
}
pub fn transform_css(css: &str, hash: &str) -> String {
let mut result = css.to_string();
loop {
if let Some(mat) = GLOBAL_RE.find(&result) {
let content = &mat.as_str()[8..mat.as_str().len() - 1];
result = format!(
"{}{}{}",
&result[..mat.start()],
content,
&result[mat.end()..]
);
} else {
break;
}
}
loop {
if let Some(mat) = LOCAL_RE.find(&result) {
let content = &mat.as_str()[7..mat.as_str().len() - 1];
let scoped = scope_class_name(content.trim(), hash);
result = format!(
"{}.{}{}",
&result[..mat.start()],
scoped,
&result[mat.end()..]
);
} else {
break;
}
}
result = CLASS_SELECTOR_RE
.replace_all(&result, |caps: ®ex::Captures| {
let class_name = &caps[1];
if class_name.contains("__") {
return format!(".{}", class_name);
}
let scoped = scope_class_name(class_name, hash);
format!(".{}", scoped)
})
.to_string();
result
}
pub fn generate_mapping(css: &str, hash: &str) -> HashMap<String, String> {
let mut mapping = HashMap::new();
let mut global_ranges = Vec::new();
for cap in GLOBAL_RE.captures_iter(css) {
if let Some(m) = cap.get(0) {
global_ranges.push((m.start(), m.end()));
}
}
for cap in CLASS_SELECTOR_RE.captures_iter(css) {
let class_name = cap[1].to_string();
let match_start = cap.get(0).unwrap().start();
let in_global = global_ranges
.iter()
.any(|&(start, end)| match_start >= start && match_start < end);
if !in_global {
let scoped = scope_class_name(&class_name, hash);
mapping.insert(class_name, scoped);
}
}
mapping
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_short_hash() {
let hash1 = generate_short_hash(".button { color: red; }");
let hash2 = generate_short_hash(".button { color: red; }");
let hash3 = generate_short_hash(".button { color: blue; }");
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
assert_eq!(hash1.len(), 8);
}
#[test]
fn test_scope_class_name() {
let scoped = scope_class_name("button", "a1b2c3d4");
assert_eq!(scoped, "button__a1b2c3d4");
}
#[test]
fn test_transform_css_basic() {
let css = r#"
.button {
color: red;
}
.container {
padding: 10px;
}
"#;
let hash = "test123";
let result = transform_css(css, hash);
assert!(result.contains(".button__test123"));
assert!(result.contains(".container__test123"));
assert!(!result.contains(".button {"));
assert!(!result.contains(".container {"));
}
#[test]
fn test_transform_css_global() {
let css = r#"
:global(.global-class) {
color: red;
}
"#;
let hash = "test123";
let result = transform_css(css, hash);
assert!(result.contains(".global-class"));
assert!(!result.contains(":global"));
}
#[test]
fn test_transform_css_local() {
let css = r#"
:local(.local-class) {
color: red;
}
"#;
let hash = "test123";
let result = transform_css(css, hash);
assert!(result.contains(".local-class__test123"));
assert!(!result.contains(":local"));
}
#[test]
fn test_generate_mapping() {
let css = r#"
.button {
color: red;
}
.container {
padding: 10px;
}
"#;
let hash = "test123";
let mapping = generate_mapping(css, hash);
assert_eq!(mapping.get("button"), Some(&"button__test123".to_string()));
assert_eq!(
mapping.get("container"),
Some(&"container__test123".to_string())
);
assert_eq!(mapping.len(), 2);
}
#[test]
fn test_css_modules_integration() {
let css = r#"
.button {
color: red;
}
:global(.external) {
font-size: 14px;
}
"#;
let hash = generate_short_hash(css);
let scoped_css = transform_css(css, &hash);
let mapping = generate_mapping(css, &hash);
assert!(scoped_css.contains(&format!(".button__{}", hash)));
assert!(scoped_css.contains(".external"));
assert!(mapping.contains_key("button"));
assert_eq!(mapping.len(), 1); }
}