tailwind_rs_postcss/css_optimizer/
minifier.rs

1//! CSS minifier for compression and minification
2
3use super::types::*;
4use regex::Regex;
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()
72                            && !result.ends_with(';')
73                            && !result.ends_with('{')
74                            && !result.ends_with('}')
75                        {
76                            if let Some(next_char) = css.chars().nth(result.len()) {
77                                if !matches!(next_char, ' ' | '\t' | '\n' | '\r' | ';' | '{' | '}')
78                                {
79                                    result.push(' ');
80                                }
81                            }
82                        }
83                    } else {
84                        result.push(ch);
85                    }
86                }
87                _ => result.push(ch),
88            }
89        }
90
91        Ok(result)
92    }
93
94    fn name(&self) -> &str {
95        "remove_whitespace"
96    }
97}
98
99/// Remove comments strategy
100pub struct RemoveCommentsStrategy;
101
102impl MinificationStrategy for RemoveCommentsStrategy {
103    fn apply(&self, css: &str) -> Result<String, OptimizationError> {
104        let mut result = String::new();
105        let mut chars = css.chars().peekable();
106
107        while let Some(ch) = chars.next() {
108            if ch == '/' {
109                if let Some(&next_ch) = chars.peek() {
110                    if next_ch == '*' {
111                        // Skip comment
112                        chars.next(); // consume *
113                        while let Some(ch) = chars.next() {
114                            if ch == '*' {
115                                if let Some(&next_ch) = chars.peek() {
116                                    if next_ch == '/' {
117                                        chars.next(); // consume /
118                                        break;
119                                    }
120                                }
121                            }
122                        }
123                        continue;
124                    }
125                }
126            }
127            result.push(ch);
128        }
129
130        Ok(result)
131    }
132
133    fn name(&self) -> &str {
134        "remove_comments"
135    }
136}
137
138/// Optimize colors strategy
139pub struct OptimizeColorsStrategy;
140
141impl MinificationStrategy for OptimizeColorsStrategy {
142    fn apply(&self, css: &str) -> Result<String, OptimizationError> {
143        let mut result = css.to_string();
144
145        // Convert #rrggbb to #rgb where possible
146        let hex_pattern = Regex::new(r"#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})").unwrap();
147        result = hex_pattern
148            .replace_all(&result, |caps: &regex::Captures| {
149                let r = &caps[1];
150                let g = &caps[2];
151                let b = &caps[3];
152
153                if r.chars().nth(0) == r.chars().nth(1)
154                    && g.chars().nth(0) == g.chars().nth(1)
155                    && b.chars().nth(0) == b.chars().nth(1)
156                {
157                    format!(
158                        "#{}{}{}",
159                        r.chars().nth(0).unwrap(),
160                        g.chars().nth(0).unwrap(),
161                        b.chars().nth(0).unwrap()
162                    )
163                } else {
164                    caps[0].to_string()
165                }
166            })
167            .to_string();
168
169        Ok(result)
170    }
171
172    fn name(&self) -> &str {
173        "optimize_colors"
174    }
175}
176
177/// Remove unnecessary semicolons strategy
178pub struct RemoveSemicolonsStrategy;
179
180impl MinificationStrategy for RemoveSemicolonsStrategy {
181    fn apply(&self, css: &str) -> Result<String, OptimizationError> {
182        let mut result = css.to_string();
183
184        // Remove semicolons before closing braces
185        result = result.replace(";}", "}");
186
187        Ok(result)
188    }
189
190    fn name(&self) -> &str {
191        "remove_semicolons"
192    }
193}