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, CssProperty, CssRule};
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
222            .get_rules()
223            .values()
224            .map(|rule| rule.properties.len())
225            .sum()
226    }
227
228    /// Remove empty CSS rules
229    fn remove_empty_rules(&self, generator: &mut CssGenerator) -> usize {
230        let rules = generator.get_rules().clone();
231        let mut removed_count = 0;
232        for (selector, rule) in rules {
233            if rule.properties.is_empty() {
234                generator.remove_rule(&selector);
235                removed_count += 1;
236            }
237        }
238        removed_count
239    }
240
241    /// Remove duplicate properties within rules
242    fn remove_duplicate_properties(&self, generator: &mut CssGenerator) -> usize {
243        let rules = generator.get_rules().clone();
244        let mut total_removed = 0;
245        for (selector, rule) in rules {
246            let mut seen_properties = std::collections::HashSet::new();
247            let mut unique_properties = Vec::new();
248
249            for property in &rule.properties {
250                if seen_properties.insert(&property.name) {
251                    unique_properties.push(property.clone());
252                }
253            }
254
255            let removed_count = rule.properties.len() - unique_properties.len();
256            if removed_count > 0 {
257                total_removed += removed_count;
258                let updated_rule = CssRule {
259                    selector: rule.selector.clone(),
260                    properties: unique_properties,
261                    media_query: rule.media_query.clone(),
262                    specificity: rule.specificity,
263                };
264                generator.update_rule(&selector, updated_rule);
265            }
266        }
267        total_removed
268    }
269
270    /// Optimize CSS properties
271    fn optimize_properties(&self, generator: &mut CssGenerator) {
272        let rules = generator.get_rules().clone();
273        for (selector, rule) in rules {
274            let mut optimized_properties = Vec::new();
275
276            for property in &rule.properties {
277                let optimized_property = CssProperty {
278                    name: property.name.clone(),
279                    value: self.optimize_property_value(&property.value),
280                    important: property.important,
281                };
282                optimized_properties.push(optimized_property);
283            }
284
285            // Update the rule with optimized properties
286            let updated_rule = CssRule {
287                selector: rule.selector.clone(),
288                properties: optimized_properties,
289                media_query: rule.media_query.clone(),
290                specificity: rule.specificity,
291            };
292            generator.update_rule(&selector, updated_rule);
293        }
294    }
295
296    /// Optimize a single property value
297    fn optimize_property_value(&self, value: &str) -> String {
298        let mut optimized = value.to_string();
299
300        // Convert 0px to 0
301        optimized = optimized.replace("0px", "0");
302        optimized = optimized.replace("0em", "0");
303        optimized = optimized.replace("0rem", "0");
304
305        // Convert redundant units
306        optimized = optimized.replace("0.0", "0");
307        optimized = optimized.replace("1.0", "1");
308
309        optimized
310    }
311
312    /// Merge compatible CSS rules
313    fn merge_compatible_rules(&self, generator: &mut CssGenerator) {
314        let rules = generator.get_rules().clone();
315        let mut merged_rules: HashMap<String, CssRule> = HashMap::new();
316
317        for (selector, rule) in rules {
318            // Simple merging: combine rules with same properties
319            if let Some(existing_rule) = merged_rules.get_mut(&selector) {
320                // Merge properties
321                for property in &rule.properties {
322                    if !existing_rule
323                        .properties
324                        .iter()
325                        .any(|p| p.name == property.name)
326                    {
327                        existing_rule.properties.push(property.clone());
328                    }
329                }
330            } else {
331                merged_rules.insert(selector, rule);
332            }
333        }
334
335        // Update generator with merged rules
336        for (selector, rule) in merged_rules {
337            generator.update_rule(&selector, rule);
338        }
339    }
340
341    /// Sort CSS properties for better compression
342    fn sort_properties(&self, generator: &mut CssGenerator) {
343        let rules = generator.get_rules().clone();
344
345        for (selector, rule) in rules {
346            let mut sorted_properties = rule.properties.clone();
347            sorted_properties.sort_by(|a, b| a.name.cmp(&b.name));
348
349            let sorted_rule = CssRule {
350                selector: rule.selector.clone(),
351                properties: sorted_properties,
352                media_query: rule.media_query.clone(),
353                specificity: rule.specificity,
354            };
355            generator.update_rule(&selector, sorted_rule);
356        }
357    }
358
359    /// Parse CSS string into generator (simplified implementation)
360    fn parse_css_into_generator(&self, css: &str, generator: &mut CssGenerator) -> Result<()> {
361        // Simple CSS parsing - extract basic rules
362        let lines: Vec<&str> = css.lines().collect();
363        let mut i = 0;
364
365        while i < lines.len() {
366            let line = lines[i].trim();
367
368            // Look for CSS rules (simplified pattern matching)
369            if line.ends_with('{') && line.contains('.') {
370                let selector = line.replace('{', "").trim().to_string();
371
372                // Collect properties until we find the closing brace
373                let mut properties = Vec::new();
374                i += 1;
375
376                while i < lines.len() && !lines[i].trim().starts_with('}') {
377                    let prop_line = lines[i].trim();
378                    if prop_line.contains(':') && prop_line.ends_with(';') {
379                        let parts: Vec<&str> = prop_line.split(':').collect();
380                        if parts.len() == 2 {
381                            let name = parts[0].trim().to_string();
382                            let value = parts[1].trim().replace(';', "").to_string();
383                            properties.push(CssProperty {
384                                name,
385                                value,
386                                important: false,
387                            });
388                        }
389                    }
390                    i += 1;
391                }
392
393                // Add the rule to the generator
394                let rule = CssRule {
395                    selector,
396                    properties,
397                    media_query: None,
398                    specificity: 1,
399                };
400                let selector = rule.selector.clone();
401                generator.update_rule(&selector, rule);
402            }
403            i += 1;
404        }
405
406        Ok(())
407    }
408
409    /// Advanced CSS compression
410    pub fn compress_css(&self, css: &str) -> Result<String> {
411        let mut compressed = css.to_string();
412
413        // Always remove comments and optimize, regardless of advanced_compression setting
414        // Remove comments
415        compressed = self.remove_comments(&compressed);
416
417        // Remove unnecessary whitespace
418        compressed = self.remove_unnecessary_whitespace(&compressed);
419
420        // Optimize colors
421        compressed = self.optimize_colors(&compressed);
422
423        // Optimize units
424        compressed = self.optimize_units(&compressed);
425
426        Ok(compressed)
427    }
428
429    /// Remove CSS comments
430    fn remove_comments(&self, css: &str) -> String {
431        let mut result = String::new();
432        let mut chars = css.chars().peekable();
433
434        while let Some(c) = chars.next() {
435            if c == '/' && chars.peek() == Some(&'*') {
436                // Skip comment
437                chars.next(); // consume *
438                while let Some(c) = chars.next() {
439                    if c == '*' && chars.peek() == Some(&'/') {
440                        chars.next(); // consume /
441                        break;
442                    }
443                }
444            } else {
445                result.push(c);
446            }
447        }
448
449        result
450    }
451
452    /// Remove unnecessary whitespace
453    fn remove_unnecessary_whitespace(&self, css: &str) -> String {
454        css.chars()
455            .filter(|c| !c.is_whitespace() || *c == ' ')
456            .collect::<String>()
457            .replace(" {", "{")
458            .replace("{ ", "{")
459            .replace("} ", "}")
460            .replace("; ", ";")
461            .replace(": ", ":")
462            .replace(", ", ",")
463    }
464
465    /// Optimize color values
466    fn optimize_colors(&self, css: &str) -> String {
467        let mut optimized = css.to_string();
468
469        // Convert #ffffff to #fff (simplified without backreferences)
470        optimized = regex::Regex::new(
471            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])",
472        )
473        .unwrap()
474        .replace_all(&optimized, |caps: &regex::Captures| {
475            let r1 = &caps[1];
476            let g1 = &caps[2];
477            let b1 = &caps[3];
478            let r2 = &caps[4];
479            let g2 = &caps[5];
480            let b2 = &caps[6];
481
482            // Only compress if all pairs are the same
483            if r1 == r2 && g1 == g2 && b1 == b2 {
484                format!("#{}{}{}", r1, g1, b1)
485            } else {
486                caps[0].to_string()
487            }
488        })
489        .to_string();
490
491        // Convert rgb(255, 255, 255) to #ffffff (simplified without backreferences)
492        optimized = regex::Regex::new(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)")
493            .unwrap()
494            .replace_all(&optimized, |caps: &regex::Captures| {
495                let r = caps.get(1).unwrap().as_str().parse::<u8>().unwrap();
496                let g = caps.get(2).unwrap().as_str().parse::<u8>().unwrap();
497                let b = caps.get(3).unwrap().as_str().parse::<u8>().unwrap();
498                format!("#{:02x}{:02x}{:02x}", r, g, b)
499            })
500            .to_string();
501
502        optimized
503    }
504
505    /// Optimize CSS units
506    fn optimize_units(&self, css: &str) -> String {
507        let mut optimized = css.to_string();
508
509        // Convert 0px to 0
510        optimized = regex::Regex::new(r"(\d+)px")
511            .unwrap()
512            .replace_all(&optimized, "$1")
513            .to_string();
514
515        // Convert 0em to 0
516        optimized = regex::Regex::new(r"(\d+)em")
517            .unwrap()
518            .replace_all(&optimized, "$1")
519            .to_string();
520
521        optimized
522    }
523}
524
525impl Default for CssOptimizer {
526    fn default() -> Self {
527        Self::new()
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    #[test]
536    fn test_optimizer_creation() {
537        let optimizer = CssOptimizer::new();
538        assert!(optimizer.get_config().minify);
539        assert!(optimizer.get_config().merge_rules);
540    }
541
542    #[test]
543    fn test_custom_config() {
544        let config = OptimizationConfig {
545            minify: false,
546            merge_rules: false,
547            optimize_properties: false,
548            optimize_selectors: false,
549            remove_empty_rules: false,
550            remove_duplicates: false,
551            sort_properties: false,
552            advanced_compression: true,
553            compression_level: 9,
554        };
555
556        let optimizer = CssOptimizer::with_config(config);
557        assert!(!optimizer.get_config().minify);
558        assert!(optimizer.get_config().advanced_compression);
559        assert_eq!(optimizer.get_config().compression_level, 9);
560    }
561
562    #[test]
563    fn test_optimize_css() {
564        let optimizer = CssOptimizer::new();
565        let css = r#"
566            .test {
567                padding: 1rem;
568                margin: 0px;
569                color: #ffffff;
570            }
571        "#;
572
573        let result = optimizer.optimize_css(css).unwrap();
574        assert!(result.len() <= css.len());
575    }
576
577    #[test]
578    fn test_compress_css() {
579        let optimizer = CssOptimizer::new();
580        let css = r#"
581            /* This is a comment */
582            .test {
583                padding: 1rem;
584                margin: 0px;
585                color: #ffffff;
586            }
587        "#;
588
589        let compressed = optimizer.compress_css(css).unwrap();
590        assert!(!compressed.contains("/*"));
591        assert!(!compressed.contains("*/"));
592        assert!(compressed.len() < css.len());
593    }
594
595    #[test]
596    fn test_remove_comments() {
597        let optimizer = CssOptimizer::new();
598        let css = "/* comment */ .test { color: red; }";
599        let result = optimizer.remove_comments(css);
600        assert!(!result.contains("/*"));
601        assert!(!result.contains("*/"));
602    }
603
604    #[test]
605    fn test_remove_unnecessary_whitespace() {
606        let optimizer = CssOptimizer::new();
607        let css = ".test {\n  color: red;\n  margin: 0px;\n}";
608        let result = optimizer.remove_unnecessary_whitespace(css);
609        assert!(!result.contains('\n'));
610        assert!(!result.contains("  "));
611    }
612
613    #[test]
614    fn test_optimize_colors() {
615        let optimizer = CssOptimizer::new();
616        let css = "color: #ffffff; background: #000000;";
617        let result = optimizer.optimize_colors(css);
618        assert!(result.contains("#fff"));
619        assert!(result.contains("#000"));
620    }
621
622    #[test]
623    fn test_optimize_units() {
624        let optimizer = CssOptimizer::new();
625        let css = "margin: 0px; padding: 1rem;";
626        let result = optimizer.optimize_units(css);
627        assert!(result.contains("margin: 0"));
628        assert!(result.contains("padding: 1rem"));
629    }
630
631    #[test]
632    fn test_optimize_generator() {
633        let mut generator = CssGenerator::new();
634        generator.add_class("p-4").unwrap();
635        generator.add_class("bg-blue-500").unwrap();
636
637        let optimizer = CssOptimizer::new();
638        let results = optimizer.optimize(&mut generator).unwrap();
639
640        assert!(results.original_size > 0);
641        assert!(results.optimized_size > 0);
642        assert!(results.reduction_percentage >= 0.0);
643    }
644}