1use crate::css_generator::{CssGenerator, CssProperty, CssRule};
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
222 .get_rules()
223 .values()
224 .map(|rule| rule.properties.len())
225 .sum()
226 }
227
228 fn remove_empty_rules(&self, generator: &mut CssGenerator) -> usize {
230 let rules = generator.get_rules().clone();
231 let mut removed_count = 0;
232 for (selector, rule) in rules {
233 if rule.properties.is_empty() {
234 generator.remove_rule(&selector);
235 removed_count += 1;
236 }
237 }
238 removed_count
239 }
240
241 fn remove_duplicate_properties(&self, generator: &mut CssGenerator) -> usize {
243 let rules = generator.get_rules().clone();
244 let mut total_removed = 0;
245 for (selector, rule) in rules {
246 let mut seen_properties = std::collections::HashSet::new();
247 let mut unique_properties = Vec::new();
248
249 for property in &rule.properties {
250 if seen_properties.insert(&property.name) {
251 unique_properties.push(property.clone());
252 }
253 }
254
255 let removed_count = rule.properties.len() - unique_properties.len();
256 if removed_count > 0 {
257 total_removed += removed_count;
258 let updated_rule = CssRule {
259 selector: rule.selector.clone(),
260 properties: unique_properties,
261 media_query: rule.media_query.clone(),
262 specificity: rule.specificity,
263 };
264 generator.update_rule(&selector, updated_rule);
265 }
266 }
267 total_removed
268 }
269
270 fn optimize_properties(&self, generator: &mut CssGenerator) {
272 let rules = generator.get_rules().clone();
273 for (selector, rule) in rules {
274 let mut optimized_properties = Vec::new();
275
276 for property in &rule.properties {
277 let optimized_property = CssProperty {
278 name: property.name.clone(),
279 value: self.optimize_property_value(&property.value),
280 important: property.important,
281 };
282 optimized_properties.push(optimized_property);
283 }
284
285 let updated_rule = CssRule {
287 selector: rule.selector.clone(),
288 properties: optimized_properties,
289 media_query: rule.media_query.clone(),
290 specificity: rule.specificity,
291 };
292 generator.update_rule(&selector, updated_rule);
293 }
294 }
295
296 fn optimize_property_value(&self, value: &str) -> String {
298 let mut optimized = value.to_string();
299
300 optimized = optimized.replace("0px", "0");
302 optimized = optimized.replace("0em", "0");
303 optimized = optimized.replace("0rem", "0");
304
305 optimized = optimized.replace("0.0", "0");
307 optimized = optimized.replace("1.0", "1");
308
309 optimized
310 }
311
312 fn merge_compatible_rules(&self, generator: &mut CssGenerator) {
314 let rules = generator.get_rules().clone();
315 let mut merged_rules: HashMap<String, CssRule> = HashMap::new();
316
317 for (selector, rule) in rules {
318 if let Some(existing_rule) = merged_rules.get_mut(&selector) {
320 for property in &rule.properties {
322 if !existing_rule
323 .properties
324 .iter()
325 .any(|p| p.name == property.name)
326 {
327 existing_rule.properties.push(property.clone());
328 }
329 }
330 } else {
331 merged_rules.insert(selector, rule);
332 }
333 }
334
335 for (selector, rule) in merged_rules {
337 generator.update_rule(&selector, rule);
338 }
339 }
340
341 fn sort_properties(&self, generator: &mut CssGenerator) {
343 let rules = generator.get_rules().clone();
344
345 for (selector, rule) in rules {
346 let mut sorted_properties = rule.properties.clone();
347 sorted_properties.sort_by(|a, b| a.name.cmp(&b.name));
348
349 let sorted_rule = CssRule {
350 selector: rule.selector.clone(),
351 properties: sorted_properties,
352 media_query: rule.media_query.clone(),
353 specificity: rule.specificity,
354 };
355 generator.update_rule(&selector, sorted_rule);
356 }
357 }
358
359 fn parse_css_into_generator(&self, css: &str, generator: &mut CssGenerator) -> Result<()> {
361 let lines: Vec<&str> = css.lines().collect();
363 let mut i = 0;
364
365 while i < lines.len() {
366 let line = lines[i].trim();
367
368 if line.ends_with('{') && line.contains('.') {
370 let selector = line.replace('{', "").trim().to_string();
371
372 let mut properties = Vec::new();
374 i += 1;
375
376 while i < lines.len() && !lines[i].trim().starts_with('}') {
377 let prop_line = lines[i].trim();
378 if prop_line.contains(':') && prop_line.ends_with(';') {
379 let parts: Vec<&str> = prop_line.split(':').collect();
380 if parts.len() == 2 {
381 let name = parts[0].trim().to_string();
382 let value = parts[1].trim().replace(';', "").to_string();
383 properties.push(CssProperty {
384 name,
385 value,
386 important: false,
387 });
388 }
389 }
390 i += 1;
391 }
392
393 let rule = CssRule {
395 selector,
396 properties,
397 media_query: None,
398 specificity: 1,
399 };
400 let selector = rule.selector.clone();
401 generator.update_rule(&selector, rule);
402 }
403 i += 1;
404 }
405
406 Ok(())
407 }
408
409 pub fn compress_css(&self, css: &str) -> Result<String> {
411 let mut compressed = css.to_string();
412
413 compressed = self.remove_comments(&compressed);
416
417 compressed = self.remove_unnecessary_whitespace(&compressed);
419
420 compressed = self.optimize_colors(&compressed);
422
423 compressed = self.optimize_units(&compressed);
425
426 Ok(compressed)
427 }
428
429 fn remove_comments(&self, css: &str) -> String {
431 let mut result = String::new();
432 let mut chars = css.chars().peekable();
433
434 while let Some(c) = chars.next() {
435 if c == '/' && chars.peek() == Some(&'*') {
436 chars.next(); while let Some(c) = chars.next() {
439 if c == '*' && chars.peek() == Some(&'/') {
440 chars.next(); break;
442 }
443 }
444 } else {
445 result.push(c);
446 }
447 }
448
449 result
450 }
451
452 fn remove_unnecessary_whitespace(&self, css: &str) -> String {
454 css.chars()
455 .filter(|c| !c.is_whitespace() || *c == ' ')
456 .collect::<String>()
457 .replace(" {", "{")
458 .replace("{ ", "{")
459 .replace("} ", "}")
460 .replace("; ", ";")
461 .replace(": ", ":")
462 .replace(", ", ",")
463 }
464
465 fn optimize_colors(&self, css: &str) -> String {
467 let mut optimized = css.to_string();
468
469 optimized = regex::Regex::new(
471 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])",
472 )
473 .unwrap()
474 .replace_all(&optimized, |caps: ®ex::Captures| {
475 let r1 = &caps[1];
476 let g1 = &caps[2];
477 let b1 = &caps[3];
478 let r2 = &caps[4];
479 let g2 = &caps[5];
480 let b2 = &caps[6];
481
482 if r1 == r2 && g1 == g2 && b1 == b2 {
484 format!("#{}{}{}", r1, g1, b1)
485 } else {
486 caps[0].to_string()
487 }
488 })
489 .to_string();
490
491 optimized = regex::Regex::new(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)")
493 .unwrap()
494 .replace_all(&optimized, |caps: ®ex::Captures| {
495 let r = caps.get(1).unwrap().as_str().parse::<u8>().unwrap();
496 let g = caps.get(2).unwrap().as_str().parse::<u8>().unwrap();
497 let b = caps.get(3).unwrap().as_str().parse::<u8>().unwrap();
498 format!("#{:02x}{:02x}{:02x}", r, g, b)
499 })
500 .to_string();
501
502 optimized
503 }
504
505 fn optimize_units(&self, css: &str) -> String {
507 let mut optimized = css.to_string();
508
509 optimized = regex::Regex::new(r"(\d+)px")
511 .unwrap()
512 .replace_all(&optimized, "$1")
513 .to_string();
514
515 optimized = regex::Regex::new(r"(\d+)em")
517 .unwrap()
518 .replace_all(&optimized, "$1")
519 .to_string();
520
521 optimized
522 }
523}
524
525impl Default for CssOptimizer {
526 fn default() -> Self {
527 Self::new()
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn test_optimizer_creation() {
537 let optimizer = CssOptimizer::new();
538 assert!(optimizer.get_config().minify);
539 assert!(optimizer.get_config().merge_rules);
540 }
541
542 #[test]
543 fn test_custom_config() {
544 let config = OptimizationConfig {
545 minify: false,
546 merge_rules: false,
547 optimize_properties: false,
548 optimize_selectors: false,
549 remove_empty_rules: false,
550 remove_duplicates: false,
551 sort_properties: false,
552 advanced_compression: true,
553 compression_level: 9,
554 };
555
556 let optimizer = CssOptimizer::with_config(config);
557 assert!(!optimizer.get_config().minify);
558 assert!(optimizer.get_config().advanced_compression);
559 assert_eq!(optimizer.get_config().compression_level, 9);
560 }
561
562 #[test]
563 fn test_optimize_css() {
564 let optimizer = CssOptimizer::new();
565 let css = r#"
566 .test {
567 padding: 1rem;
568 margin: 0px;
569 color: #ffffff;
570 }
571 "#;
572
573 let result = optimizer.optimize_css(css).unwrap();
574 assert!(result.len() <= css.len());
575 }
576
577 #[test]
578 fn test_compress_css() {
579 let optimizer = CssOptimizer::new();
580 let css = r#"
581 /* This is a comment */
582 .test {
583 padding: 1rem;
584 margin: 0px;
585 color: #ffffff;
586 }
587 "#;
588
589 let compressed = optimizer.compress_css(css).unwrap();
590 assert!(!compressed.contains("/*"));
591 assert!(!compressed.contains("*/"));
592 assert!(compressed.len() < css.len());
593 }
594
595 #[test]
596 fn test_remove_comments() {
597 let optimizer = CssOptimizer::new();
598 let css = "/* comment */ .test { color: red; }";
599 let result = optimizer.remove_comments(css);
600 assert!(!result.contains("/*"));
601 assert!(!result.contains("*/"));
602 }
603
604 #[test]
605 fn test_remove_unnecessary_whitespace() {
606 let optimizer = CssOptimizer::new();
607 let css = ".test {\n color: red;\n margin: 0px;\n}";
608 let result = optimizer.remove_unnecessary_whitespace(css);
609 assert!(!result.contains('\n'));
610 assert!(!result.contains(" "));
611 }
612
613 #[test]
614 fn test_optimize_colors() {
615 let optimizer = CssOptimizer::new();
616 let css = "color: #ffffff; background: #000000;";
617 let result = optimizer.optimize_colors(css);
618 assert!(result.contains("#fff"));
619 assert!(result.contains("#000"));
620 }
621
622 #[test]
623 fn test_optimize_units() {
624 let optimizer = CssOptimizer::new();
625 let css = "margin: 0px; padding: 1rem;";
626 let result = optimizer.optimize_units(css);
627 assert!(result.contains("margin: 0"));
628 assert!(result.contains("padding: 1rem"));
629 }
630
631 #[test]
632 fn test_optimize_generator() {
633 let mut generator = CssGenerator::new();
634 generator.add_class("p-4").unwrap();
635 generator.add_class("bg-blue-500").unwrap();
636
637 let optimizer = CssOptimizer::new();
638 let results = optimizer.optimize(&mut generator).unwrap();
639
640 assert!(results.original_size > 0);
641 assert!(results.optimized_size > 0);
642 assert!(results.reduction_percentage >= 0.0);
643 }
644}