1use crate::css_generator::{CssGenerator, CssRule, CssProperty};
7use crate::error::Result;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone)]
12pub struct OptimizationConfig {
13 pub minify: bool,
15 pub merge_rules: bool,
17 pub optimize_properties: bool,
19 pub optimize_selectors: bool,
21 pub remove_empty_rules: bool,
23 pub remove_duplicates: bool,
25 pub sort_properties: bool,
27 pub advanced_compression: bool,
29 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#[derive(Debug, Clone)]
51pub struct OptimizationResults {
52 pub original_size: usize,
54 pub optimized_size: usize,
56 pub size_reduction: usize,
58 pub reduction_percentage: f64,
60 pub original_rules: usize,
62 pub optimized_rules: usize,
64 pub original_properties: usize,
66 pub optimized_properties: usize,
68 pub stats: OptimizationStats,
70}
71
72#[derive(Debug, Clone)]
74pub struct OptimizationStats {
75 pub rules_merged: usize,
77 pub properties_optimized: usize,
79 pub selectors_optimized: usize,
81 pub empty_rules_removed: usize,
83 pub duplicate_properties_removed: usize,
85 pub processing_time_ms: u64,
87}
88
89#[derive(Debug, Clone, Default)]
91struct OptimizationTracker {
92 empty_rules_removed: usize,
93 duplicate_properties_removed: usize,
94 selectors_optimized: usize,
95}
96
97#[derive(Debug, Clone)]
99pub struct CssOptimizer {
100 config: OptimizationConfig,
101}
102
103impl CssOptimizer {
104 pub fn new() -> Self {
106 Self {
107 config: OptimizationConfig::default(),
108 }
109 }
110
111 pub fn with_config(config: OptimizationConfig) -> Self {
113 Self { config }
114 }
115
116 pub fn optimize(&self, generator: &mut CssGenerator) -> Result<OptimizationResults> {
118 let start_time = std::time::Instant::now();
119
120 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 let mut tracker = OptimizationTracker::default();
128
129 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 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 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 pub fn optimize_css(&self, css: &str) -> Result<String> {
193 let mut generator = CssGenerator::new();
194
195 self.parse_css_into_generator(css, &mut generator)?;
197
198 self.optimize(&mut generator)?;
200
201 if self.config.minify {
203 Ok(generator.generate_minified_css())
204 } else {
205 Ok(generator.generate_css())
206 }
207 }
208
209 pub fn get_config(&self) -> &OptimizationConfig {
211 &self.config
212 }
213
214 pub fn set_config(&mut self, config: OptimizationConfig) {
216 self.config = config;
217 }
218
219 fn count_properties(&self, generator: &CssGenerator) -> usize {
221 generator.get_rules().values()
222 .map(|rule| rule.properties.len())
223 .sum()
224 }
225
226 fn remove_empty_rules(&self, generator: &mut CssGenerator) -> usize {
228 let rules = generator.get_rules().clone();
229 let mut removed_count = 0;
230 for (selector, rule) in rules {
231 if rule.properties.is_empty() {
232 generator.remove_rule(&selector);
233 removed_count += 1;
234 }
235 }
236 removed_count
237 }
238
239 fn remove_duplicate_properties(&self, generator: &mut CssGenerator) -> usize {
241 let rules = generator.get_rules().clone();
242 let mut total_removed = 0;
243 for (selector, rule) in rules {
244 let mut seen_properties = std::collections::HashSet::new();
245 let mut unique_properties = Vec::new();
246
247 for property in &rule.properties {
248 if seen_properties.insert(&property.name) {
249 unique_properties.push(property.clone());
250 }
251 }
252
253 let removed_count = rule.properties.len() - unique_properties.len();
254 if removed_count > 0 {
255 total_removed += removed_count;
256 let updated_rule = CssRule {
257 selector: rule.selector.clone(),
258 properties: unique_properties,
259 media_query: rule.media_query.clone(),
260 specificity: rule.specificity,
261 };
262 generator.update_rule(&selector, updated_rule);
263 }
264 }
265 total_removed
266 }
267
268 fn optimize_properties(&self, generator: &mut CssGenerator) {
270 let rules = generator.get_rules().clone();
271 for (selector, rule) in rules {
272 let mut optimized_properties = Vec::new();
273
274 for property in &rule.properties {
275 let optimized_property = CssProperty {
276 name: property.name.clone(),
277 value: self.optimize_property_value(&property.value),
278 important: property.important,
279 };
280 optimized_properties.push(optimized_property);
281 }
282
283 let updated_rule = CssRule {
285 selector: rule.selector.clone(),
286 properties: optimized_properties,
287 media_query: rule.media_query.clone(),
288 specificity: rule.specificity,
289 };
290 generator.update_rule(&selector, updated_rule);
291 }
292 }
293
294 fn optimize_property_value(&self, value: &str) -> String {
296 let mut optimized = value.to_string();
297
298 optimized = optimized.replace("0px", "0");
300 optimized = optimized.replace("0em", "0");
301 optimized = optimized.replace("0rem", "0");
302
303 optimized = optimized.replace("0.0", "0");
305 optimized = optimized.replace("1.0", "1");
306
307 optimized
308 }
309
310 fn merge_compatible_rules(&self, generator: &mut CssGenerator) {
312 let rules = generator.get_rules().clone();
313 let mut merged_rules: HashMap<String, CssRule> = HashMap::new();
314
315 for (selector, rule) in rules {
316 if let Some(existing_rule) = merged_rules.get_mut(&selector) {
318 for property in &rule.properties {
320 if !existing_rule.properties.iter().any(|p| p.name == property.name) {
321 existing_rule.properties.push(property.clone());
322 }
323 }
324 } else {
325 merged_rules.insert(selector, rule);
326 }
327 }
328
329 for (selector, rule) in merged_rules {
331 generator.update_rule(&selector, rule);
332 }
333 }
334
335 fn sort_properties(&self, generator: &mut CssGenerator) {
337 let rules = generator.get_rules().clone();
338
339 for (selector, rule) in rules {
340 let mut sorted_properties = rule.properties.clone();
341 sorted_properties.sort_by(|a, b| a.name.cmp(&b.name));
342
343 let sorted_rule = CssRule {
344 selector: rule.selector.clone(),
345 properties: sorted_properties,
346 media_query: rule.media_query.clone(),
347 specificity: rule.specificity,
348 };
349 generator.update_rule(&selector, sorted_rule);
350 }
351 }
352
353 fn parse_css_into_generator(&self, css: &str, generator: &mut CssGenerator) -> Result<()> {
355 let lines: Vec<&str> = css.lines().collect();
357 let mut i = 0;
358
359 while i < lines.len() {
360 let line = lines[i].trim();
361
362 if line.ends_with('{') && line.contains('.') {
364 let selector = line.replace('{', "").trim().to_string();
365
366 let mut properties = Vec::new();
368 i += 1;
369
370 while i < lines.len() && !lines[i].trim().starts_with('}') {
371 let prop_line = lines[i].trim();
372 if prop_line.contains(':') && prop_line.ends_with(';') {
373 let parts: Vec<&str> = prop_line.split(':').collect();
374 if parts.len() == 2 {
375 let name = parts[0].trim().to_string();
376 let value = parts[1].trim().replace(';', "").to_string();
377 properties.push(CssProperty {
378 name,
379 value,
380 important: false,
381 });
382 }
383 }
384 i += 1;
385 }
386
387 let rule = CssRule {
389 selector,
390 properties,
391 media_query: None,
392 specificity: 1,
393 };
394 let selector = rule.selector.clone();
395 generator.update_rule(&selector, rule);
396 }
397 i += 1;
398 }
399
400 Ok(())
401 }
402
403 pub fn compress_css(&self, css: &str) -> Result<String> {
405 let mut compressed = css.to_string();
406
407 compressed = self.remove_comments(&compressed);
410
411 compressed = self.remove_unnecessary_whitespace(&compressed);
413
414 compressed = self.optimize_colors(&compressed);
416
417 compressed = self.optimize_units(&compressed);
419
420 Ok(compressed)
421 }
422
423
424 fn remove_comments(&self, css: &str) -> String {
426 let mut result = String::new();
427 let mut chars = css.chars().peekable();
428
429 while let Some(c) = chars.next() {
430 if c == '/' && chars.peek() == Some(&'*') {
431 chars.next(); while let Some(c) = chars.next() {
434 if c == '*' && chars.peek() == Some(&'/') {
435 chars.next(); break;
437 }
438 }
439 } else {
440 result.push(c);
441 }
442 }
443
444 result
445 }
446
447 fn remove_unnecessary_whitespace(&self, css: &str) -> String {
449 css.chars()
450 .filter(|c| !c.is_whitespace() || *c == ' ')
451 .collect::<String>()
452 .replace(" {", "{")
453 .replace("{ ", "{")
454 .replace("} ", "}")
455 .replace("; ", ";")
456 .replace(": ", ":")
457 .replace(", ", ",")
458 }
459
460 fn optimize_colors(&self, css: &str) -> String {
462 let mut optimized = css.to_string();
463
464 optimized = regex::Regex::new(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])")
466 .unwrap()
467 .replace_all(&optimized, |caps: ®ex::Captures| {
468 let r1 = &caps[1];
469 let g1 = &caps[2];
470 let b1 = &caps[3];
471 let r2 = &caps[4];
472 let g2 = &caps[5];
473 let b2 = &caps[6];
474
475 if r1 == r2 && g1 == g2 && b1 == b2 {
477 format!("#{}{}{}", r1, g1, b1)
478 } else {
479 caps[0].to_string()
480 }
481 })
482 .to_string();
483
484 optimized = regex::Regex::new(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)")
486 .unwrap()
487 .replace_all(&optimized, |caps: ®ex::Captures| {
488 let r = caps.get(1).unwrap().as_str().parse::<u8>().unwrap();
489 let g = caps.get(2).unwrap().as_str().parse::<u8>().unwrap();
490 let b = caps.get(3).unwrap().as_str().parse::<u8>().unwrap();
491 format!("#{:02x}{:02x}{:02x}", r, g, b)
492 })
493 .to_string();
494
495 optimized
496 }
497
498 fn optimize_units(&self, css: &str) -> String {
500 let mut optimized = css.to_string();
501
502 optimized = regex::Regex::new(r"(\d+)px")
504 .unwrap()
505 .replace_all(&optimized, "$1")
506 .to_string();
507
508 optimized = regex::Regex::new(r"(\d+)em")
510 .unwrap()
511 .replace_all(&optimized, "$1")
512 .to_string();
513
514 optimized
515 }
516}
517
518impl Default for CssOptimizer {
519 fn default() -> Self {
520 Self::new()
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
529 fn test_optimizer_creation() {
530 let optimizer = CssOptimizer::new();
531 assert!(optimizer.get_config().minify);
532 assert!(optimizer.get_config().merge_rules);
533 }
534
535 #[test]
536 fn test_custom_config() {
537 let config = OptimizationConfig {
538 minify: false,
539 merge_rules: false,
540 optimize_properties: false,
541 optimize_selectors: false,
542 remove_empty_rules: false,
543 remove_duplicates: false,
544 sort_properties: false,
545 advanced_compression: true,
546 compression_level: 9,
547 };
548
549 let optimizer = CssOptimizer::with_config(config);
550 assert!(!optimizer.get_config().minify);
551 assert!(optimizer.get_config().advanced_compression);
552 assert_eq!(optimizer.get_config().compression_level, 9);
553 }
554
555 #[test]
556 fn test_optimize_css() {
557 let optimizer = CssOptimizer::new();
558 let css = r#"
559 .test {
560 padding: 1rem;
561 margin: 0px;
562 color: #ffffff;
563 }
564 "#;
565
566 let result = optimizer.optimize_css(css).unwrap();
567 assert!(result.len() <= css.len());
568 }
569
570 #[test]
571 fn test_compress_css() {
572 let optimizer = CssOptimizer::new();
573 let css = r#"
574 /* This is a comment */
575 .test {
576 padding: 1rem;
577 margin: 0px;
578 color: #ffffff;
579 }
580 "#;
581
582 let compressed = optimizer.compress_css(css).unwrap();
583 assert!(!compressed.contains("/*"));
584 assert!(!compressed.contains("*/"));
585 assert!(compressed.len() < css.len());
586 }
587
588 #[test]
589 fn test_remove_comments() {
590 let optimizer = CssOptimizer::new();
591 let css = "/* comment */ .test { color: red; }";
592 let result = optimizer.remove_comments(css);
593 assert!(!result.contains("/*"));
594 assert!(!result.contains("*/"));
595 }
596
597 #[test]
598 fn test_remove_unnecessary_whitespace() {
599 let optimizer = CssOptimizer::new();
600 let css = ".test {\n color: red;\n margin: 0px;\n}";
601 let result = optimizer.remove_unnecessary_whitespace(css);
602 assert!(!result.contains('\n'));
603 assert!(!result.contains(" "));
604 }
605
606 #[test]
607 fn test_optimize_colors() {
608 let optimizer = CssOptimizer::new();
609 let css = "color: #ffffff; background: #000000;";
610 let result = optimizer.optimize_colors(css);
611 assert!(result.contains("#fff"));
612 assert!(result.contains("#000"));
613 }
614
615 #[test]
616 fn test_optimize_units() {
617 let optimizer = CssOptimizer::new();
618 let css = "margin: 0px; padding: 1rem;";
619 let result = optimizer.optimize_units(css);
620 assert!(result.contains("margin: 0"));
621 assert!(result.contains("padding: 1rem"));
622 }
623
624 #[test]
625 fn test_optimize_generator() {
626 let mut generator = CssGenerator::new();
627 generator.add_class("p-4").unwrap();
628 generator.add_class("bg-blue-500").unwrap();
629
630 let optimizer = CssOptimizer::new();
631 let results = optimizer.optimize(&mut generator).unwrap();
632
633 assert!(results.original_size > 0);
634 assert!(results.optimized_size > 0);
635 assert!(results.reduction_percentage >= 0.0);
636 }
637}