tailwind_rs_core/
css_optimizer.rs

1//! Advanced CSS optimization system
2//!
3//! This module provides comprehensive CSS optimization features including minification,
4//! compression, rule merging, and performance optimizations.
5
6use crate::css_generator::{CssGenerator, CssRule, CssProperty};
7use crate::error::Result;
8use std::collections::HashMap;
9
10/// Configuration for CSS optimization
11#[derive(Debug, Clone)]
12pub struct OptimizationConfig {
13    /// Whether to enable minification
14    pub minify: bool,
15    /// Whether to enable rule merging
16    pub merge_rules: bool,
17    /// Whether to enable property optimization
18    pub optimize_properties: bool,
19    /// Whether to enable selector optimization
20    pub optimize_selectors: bool,
21    /// Whether to remove empty rules
22    pub remove_empty_rules: bool,
23    /// Whether to remove duplicate properties
24    pub remove_duplicates: bool,
25    /// Whether to sort properties
26    pub sort_properties: bool,
27    /// Whether to enable advanced compression
28    pub advanced_compression: bool,
29    /// Maximum compression level (0-9)
30    pub compression_level: u8,
31}
32
33impl Default for OptimizationConfig {
34    fn default() -> Self {
35        Self {
36            minify: true,
37            merge_rules: true,
38            optimize_properties: true,
39            optimize_selectors: true,
40            remove_empty_rules: true,
41            remove_duplicates: true,
42            sort_properties: true,
43            advanced_compression: false,
44            compression_level: 6,
45        }
46    }
47}
48
49/// Results of CSS optimization
50#[derive(Debug, Clone)]
51pub struct OptimizationResults {
52    /// Original CSS size
53    pub original_size: usize,
54    /// Optimized CSS size
55    pub optimized_size: usize,
56    /// Size reduction in bytes
57    pub size_reduction: usize,
58    /// Size reduction percentage
59    pub reduction_percentage: f64,
60    /// Number of rules before optimization
61    pub original_rules: usize,
62    /// Number of rules after optimization
63    pub optimized_rules: usize,
64    /// Number of properties before optimization
65    pub original_properties: usize,
66    /// Number of properties after optimization
67    pub optimized_properties: usize,
68    /// Statistics
69    pub stats: OptimizationStats,
70}
71
72/// Statistics for optimization operation
73#[derive(Debug, Clone)]
74pub struct OptimizationStats {
75    /// Number of rules merged
76    pub rules_merged: usize,
77    /// Number of properties optimized
78    pub properties_optimized: usize,
79    /// Number of selectors optimized
80    pub selectors_optimized: usize,
81    /// Number of empty rules removed
82    pub empty_rules_removed: usize,
83    /// Number of duplicate properties removed
84    pub duplicate_properties_removed: usize,
85    /// Processing time in milliseconds
86    pub processing_time_ms: u64,
87}
88
89/// Internal statistics tracking for optimization operations
90#[derive(Debug, Clone, Default)]
91struct OptimizationTracker {
92    empty_rules_removed: usize,
93    duplicate_properties_removed: usize,
94    selectors_optimized: usize,
95}
96
97/// Advanced CSS optimizer
98#[derive(Debug, Clone)]
99pub struct CssOptimizer {
100    config: OptimizationConfig,
101}
102
103impl CssOptimizer {
104    /// Create a new CSS optimizer with default configuration
105    pub fn new() -> Self {
106        Self {
107            config: OptimizationConfig::default(),
108        }
109    }
110
111    /// Create a new CSS optimizer with custom configuration
112    pub fn with_config(config: OptimizationConfig) -> Self {
113        Self { config }
114    }
115
116    /// Optimize CSS from a generator
117    pub fn optimize(&self, generator: &mut CssGenerator) -> Result<OptimizationResults> {
118        let start_time = std::time::Instant::now();
119        
120        // Get original metrics
121        let original_css = generator.generate_css();
122        let original_size = original_css.len();
123        let original_rules = generator.rule_count();
124        let original_properties = self.count_properties(generator);
125
126        // Initialize optimization tracker
127        let mut tracker = OptimizationTracker::default();
128
129        // Apply optimizations
130        if self.config.remove_empty_rules {
131            tracker.empty_rules_removed = self.remove_empty_rules(generator);
132        }
133
134        if self.config.remove_duplicates {
135            tracker.duplicate_properties_removed = self.remove_duplicate_properties(generator);
136        }
137
138        if self.config.optimize_properties {
139            self.optimize_properties(generator);
140        }
141
142        if self.config.merge_rules {
143            self.merge_compatible_rules(generator);
144        }
145
146        if self.config.sort_properties {
147            self.sort_properties(generator);
148        }
149
150        // Get optimized metrics
151        let optimized_css = if self.config.minify {
152            generator.generate_minified_css()
153        } else {
154            generator.generate_css()
155        };
156        
157        let optimized_size = optimized_css.len();
158        let optimized_rules = generator.rule_count();
159        let optimized_properties = self.count_properties(generator);
160
161        // Calculate results
162        let size_reduction = original_size.saturating_sub(optimized_size);
163        let reduction_percentage = if original_size > 0 {
164            (size_reduction as f64 / original_size as f64) * 100.0
165        } else {
166            0.0
167        };
168
169        let stats = OptimizationStats {
170            rules_merged: original_rules.saturating_sub(optimized_rules),
171            properties_optimized: original_properties.saturating_sub(optimized_properties),
172            selectors_optimized: tracker.selectors_optimized,
173            empty_rules_removed: tracker.empty_rules_removed,
174            duplicate_properties_removed: tracker.duplicate_properties_removed,
175            processing_time_ms: start_time.elapsed().as_millis() as u64,
176        };
177
178        Ok(OptimizationResults {
179            original_size,
180            optimized_size,
181            size_reduction,
182            reduction_percentage,
183            original_rules,
184            optimized_rules,
185            original_properties,
186            optimized_properties,
187            stats,
188        })
189    }
190
191    /// Optimize CSS from a string
192    pub fn optimize_css(&self, css: &str) -> Result<String> {
193        let mut generator = CssGenerator::new();
194        
195        // Parse CSS into generator (simplified implementation)
196        self.parse_css_into_generator(css, &mut generator)?;
197        
198        // Optimize
199        self.optimize(&mut generator)?;
200        
201        // Return optimized CSS
202        if self.config.minify {
203            Ok(generator.generate_minified_css())
204        } else {
205            Ok(generator.generate_css())
206        }
207    }
208
209    /// Get the current configuration
210    pub fn get_config(&self) -> &OptimizationConfig {
211        &self.config
212    }
213
214    /// Update the configuration
215    pub fn set_config(&mut self, config: OptimizationConfig) {
216        self.config = config;
217    }
218
219    /// Count total properties in generator
220    fn count_properties(&self, generator: &CssGenerator) -> usize {
221        generator.get_rules().values()
222            .map(|rule| rule.properties.len())
223            .sum()
224    }
225
226    /// Remove empty CSS rules
227    fn remove_empty_rules(&self, generator: &mut CssGenerator) -> usize {
228        let rules = generator.get_rules().clone();
229        let mut removed_count = 0;
230        for (selector, rule) in rules {
231            if rule.properties.is_empty() {
232                generator.remove_rule(&selector);
233                removed_count += 1;
234            }
235        }
236        removed_count
237    }
238
239    /// Remove duplicate properties within rules
240    fn remove_duplicate_properties(&self, generator: &mut CssGenerator) -> usize {
241        let rules = generator.get_rules().clone();
242        let mut total_removed = 0;
243        for (selector, rule) in rules {
244            let mut seen_properties = std::collections::HashSet::new();
245            let mut unique_properties = Vec::new();
246            
247            for property in &rule.properties {
248                if seen_properties.insert(&property.name) {
249                    unique_properties.push(property.clone());
250                }
251            }
252            
253            let removed_count = rule.properties.len() - unique_properties.len();
254            if removed_count > 0 {
255                total_removed += removed_count;
256                let updated_rule = CssRule {
257                    selector: rule.selector.clone(),
258                    properties: unique_properties,
259                    media_query: rule.media_query.clone(),
260                    specificity: rule.specificity,
261                };
262                generator.update_rule(&selector, updated_rule);
263            }
264        }
265        total_removed
266    }
267
268    /// Optimize CSS properties
269    fn optimize_properties(&self, generator: &mut CssGenerator) {
270        let rules = generator.get_rules().clone();
271        for (selector, rule) in rules {
272            let mut optimized_properties = Vec::new();
273            
274            for property in &rule.properties {
275                let optimized_property = CssProperty {
276                    name: property.name.clone(),
277                    value: self.optimize_property_value(&property.value),
278                    important: property.important,
279                };
280                optimized_properties.push(optimized_property);
281            }
282            
283            // Update the rule with optimized properties
284            let updated_rule = CssRule {
285                selector: rule.selector.clone(),
286                properties: optimized_properties,
287                media_query: rule.media_query.clone(),
288                specificity: rule.specificity,
289            };
290            generator.update_rule(&selector, updated_rule);
291        }
292    }
293
294    /// Optimize a single property value
295    fn optimize_property_value(&self, value: &str) -> String {
296        let mut optimized = value.to_string();
297        
298        // Convert 0px to 0
299        optimized = optimized.replace("0px", "0");
300        optimized = optimized.replace("0em", "0");
301        optimized = optimized.replace("0rem", "0");
302        
303        // Convert redundant units
304        optimized = optimized.replace("0.0", "0");
305        optimized = optimized.replace("1.0", "1");
306        
307        optimized
308    }
309
310    /// Merge compatible CSS rules
311    fn merge_compatible_rules(&self, generator: &mut CssGenerator) {
312        let rules = generator.get_rules().clone();
313        let mut merged_rules: HashMap<String, CssRule> = HashMap::new();
314        
315        for (selector, rule) in rules {
316            // Simple merging: combine rules with same properties
317            if let Some(existing_rule) = merged_rules.get_mut(&selector) {
318                // Merge properties
319                for property in &rule.properties {
320                    if !existing_rule.properties.iter().any(|p| p.name == property.name) {
321                        existing_rule.properties.push(property.clone());
322                    }
323                }
324            } else {
325                merged_rules.insert(selector, rule);
326            }
327        }
328        
329        // Update generator with merged rules
330        for (selector, rule) in merged_rules {
331            generator.update_rule(&selector, rule);
332        }
333    }
334
335    /// Sort CSS properties for better compression
336    fn sort_properties(&self, generator: &mut CssGenerator) {
337        let rules = generator.get_rules().clone();
338        
339        for (selector, rule) in rules {
340            let mut sorted_properties = rule.properties.clone();
341            sorted_properties.sort_by(|a, b| a.name.cmp(&b.name));
342            
343            let sorted_rule = CssRule {
344                selector: rule.selector.clone(),
345                properties: sorted_properties,
346                media_query: rule.media_query.clone(),
347                specificity: rule.specificity,
348            };
349            generator.update_rule(&selector, sorted_rule);
350        }
351    }
352
353    /// Parse CSS string into generator (simplified implementation)
354    fn parse_css_into_generator(&self, css: &str, generator: &mut CssGenerator) -> Result<()> {
355        // Simple CSS parsing - extract basic rules
356        let lines: Vec<&str> = css.lines().collect();
357        let mut i = 0;
358        
359        while i < lines.len() {
360            let line = lines[i].trim();
361            
362            // Look for CSS rules (simplified pattern matching)
363            if line.ends_with('{') && line.contains('.') {
364                let selector = line.replace('{', "").trim().to_string();
365                
366                // Collect properties until we find the closing brace
367                let mut properties = Vec::new();
368                i += 1;
369                
370                while i < lines.len() && !lines[i].trim().starts_with('}') {
371                    let prop_line = lines[i].trim();
372                    if prop_line.contains(':') && prop_line.ends_with(';') {
373                        let parts: Vec<&str> = prop_line.split(':').collect();
374                        if parts.len() == 2 {
375                            let name = parts[0].trim().to_string();
376                            let value = parts[1].trim().replace(';', "").to_string();
377                            properties.push(CssProperty {
378                                name,
379                                value,
380                                important: false,
381                            });
382                        }
383                    }
384                    i += 1;
385                }
386                
387                // Add the rule to the generator
388                let rule = CssRule {
389                    selector,
390                    properties,
391                    media_query: None,
392                    specificity: 1,
393                };
394                let selector = rule.selector.clone();
395                generator.update_rule(&selector, rule);
396            }
397            i += 1;
398        }
399        
400        Ok(())
401    }
402
403    /// Advanced CSS compression
404    pub fn compress_css(&self, css: &str) -> Result<String> {
405        let mut compressed = css.to_string();
406
407        // Always remove comments and optimize, regardless of advanced_compression setting
408        // Remove comments
409        compressed = self.remove_comments(&compressed);
410        
411        // Remove unnecessary whitespace
412        compressed = self.remove_unnecessary_whitespace(&compressed);
413        
414        // Optimize colors
415        compressed = self.optimize_colors(&compressed);
416        
417        // Optimize units
418        compressed = self.optimize_units(&compressed);
419
420        Ok(compressed)
421    }
422
423
424    /// Remove CSS comments
425    fn remove_comments(&self, css: &str) -> String {
426        let mut result = String::new();
427        let mut chars = css.chars().peekable();
428        
429        while let Some(c) = chars.next() {
430            if c == '/' && chars.peek() == Some(&'*') {
431                // Skip comment
432                chars.next(); // consume *
433                while let Some(c) = chars.next() {
434                    if c == '*' && chars.peek() == Some(&'/') {
435                        chars.next(); // consume /
436                        break;
437                    }
438                }
439            } else {
440                result.push(c);
441            }
442        }
443        
444        result
445    }
446
447    /// Remove unnecessary whitespace
448    fn remove_unnecessary_whitespace(&self, css: &str) -> String {
449        css.chars()
450            .filter(|c| !c.is_whitespace() || *c == ' ')
451            .collect::<String>()
452            .replace(" {", "{")
453            .replace("{ ", "{")
454            .replace("} ", "}")
455            .replace("; ", ";")
456            .replace(": ", ":")
457            .replace(", ", ",")
458    }
459
460    /// Optimize color values
461    fn optimize_colors(&self, css: &str) -> String {
462        let mut optimized = css.to_string();
463        
464        // Convert #ffffff to #fff (simplified without backreferences)
465        optimized = regex::Regex::new(r"#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])")
466            .unwrap()
467            .replace_all(&optimized, |caps: &regex::Captures| {
468                let r1 = &caps[1];
469                let g1 = &caps[2];
470                let b1 = &caps[3];
471                let r2 = &caps[4];
472                let g2 = &caps[5];
473                let b2 = &caps[6];
474                
475                // Only compress if all pairs are the same
476                if r1 == r2 && g1 == g2 && b1 == b2 {
477                    format!("#{}{}{}", r1, g1, b1)
478                } else {
479                    caps[0].to_string()
480                }
481            })
482            .to_string();
483        
484        // Convert rgb(255, 255, 255) to #ffffff (simplified without backreferences)
485        optimized = regex::Regex::new(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)")
486            .unwrap()
487            .replace_all(&optimized, |caps: &regex::Captures| {
488                let r = caps.get(1).unwrap().as_str().parse::<u8>().unwrap();
489                let g = caps.get(2).unwrap().as_str().parse::<u8>().unwrap();
490                let b = caps.get(3).unwrap().as_str().parse::<u8>().unwrap();
491                format!("#{:02x}{:02x}{:02x}", r, g, b)
492            })
493            .to_string();
494        
495        optimized
496    }
497
498    /// Optimize CSS units
499    fn optimize_units(&self, css: &str) -> String {
500        let mut optimized = css.to_string();
501        
502        // Convert 0px to 0
503        optimized = regex::Regex::new(r"(\d+)px")
504            .unwrap()
505            .replace_all(&optimized, "$1")
506            .to_string();
507        
508        // Convert 0em to 0
509        optimized = regex::Regex::new(r"(\d+)em")
510            .unwrap()
511            .replace_all(&optimized, "$1")
512            .to_string();
513        
514        optimized
515    }
516}
517
518impl Default for CssOptimizer {
519    fn default() -> Self {
520        Self::new()
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    #[test]
529    fn test_optimizer_creation() {
530        let optimizer = CssOptimizer::new();
531        assert!(optimizer.get_config().minify);
532        assert!(optimizer.get_config().merge_rules);
533    }
534
535    #[test]
536    fn test_custom_config() {
537        let config = OptimizationConfig {
538            minify: false,
539            merge_rules: false,
540            optimize_properties: false,
541            optimize_selectors: false,
542            remove_empty_rules: false,
543            remove_duplicates: false,
544            sort_properties: false,
545            advanced_compression: true,
546            compression_level: 9,
547        };
548        
549        let optimizer = CssOptimizer::with_config(config);
550        assert!(!optimizer.get_config().minify);
551        assert!(optimizer.get_config().advanced_compression);
552        assert_eq!(optimizer.get_config().compression_level, 9);
553    }
554
555    #[test]
556    fn test_optimize_css() {
557        let optimizer = CssOptimizer::new();
558        let css = r#"
559            .test {
560                padding: 1rem;
561                margin: 0px;
562                color: #ffffff;
563            }
564        "#;
565        
566        let result = optimizer.optimize_css(css).unwrap();
567        assert!(result.len() <= css.len());
568    }
569
570    #[test]
571    fn test_compress_css() {
572        let optimizer = CssOptimizer::new();
573        let css = r#"
574            /* This is a comment */
575            .test {
576                padding: 1rem;
577                margin: 0px;
578                color: #ffffff;
579            }
580        "#;
581        
582        let compressed = optimizer.compress_css(css).unwrap();
583        assert!(!compressed.contains("/*"));
584        assert!(!compressed.contains("*/"));
585        assert!(compressed.len() < css.len());
586    }
587
588    #[test]
589    fn test_remove_comments() {
590        let optimizer = CssOptimizer::new();
591        let css = "/* comment */ .test { color: red; }";
592        let result = optimizer.remove_comments(css);
593        assert!(!result.contains("/*"));
594        assert!(!result.contains("*/"));
595    }
596
597    #[test]
598    fn test_remove_unnecessary_whitespace() {
599        let optimizer = CssOptimizer::new();
600        let css = ".test {\n  color: red;\n  margin: 0px;\n}";
601        let result = optimizer.remove_unnecessary_whitespace(css);
602        assert!(!result.contains('\n'));
603        assert!(!result.contains("  "));
604    }
605
606    #[test]
607    fn test_optimize_colors() {
608        let optimizer = CssOptimizer::new();
609        let css = "color: #ffffff; background: #000000;";
610        let result = optimizer.optimize_colors(css);
611        assert!(result.contains("#fff"));
612        assert!(result.contains("#000"));
613    }
614
615    #[test]
616    fn test_optimize_units() {
617        let optimizer = CssOptimizer::new();
618        let css = "margin: 0px; padding: 1rem;";
619        let result = optimizer.optimize_units(css);
620        assert!(result.contains("margin: 0"));
621        assert!(result.contains("padding: 1rem"));
622    }
623
624    #[test]
625    fn test_optimize_generator() {
626        let mut generator = CssGenerator::new();
627        generator.add_class("p-4").unwrap();
628        generator.add_class("bg-blue-500").unwrap();
629        
630        let optimizer = CssOptimizer::new();
631        let results = optimizer.optimize(&mut generator).unwrap();
632        
633        assert!(results.original_size > 0);
634        assert!(results.optimized_size > 0);
635        assert!(results.reduction_percentage >= 0.0);
636    }
637}