Skip to main content

csskit_transform/
css_minifier.rs

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	/// Runtime feature flags for the CSS minifier, enabling individual transforms.
10	pub enum CssMinifierFeature[CssMetadata, Visitable] {
11		/// Enables the [ReduceCharsetRule] transformer.
12		ReduceCharsetRule,
13		/// Enables the [ReduceColors] transformer.
14		ReduceColors,
15		/// Enables the [ReduceLengths] transformer.
16		ReduceLengths,
17		/// Enables the [ReduceTimeUnits] transformer.
18		ReduceTimeUnits,
19		/// Enables the [ReduceUrls] transformer.
20		ReduceUrls,
21		/// Enables the [ReduceShorthandValues] transformer.
22		ReduceShorthandValues,
23		/// Enables the [RemoveOverriddenDeclarations] transformer.
24		RemoveOverriddenDeclarations,
25		/// Enables the [RemoveInertNodes] transformer.
26		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}