tailwind_rs_postcss/css_optimizer/
minifier.rs1use super::types::*;
4use regex::Regex;
5
6pub struct CSSMinifier {
8 minification_strategies: Vec<Box<dyn MinificationStrategy>>,
9}
10
11impl CSSMinifier {
12 pub fn new() -> Self {
14 Self {
15 minification_strategies: Self::build_minification_strategies(),
16 }
17 }
18
19 pub fn minify(&self, css: &str) -> Result<String, OptimizationError> {
21 let mut minified = css.to_string();
22
23 for strategy in &self.minification_strategies {
25 minified = strategy.apply(&minified)?;
26 }
27
28 Ok(minified)
29 }
30
31 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
42pub trait MinificationStrategy {
44 fn apply(&self, css: &str) -> Result<String, OptimizationError>;
45 fn name(&self) -> &str;
46}
47
48pub 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 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
99pub 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 chars.next(); while let Some(ch) = chars.next() {
114 if ch == '*' {
115 if let Some(&next_ch) = chars.peek() {
116 if next_ch == '/' {
117 chars.next(); 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
138pub 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 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: ®ex::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
177pub 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 result = result.replace(";}", "}");
186
187 Ok(result)
188 }
189
190 fn name(&self) -> &str {
191 "remove_semicolons"
192 }
193}