use crate::jsx_diff::JsxChange;
use crate::language::TsCategory;
use regex::Regex;
use std::collections::BTreeSet;
use std::path::Path;
use std::sync::LazyLock;
static CSS_VAR_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"--[a-zA-Z][a-zA-Z0-9_-]*(?:--[a-zA-Z0-9_-]+)+").unwrap());
static CSS_CLASS_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\bpf-(?:v\d+-)(?:[a-z]-)?[a-z][a-z0-9-]+").unwrap());
pub fn diff_css_references(
old_body: &str,
new_body: &str,
symbol: &str,
file: &Path,
) -> Vec<JsxChange> {
let mut changes = Vec::new();
let old_vars = extract_matches(&CSS_VAR_RE, old_body);
let new_vars = extract_matches(&CSS_VAR_RE, new_body);
for var in old_vars.difference(&new_vars) {
changes.push(JsxChange {
symbol: symbol.to_string(),
file: file.to_path_buf(),
category: TsCategory::CssVariable,
description: format!("CSS variable '{}' removed from source", var),
before: Some(var.clone()),
after: None,
});
}
for var in new_vars.difference(&old_vars) {
changes.push(JsxChange {
symbol: symbol.to_string(),
file: file.to_path_buf(),
category: TsCategory::CssVariable,
description: format!("CSS variable '{}' added to source", var),
before: None,
after: Some(var.clone()),
});
}
let old_classes = extract_matches(&CSS_CLASS_RE, old_body);
let new_classes = extract_matches(&CSS_CLASS_RE, new_body);
for class in old_classes.difference(&new_classes) {
changes.push(JsxChange {
symbol: symbol.to_string(),
file: file.to_path_buf(),
category: TsCategory::CssClass,
description: format!("CSS class prefix '{}' removed from source", class),
before: Some(class.clone()),
after: None,
});
}
for class in new_classes.difference(&old_classes) {
changes.push(JsxChange {
symbol: symbol.to_string(),
file: file.to_path_buf(),
category: TsCategory::CssClass,
description: format!("CSS class prefix '{}' added to source", class),
before: None,
after: Some(class.clone()),
});
}
changes
}
pub fn body_contains_css_refs(body: &str) -> bool {
CSS_VAR_RE.is_match(body) || CSS_CLASS_RE.is_match(body)
}
fn extract_matches(re: &Regex, text: &str) -> BTreeSet<String> {
re.find_iter(text).map(|m| m.as_str().to_string()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_css_var_removed() {
let old = r#"const color = "var(--pf-v5-global--Color--100)";"#;
let new = r#"const color = "var(--pf-v6-global--Color--100)";"#;
let changes = diff_css_references(old, new, "MyComponent", &PathBuf::from("test.tsx"));
let var_changes: Vec<_> = changes
.iter()
.filter(|c| c.category == TsCategory::CssVariable)
.collect();
assert_eq!(var_changes.len(), 2); let removed: Vec<_> = var_changes
.iter()
.filter(|c| c.description.contains("removed"))
.collect();
let added: Vec<_> = var_changes
.iter()
.filter(|c| c.description.contains("added"))
.collect();
assert_eq!(removed.len(), 1);
assert_eq!(added.len(), 1);
assert!(removed[0]
.before
.as_ref()
.unwrap()
.contains("--pf-v5-global--Color--100"));
assert!(added[0]
.after
.as_ref()
.unwrap()
.contains("--pf-v6-global--Color--100"));
}
#[test]
fn test_css_class_prefix_changed() {
let old = r#"className="pf-v5-c-button pf-v5-c-button--primary""#;
let new = r#"className="pf-v6-c-button pf-v6-c-button--primary""#;
let changes = diff_css_references(old, new, "Button", &PathBuf::from("test.tsx"));
let removed: Vec<_> = changes
.iter()
.filter(|c| c.category == TsCategory::CssClass && c.before.is_some())
.collect();
assert!(!removed.is_empty());
assert!(removed
.iter()
.any(|c| c.before.as_ref().unwrap().starts_with("pf-v5-")));
}
#[test]
fn test_no_css_refs_returns_empty() {
let old = "const x = 42;";
let new = "const x = 43;";
let changes = diff_css_references(old, new, "foo", &PathBuf::from("test.ts"));
assert!(changes.is_empty());
}
#[test]
fn test_body_contains_css_refs() {
assert!(body_contains_css_refs(r#"var(--pf-v5-global--Color--100)"#));
assert!(body_contains_css_refs(r#"className="pf-v5-c-button""#));
assert!(!body_contains_css_refs("const x = 42;"));
}
#[test]
fn test_css_var_unchanged() {
let body = r#"const color = "var(--pf-v5-global--Color--100)";"#;
let changes = diff_css_references(body, body, "Comp", &PathBuf::from("test.tsx"));
assert!(changes.is_empty());
}
#[test]
fn test_multiple_vars_mixed() {
let old = r#"
const a = "var(--pf-v5-global--Color--100)";
const b = "var(--pf-v5-global--spacer--md)";
const c = "var(--pf-v5-global--FontSize--sm)";
"#;
let new = r#"
const a = "var(--pf-v6-global--Color--100)";
const b = "var(--pf-v5-global--spacer--md)";
"#;
let changes = diff_css_references(old, new, "Comp", &PathBuf::from("test.tsx"));
let removed: Vec<_> = changes
.iter()
.filter(|c| c.description.contains("removed"))
.collect();
assert!(removed.len() >= 2);
}
}