1use crate::{
2 ReduceCharsetRule, ReduceColors, ReduceLengths, ReduceShorthandValues, ReduceTimeUnits, ReduceUrls,
3 RemoveInertNodes, RemoveOverriddenDeclarations, transformer,
4};
5use bitmask_enum::bitmask;
6use css_ast::{CssMetadata, Visitable};
7
8transformer!(
9 pub enum CssMinifierFeature[CssMetadata, Visitable] {
11 ReduceCharsetRule,
13 ReduceColors,
15 ReduceLengths,
17 ReduceTimeUnits,
19 ReduceUrls,
21 ReduceShorthandValues,
23 RemoveOverriddenDeclarations,
25 RemoveInertNodes,
27 }
28);
29
30impl Default for CssMinifierFeature {
31 fn default() -> Self {
32 Self::none()
33 }
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39 use crate::Transformer;
40 use css_ast::{CssAtomSet, StyleSheet};
41 use css_lexer::Lexer;
42 use css_parse::{Arena, CursorCompactWriteSink, CursorOverlaySink, Parser, ToCursors};
43
44 fn minify(source_text: &str, features: CssMinifierFeature) -> (String, bool) {
45 let alloc = Arena::default();
46 let mut transformer = Transformer::new_in(&alloc, features, &CssAtomSet::ATOMS, source_text);
47 let lexer = Lexer::new(&CssAtomSet::ATOMS, source_text);
48 let mut parser = Parser::new(&alloc, source_text, lexer);
49 let mut result = parser.parse_entirely::<StyleSheet>().with_trivia();
50 let mut output = String::new();
51 if let Some(ref mut node) = result.output {
52 transformer.transform(node);
53 let overlays = transformer.overlays();
54 let changed = transformer.has_changed();
55 {
56 let mut overlay_stream = CursorOverlaySink::new(
57 source_text,
58 &overlays,
59 CursorCompactWriteSink::new(source_text, &mut output),
60 );
61 result.to_cursors(&mut overlay_stream);
62 }
63 (output, changed)
64 } else {
65 panic!("Could not transform output");
66 }
67 }
68
69 #[test]
70 fn test_reduce_lengths_feature() {
71 let input = "body { width: 0px; }";
72 let (output, changed) = minify(input, CssMinifierFeature::ReduceLengths);
73 assert!(changed);
74 assert!(output.contains("width:0"), "Should apply length reduction, got: {}", output);
75 assert!(!output.contains("0px"), "Should not contain 0px, got: {}", output);
76 }
77
78 #[test]
79 fn test_no_features() {
80 let input = "body { width: 0px; }";
81 let (output, changed) = minify(input, CssMinifierFeature::none());
82 assert!(!changed, "Should not make changes with no features enabled");
83 assert!(output.contains("width:0px"));
84 }
85
86 #[test]
87 fn test_changed_flag_accuracy() {
88 let input = "body { width: 10px; }";
89 let (_, changed) = minify(input, CssMinifierFeature::all_bits());
90 assert!(!changed, "Should report no changes when no optimizations apply");
91 }
92
93 #[test]
94 fn test_keeps_significant_whitespace() {
95 for input in [
96 "@charset \"utf-8\";",
97 ":is(a) b{color:red}",
98 "[x] d{color:red}",
99 "* e{color:red}",
100 "a:not(.x) f{color:red}",
101 "a.b c{color:red}",
102 ".a :hover{color:red}",
103 "@supports foo(.a .b){a{color:red}}",
104 ] {
105 let (output, _) = minify(input, CssMinifierFeature::none());
106 assert_eq!(output, input);
107 }
108 }
109
110 #[test]
111 fn test_compacts_significant_whitespace() {
112 for (input, expected) in
113 [(".a .b{color:red}", ".a .b{color:red}"), ("a{--custom:.a\n\t.b}", "a{--custom:.a .b}")]
114 {
115 let (output, _) = minify(input, CssMinifierFeature::none());
116 assert_eq!(output, expected);
117 }
118 }
119
120 #[test]
121 fn test_removes_trivia_whitespace() {
122 for (input, expected) in [
123 ("a , b {color: red}", "a,b{color:red}"),
124 ("a{color: rgb(255, 128, 0)}", "a{color:rgb(255,128,0)}"),
125 ("a{margin: 0 0 }", "a{margin:0 0}"),
126 ("a{color:red}\n\nb{color:blue}", "a{color:red}b{color:blue}"),
127 ("@media screen {\n\ta {\n\t\tcolor: red;\n\t}\n}", "@media screen{a{color:red}}"),
128 ] {
129 let (output, _) = minify(input, CssMinifierFeature::none());
130 assert_eq!(output, expected);
131 }
132 }
133}