use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClassAnalyzer {
pub used_classes: HashSet<String>,
pub unused_classes: HashSet<String>,
pub dependencies: HashMap<String, HashSet<String>>,
pub critical_classes: HashSet<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CssPurger {
pub keep_classes: HashSet<String>,
pub remove_classes: HashSet<String>,
pub keep_rules: HashSet<String>,
pub remove_rules: HashSet<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OptimizationResult {
pub original_size: usize,
pub optimized_size: usize,
pub reduction_percentage: f32,
pub classes_removed: usize,
pub rules_removed: usize,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BundleAnalyzer {
pub class_stats: HashMap<String, ClassUsageStats>,
pub rule_stats: HashMap<String, RuleUsageStats>,
pub metrics: PerformanceMetrics,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClassUsageStats {
pub usage_count: u32,
pub used_in_files: HashSet<String>,
pub is_critical: bool,
pub dependencies: HashSet<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RuleUsageStats {
pub usage_count: u32,
pub selectors: HashSet<String>,
pub properties: HashSet<String>,
pub size_bytes: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PerformanceMetrics {
pub total_size: usize,
pub class_count: usize,
pub rule_count: usize,
pub avg_class_size: f32,
pub avg_rule_size: f32,
pub compression_ratio: f32,
}
impl Default for ClassAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl ClassAnalyzer {
pub fn new() -> Self {
Self {
used_classes: HashSet::new(),
unused_classes: HashSet::new(),
dependencies: HashMap::new(),
critical_classes: HashSet::new(),
}
}
pub fn add_used_class(&mut self, class: String) {
self.used_classes.insert(class);
}
pub fn add_used_classes(&mut self, classes: Vec<String>) {
for class in classes {
self.used_classes.insert(class);
}
}
pub fn add_dependency(&mut self, class: String, dependency: String) {
self.dependencies
.entry(class)
.or_default()
.insert(dependency);
}
pub fn mark_critical(&mut self, class: String) {
self.critical_classes.insert(class);
}
pub fn analyze_usage(&mut self, all_classes: HashSet<String>) {
self.unused_classes = all_classes
.difference(&self.used_classes)
.cloned()
.collect();
self.unused_classes = self
.unused_classes
.difference(&self.critical_classes)
.cloned()
.collect();
let mut to_check = self.used_classes.clone();
while !to_check.is_empty() {
let mut new_dependencies = HashSet::new();
for class in &to_check {
if let Some(deps) = self.dependencies.get(class) {
for dep in deps {
if !self.used_classes.contains(dep) {
new_dependencies.insert(dep.clone());
self.used_classes.insert(dep.clone());
}
}
}
}
to_check = new_dependencies;
}
}
pub fn get_optimization_suggestions(&self) -> Vec<String> {
let mut suggestions = Vec::new();
if !self.unused_classes.is_empty() {
suggestions.push(format!(
"Remove {} unused classes to reduce bundle size",
self.unused_classes.len()
));
}
let critical_count = self.critical_classes.len();
if critical_count > 0 {
suggestions.push(format!(
"{} critical classes are protected from removal",
critical_count
));
}
let dependency_count: usize = self.dependencies.values().map(|deps| deps.len()).sum();
if dependency_count > 0 {
suggestions.push(format!(
"Found {} class dependencies that may affect optimization",
dependency_count
));
}
suggestions
}
}
impl Default for CssPurger {
fn default() -> Self {
Self::new()
}
}
impl CssPurger {
pub fn new() -> Self {
Self {
keep_classes: HashSet::new(),
remove_classes: HashSet::new(),
keep_rules: HashSet::new(),
remove_rules: HashSet::new(),
}
}
pub fn keep_classes(&mut self, classes: HashSet<String>) {
self.keep_classes.extend(classes);
}
pub fn remove_classes(&mut self, classes: HashSet<String>) {
self.remove_classes.extend(classes);
}
pub fn purge_css(&self, css: &str) -> String {
let mut result = String::new();
let lines: Vec<&str> = css.lines().collect();
let mut in_rule = false;
let mut current_rule = String::new();
let mut rule_selectors = Vec::new();
for line in lines {
let trimmed = line.trim();
if trimmed.ends_with('{') {
in_rule = true;
current_rule = line.to_string();
rule_selectors = self.extract_selectors(trimmed);
} else if trimmed == "}" && in_rule {
current_rule.push_str(&format!("{}\n", line));
if self.should_keep_rule(&rule_selectors) {
result.push_str(¤t_rule);
}
in_rule = false;
current_rule.clear();
rule_selectors.clear();
} else if in_rule {
current_rule.push_str(&format!("{}\n", line));
} else {
result.push_str(&format!("{}\n", line));
}
}
result
}
fn extract_selectors(&self, line: &str) -> Vec<String> {
let selector_part = line.trim_end_matches(" {");
selector_part
.split(',')
.map(|s| s.trim().to_string())
.collect()
}
fn should_keep_rule(&self, selectors: &[String]) -> bool {
for selector in selectors {
if self.should_keep_selector(selector) {
return true;
}
}
false
}
fn should_keep_selector(&self, selector: &str) -> bool {
if selector.starts_with('*') || selector.starts_with("html") || selector.starts_with("body")
{
return true;
}
for class in &self.keep_classes {
if selector.contains(&format!(".{}", class)) {
return true;
}
}
for class in &self.remove_classes {
if selector.contains(&format!(".{}", class)) {
return false;
}
}
!selector.contains('.')
}
pub fn calculate_optimization(
&self,
original_css: &str,
optimized_css: &str,
) -> OptimizationResult {
let original_size = original_css.len();
let optimized_size = optimized_css.len();
let reduction_percentage = if original_size > 0 {
((original_size - optimized_size) as f32 / original_size as f32) * 100.0
} else {
0.0
};
let classes_removed = self.remove_classes.len();
let rules_removed = self.remove_rules.len();
let mut warnings = Vec::new();
if reduction_percentage > 50.0 {
warnings.push(
"Large size reduction detected. Verify all functionality still works.".to_string(),
);
}
if classes_removed > 100 {
warnings.push("Many classes removed. Check for missing styles.".to_string());
}
OptimizationResult {
original_size,
optimized_size,
reduction_percentage,
classes_removed,
rules_removed,
warnings,
}
}
}
impl Default for BundleAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl BundleAnalyzer {
pub fn new() -> Self {
Self {
class_stats: HashMap::new(),
rule_stats: HashMap::new(),
metrics: PerformanceMetrics {
total_size: 0,
class_count: 0,
rule_count: 0,
avg_class_size: 0.0,
avg_rule_size: 0.0,
compression_ratio: 0.0,
},
}
}
pub fn analyze_bundle(&mut self, css: &str) {
self.analyze_classes(css);
self.analyze_rules(css);
self.calculate_metrics(css);
}
fn analyze_classes(&mut self, css: &str) {
let lines: Vec<&str> = css.lines().collect();
for line in lines {
if line.contains('.') && line.contains('{') {
let selectors = self.extract_selectors(line);
for selector in selectors {
if let Some(class_name) = self.extract_class_name(&selector) {
let stats =
self.class_stats
.entry(class_name.clone())
.or_insert_with(|| ClassUsageStats {
usage_count: 0,
used_in_files: HashSet::new(),
is_critical: false,
dependencies: HashSet::new(),
});
stats.usage_count += 1;
}
}
}
}
}
fn analyze_rules(&mut self, css: &str) {
let lines: Vec<&str> = css.lines().collect();
let mut current_rule = String::new();
let mut in_rule = false;
for line in lines {
let trimmed = line.trim();
if trimmed.ends_with('{') {
in_rule = true;
current_rule = line.to_string();
} else if trimmed == "}" && in_rule {
current_rule.push_str(&format!("{}\n", line));
let rule_id = format!("rule_{}", self.rule_stats.len());
let selectors = self.extract_selectors(¤t_rule);
let properties = self.extract_properties(¤t_rule);
self.rule_stats.insert(
rule_id,
RuleUsageStats {
usage_count: 1,
selectors: selectors.into_iter().collect(),
properties: properties.into_iter().collect(),
size_bytes: current_rule.len(),
},
);
in_rule = false;
current_rule.clear();
} else if in_rule {
current_rule.push_str(&format!("{}\n", line));
}
}
}
fn calculate_metrics(&mut self, css: &str) {
self.metrics.total_size = css.len();
self.metrics.class_count = self.class_stats.len();
self.metrics.rule_count = self.rule_stats.len();
if self.metrics.class_count > 0 {
let total_class_size: usize = self
.class_stats
.values()
.map(|stats| stats.usage_count as usize * 10) .sum();
self.metrics.avg_class_size = total_class_size as f32 / self.metrics.class_count as f32;
}
if self.metrics.rule_count > 0 {
let total_rule_size: usize =
self.rule_stats.values().map(|stats| stats.size_bytes).sum();
self.metrics.avg_rule_size = total_rule_size as f32 / self.metrics.rule_count as f32;
}
self.metrics.compression_ratio = if self.metrics.total_size > 0 {
(self.metrics.total_size as f32 - self.metrics.avg_rule_size)
/ self.metrics.total_size as f32
} else {
0.0
};
}
fn extract_selectors(&self, line: &str) -> Vec<String> {
let selector_part = line.trim_end_matches(" {");
selector_part
.split(',')
.map(|s| s.trim().to_string())
.collect()
}
fn extract_class_name(&self, selector: &str) -> Option<String> {
if let Some(start) = selector.find('.') {
let class_part = &selector[start + 1..];
if let Some(end) =
class_part.find(|c: char| !c.is_alphanumeric() && c != '-' && c != '_')
{
Some(class_part[..end].to_string())
} else {
Some(class_part.to_string())
}
} else {
None
}
}
fn extract_properties(&self, rule: &str) -> Vec<String> {
let mut properties = Vec::new();
let lines: Vec<&str> = rule.lines().collect();
for line in lines {
let trimmed = line.trim();
if trimmed.contains(':') && !trimmed.ends_with('{') && !trimmed.ends_with('}') {
if let Some(colon_pos) = trimmed.find(':') {
let property = trimmed[..colon_pos].trim().to_string();
properties.push(property);
}
}
}
properties
}
pub fn get_recommendations(&self) -> Vec<String> {
let mut recommendations = Vec::new();
if self.metrics.total_size > 100_000 {
recommendations.push("Bundle size is large. Consider code splitting.".to_string());
}
if self.metrics.class_count > 1000 {
recommendations
.push("Many classes detected. Consider purging unused classes.".to_string());
}
if self.metrics.avg_rule_size > 200.0 {
recommendations
.push("Large CSS rules detected. Consider breaking them down.".to_string());
}
if self.metrics.compression_ratio < 0.3 {
recommendations.push("Low compression ratio. Consider optimization.".to_string());
}
let unused_classes: Vec<_> = self
.class_stats
.iter()
.filter(|(_, stats)| stats.usage_count == 1)
.map(|(name, _)| name)
.collect();
if unused_classes.len() > 50 {
recommendations.push(format!(
"{} classes used only once. Consider consolidation.",
unused_classes.len()
));
}
recommendations
}
}
impl fmt::Display for OptimizationResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Optimization Result: {} bytes -> {} bytes ({}% reduction)",
self.original_size, self.optimized_size, self.reduction_percentage
)
}
}
impl fmt::Display for PerformanceMetrics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Bundle: {} bytes, {} classes, {} rules, {:.1}% compression",
self.total_size,
self.class_count,
self.rule_count,
self.compression_ratio * 100.0
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_class_analyzer_creation() {
let analyzer = ClassAnalyzer::new();
assert!(analyzer.used_classes.is_empty());
assert!(analyzer.unused_classes.is_empty());
assert!(analyzer.dependencies.is_empty());
assert!(analyzer.critical_classes.is_empty());
}
#[test]
fn test_class_analyzer_add_used_class() {
let mut analyzer = ClassAnalyzer::new();
analyzer.add_used_class("bg-red-500".to_string());
assert!(analyzer.used_classes.contains("bg-red-500"));
assert_eq!(analyzer.used_classes.len(), 1);
}
#[test]
fn test_class_analyzer_add_multiple_classes() {
let mut analyzer = ClassAnalyzer::new();
analyzer.add_used_classes(vec!["bg-red-500".to_string(), "text-white".to_string()]);
assert!(analyzer.used_classes.contains("bg-red-500"));
assert!(analyzer.used_classes.contains("text-white"));
assert_eq!(analyzer.used_classes.len(), 2);
}
#[test]
fn test_class_analyzer_add_dependency() {
let mut analyzer = ClassAnalyzer::new();
analyzer.add_dependency("btn".to_string(), "bg-blue-500".to_string());
assert!(analyzer.dependencies.contains_key("btn"));
assert!(analyzer.dependencies["btn"].contains("bg-blue-500"));
}
#[test]
fn test_class_analyzer_mark_critical() {
let mut analyzer = ClassAnalyzer::new();
analyzer.mark_critical("container".to_string());
assert!(analyzer.critical_classes.contains("container"));
}
#[test]
fn test_class_analyzer_analyze_usage() {
let mut analyzer = ClassAnalyzer::new();
analyzer.add_used_class("bg-red-500".to_string());
analyzer.mark_critical("container".to_string());
let all_classes: HashSet<String> = vec![
"bg-red-500".to_string(),
"bg-blue-500".to_string(),
"container".to_string(),
"unused-class".to_string(),
]
.into_iter()
.collect();
analyzer.analyze_usage(all_classes);
assert!(analyzer.used_classes.contains("bg-red-500"));
assert!(analyzer.unused_classes.contains("bg-blue-500"));
assert!(analyzer.unused_classes.contains("unused-class"));
assert!(!analyzer.unused_classes.contains("container")); }
#[test]
fn test_css_purger_creation() {
let purger = CssPurger::new();
assert!(purger.keep_classes.is_empty());
assert!(purger.remove_classes.is_empty());
assert!(purger.keep_rules.is_empty());
assert!(purger.remove_rules.is_empty());
}
#[test]
fn test_css_purger_keep_classes() {
let mut purger = CssPurger::new();
let classes: HashSet<String> = vec!["bg-red-500".to_string(), "text-white".to_string()]
.into_iter()
.collect();
purger.keep_classes(classes);
assert!(purger.keep_classes.contains("bg-red-500"));
assert!(purger.keep_classes.contains("text-white"));
}
#[test]
fn test_css_purger_remove_classes() {
let mut purger = CssPurger::new();
let classes: HashSet<String> = vec!["unused-class".to_string()].into_iter().collect();
purger.remove_classes(classes);
assert!(purger.remove_classes.contains("unused-class"));
}
#[test]
fn test_css_purger_purge_css() {
let mut purger = CssPurger::new();
purger.keep_classes(vec!["bg-red-500".to_string()].into_iter().collect());
purger.remove_classes(vec!["unused-class".to_string()].into_iter().collect());
let css = r#"
.bg-red-500 { background-color: #ef4444; }
.unused-class { display: none; }
.text-white { color: white; }
"#;
let result = purger.purge_css(css);
assert!(!result.is_empty());
assert!(result.contains(".bg-red-500"));
assert!(result.contains("{"));
assert!(result.contains("}"));
}
#[test]
fn test_css_purger_calculate_optimization() {
let mut purger = CssPurger::new();
purger.remove_classes(vec!["unused-class".to_string()].into_iter().collect());
let original_css = ".bg-red-500 { color: red; } .unused-class { display: none; }";
let optimized_css = ".bg-red-500 { color: red; }";
let result = purger.calculate_optimization(original_css, optimized_css);
assert!(result.original_size > result.optimized_size);
assert!(result.reduction_percentage > 0.0);
assert_eq!(result.classes_removed, 1);
}
#[test]
fn test_bundle_analyzer_creation() {
let analyzer = BundleAnalyzer::new();
assert!(analyzer.class_stats.is_empty());
assert!(analyzer.rule_stats.is_empty());
assert_eq!(analyzer.metrics.total_size, 0);
}
#[test]
fn test_bundle_analyzer_analyze_bundle() {
let mut analyzer = BundleAnalyzer::new();
let css = r#"
.bg-red-500 { background-color: #ef4444; }
.text-white { color: white; }
"#;
analyzer.analyze_bundle(css);
assert!(analyzer.class_stats.contains_key("bg-red-500"));
assert!(analyzer.class_stats.contains_key("text-white"));
assert!(analyzer.metrics.class_count >= 2);
assert!(analyzer.metrics.rule_count == 0); }
#[test]
fn test_optimization_result_display() {
let result = OptimizationResult {
original_size: 1000,
optimized_size: 500,
reduction_percentage: 50.0,
classes_removed: 10,
rules_removed: 5,
warnings: vec!["Test warning".to_string()],
};
let display = format!("{}", result);
assert!(display.contains("1000 bytes -> 500 bytes"));
assert!(display.contains("50% reduction"));
}
#[test]
fn test_performance_metrics_display() {
let metrics = PerformanceMetrics {
total_size: 10000,
class_count: 100,
rule_count: 50,
avg_class_size: 10.0,
avg_rule_size: 20.0,
compression_ratio: 0.3,
};
let display = format!("{}", metrics);
assert!(display.contains("10000 bytes"));
assert!(display.contains("100 classes"));
assert!(display.contains("50 rules"));
assert!(display.contains("30.0% compression"));
}
#[test]
fn test_class_analyzer_serialization() {
let mut analyzer = ClassAnalyzer::new();
analyzer.add_used_class("bg-red-500".to_string());
analyzer.mark_critical("container".to_string());
let serialized = serde_json::to_string(&analyzer).unwrap();
let deserialized: ClassAnalyzer = serde_json::from_str(&serialized).unwrap();
assert_eq!(analyzer, deserialized);
}
#[test]
fn test_css_purger_serialization() {
let mut purger = CssPurger::new();
purger.keep_classes(vec!["bg-red-500".to_string()].into_iter().collect());
purger.remove_classes(vec!["unused-class".to_string()].into_iter().collect());
let serialized = serde_json::to_string(&purger).unwrap();
let deserialized: CssPurger = serde_json::from_str(&serialized).unwrap();
assert_eq!(purger, deserialized);
}
#[test]
fn test_bundle_analyzer_serialization() {
let mut analyzer = BundleAnalyzer::new();
analyzer.analyze_bundle(".test { color: red; }");
let serialized = serde_json::to_string(&analyzer).unwrap();
let deserialized: BundleAnalyzer = serde_json::from_str(&serialized).unwrap();
assert_eq!(analyzer, deserialized);
}
}