tailwind_rs_postcss/css_optimizer/
minifier.rs1use regex::Regex;
4use super::types::*;
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() && !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
94pub 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 chars.next(); while let Some(ch) = chars.next() {
109 if ch == '*' {
110 if let Some(&next_ch) = chars.peek() {
111 if next_ch == '/' {
112 chars.next(); 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
133pub 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 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: ®ex::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
164pub 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 result = result.replace(";}", "}");
173
174 Ok(result)
175 }
176
177 fn name(&self) -> &str {
178 "remove_semicolons"
179 }
180}