tailwind_rs_postcss/css_optimizer/
minifier.rs

1//! CSS minifier for compression and minification
2
3use regex::Regex;
4use super::types::*;
5
6/// CSS minifier for compression and minification
7pub struct CSSMinifier {
8    minification_strategies: Vec<Box<dyn MinificationStrategy>>,
9}
10
11impl CSSMinifier {
12    /// Create new CSS minifier
13    pub fn new() -> Self {
14        Self {
15            minification_strategies: Self::build_minification_strategies(),
16        }
17    }
18    
19    /// Minify CSS
20    pub fn minify(&self, css: &str) -> Result<String, OptimizationError> {
21        let mut minified = css.to_string();
22        
23        // Apply minification strategies
24        for strategy in &self.minification_strategies {
25            minified = strategy.apply(&minified)?;
26        }
27        
28        Ok(minified)
29    }
30    
31    /// Build minification strategies
32    fn build_minification_strategies() -> Vec<Box<dyn MinificationStrategy>> {
33        vec![
34            Box::new(RemoveWhitespaceStrategy),
35            Box::new(RemoveCommentsStrategy),
36            Box::new(OptimizeColorsStrategy),
37            Box::new(RemoveSemicolonsStrategy),
38        ]
39    }
40}
41
42/// Minification strategy trait
43pub trait MinificationStrategy {
44    fn apply(&self, css: &str) -> Result<String, OptimizationError>;
45    fn name(&self) -> &str;
46}
47
48/// Remove whitespace strategy
49pub struct RemoveWhitespaceStrategy;
50
51impl MinificationStrategy for RemoveWhitespaceStrategy {
52    fn apply(&self, css: &str) -> Result<String, OptimizationError> {
53        let mut result = String::new();
54        let mut in_string = false;
55        let mut string_char = '\0';
56        
57        for ch in css.chars() {
58            match ch {
59                '"' | '\'' => {
60                    if !in_string {
61                        in_string = true;
62                        string_char = ch;
63                    } else if ch == string_char {
64                        in_string = false;
65                    }
66                    result.push(ch);
67                }
68                ' ' | '\t' | '\n' | '\r' => {
69                    if !in_string {
70                        // Only add space if necessary
71                        if !result.is_empty() && !result.ends_with(';') && !result.ends_with('{') && !result.ends_with('}') {
72                            if let Some(next_char) = css.chars().nth(result.len()) {
73                                if !matches!(next_char, ' ' | '\t' | '\n' | '\r' | ';' | '{' | '}') {
74                                    result.push(' ');
75                                }
76                            }
77                        }
78                    } else {
79                        result.push(ch);
80                    }
81                }
82                _ => result.push(ch),
83            }
84        }
85        
86        Ok(result)
87    }
88    
89    fn name(&self) -> &str {
90        "remove_whitespace"
91    }
92}
93
94/// Remove comments strategy
95pub struct RemoveCommentsStrategy;
96
97impl MinificationStrategy for RemoveCommentsStrategy {
98    fn apply(&self, css: &str) -> Result<String, OptimizationError> {
99        let mut result = String::new();
100        let mut chars = css.chars().peekable();
101        
102        while let Some(ch) = chars.next() {
103            if ch == '/' {
104                if let Some(&next_ch) = chars.peek() {
105                    if next_ch == '*' {
106                        // Skip comment
107                        chars.next(); // consume *
108                        while let Some(ch) = chars.next() {
109                            if ch == '*' {
110                                if let Some(&next_ch) = chars.peek() {
111                                    if next_ch == '/' {
112                                        chars.next(); // consume /
113                                        break;
114                                    }
115                                }
116                            }
117                        }
118                        continue;
119                    }
120                }
121            }
122            result.push(ch);
123        }
124        
125        Ok(result)
126    }
127    
128    fn name(&self) -> &str {
129        "remove_comments"
130    }
131}
132
133/// Optimize colors strategy
134pub struct OptimizeColorsStrategy;
135
136impl MinificationStrategy for OptimizeColorsStrategy {
137    fn apply(&self, css: &str) -> Result<String, OptimizationError> {
138        let mut result = css.to_string();
139        
140        // Convert #rrggbb to #rgb where possible
141        let hex_pattern = Regex::new(r"#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})").unwrap();
142        result = hex_pattern.replace_all(&result, |caps: &regex::Captures| {
143            let r = &caps[1];
144            let g = &caps[2];
145            let b = &caps[3];
146            
147            if r.chars().nth(0) == r.chars().nth(1) &&
148               g.chars().nth(0) == g.chars().nth(1) &&
149               b.chars().nth(0) == b.chars().nth(1) {
150                format!("#{}{}{}", r.chars().nth(0).unwrap(), g.chars().nth(0).unwrap(), b.chars().nth(0).unwrap())
151            } else {
152                caps[0].to_string()
153            }
154        }).to_string();
155        
156        Ok(result)
157    }
158    
159    fn name(&self) -> &str {
160        "optimize_colors"
161    }
162}
163
164/// Remove unnecessary semicolons strategy
165pub struct RemoveSemicolonsStrategy;
166
167impl MinificationStrategy for RemoveSemicolonsStrategy {
168    fn apply(&self, css: &str) -> Result<String, OptimizationError> {
169        let mut result = css.to_string();
170        
171        // Remove semicolons before closing braces
172        result = result.replace(";}", "}");
173        
174        Ok(result)
175    }
176    
177    fn name(&self) -> &str {
178        "remove_semicolons"
179    }
180}