csskit_transform 0.0.32-canary.13e01cac8c

AST transformation and minification utilities for CSS.
Documentation
use crate::{
	ReduceCharsetRule, ReduceColors, ReduceLengths, ReduceShorthandValues, ReduceTimeUnits, ReduceUrls,
	RemoveInertNodes, RemoveOverriddenDeclarations, transformer,
};
use bitmask_enum::bitmask;
use css_ast::{CssMetadata, Visitable};

transformer!(
	/// Runtime feature flags for the CSS minifier, enabling individual transforms.
	pub enum CssMinifierFeature[CssMetadata, Visitable] {
		/// Enables the [ReduceCharsetRule] transformer.
		ReduceCharsetRule,
		/// Enables the [ReduceColors] transformer.
		ReduceColors,
		/// Enables the [ReduceLengths] transformer.
		ReduceLengths,
		/// Enables the [ReduceTimeUnits] transformer.
		ReduceTimeUnits,
		/// Enables the [ReduceUrls] transformer.
		ReduceUrls,
		/// Enables the [ReduceShorthandValues] transformer.
		ReduceShorthandValues,
		/// Enables the [RemoveOverriddenDeclarations] transformer.
		RemoveOverriddenDeclarations,
		/// Enables the [RemoveInertNodes] transformer.
		RemoveInertNodes,
	}
);

impl Default for CssMinifierFeature {
	fn default() -> Self {
		Self::none()
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::Transformer;
	use css_ast::{CssAtomSet, StyleSheet};
	use css_lexer::Lexer;
	use css_parse::{Arena, CursorCompactWriteSink, CursorOverlaySink, Parser, ToCursors};

	fn minify(source_text: &str, features: CssMinifierFeature) -> (String, bool) {
		let alloc = Arena::default();
		let mut transformer = Transformer::new_in(&alloc, features, &CssAtomSet::ATOMS, source_text);
		let lexer = Lexer::new(&CssAtomSet::ATOMS, source_text);
		let mut parser = Parser::new(&alloc, source_text, lexer);
		let mut result = parser.parse_entirely::<StyleSheet>().with_trivia();
		let mut output = String::new();
		if let Some(ref mut node) = result.output {
			transformer.transform(node);
			let overlays = transformer.overlays();
			let changed = transformer.has_changed();
			{
				let mut overlay_stream = CursorOverlaySink::new(
					source_text,
					&overlays,
					CursorCompactWriteSink::new(source_text, &mut output),
				);
				result.to_cursors(&mut overlay_stream);
			}
			(output, changed)
		} else {
			panic!("Could not transform output");
		}
	}

	#[test]
	fn test_reduce_lengths_feature() {
		let input = "body { width: 0px; }";
		let (output, changed) = minify(input, CssMinifierFeature::ReduceLengths);
		assert!(changed);
		assert!(output.contains("width:0"), "Should apply length reduction, got: {}", output);
		assert!(!output.contains("0px"), "Should not contain 0px, got: {}", output);
	}

	#[test]
	fn test_no_features() {
		let input = "body { width: 0px; }";
		let (output, changed) = minify(input, CssMinifierFeature::none());
		assert!(!changed, "Should not make changes with no features enabled");
		assert!(output.contains("width:0px"));
	}

	#[test]
	fn test_changed_flag_accuracy() {
		let input = "body { width: 10px; }";
		let (_, changed) = minify(input, CssMinifierFeature::all_bits());
		assert!(!changed, "Should report no changes when no optimizations apply");
	}

	#[test]
	fn test_keeps_significant_whitespace() {
		for input in [
			"@charset \"utf-8\";",
			":is(a) b{color:red}",
			"[x] d{color:red}",
			"* e{color:red}",
			"a:not(.x) f{color:red}",
			"a.b c{color:red}",
			".a :hover{color:red}",
			"@supports foo(.a .b){a{color:red}}",
		] {
			let (output, _) = minify(input, CssMinifierFeature::none());
			assert_eq!(output, input);
		}
	}

	#[test]
	fn test_compacts_significant_whitespace() {
		for (input, expected) in
			[(".a   .b{color:red}", ".a .b{color:red}"), ("a{--custom:.a\n\t.b}", "a{--custom:.a .b}")]
		{
			let (output, _) = minify(input, CssMinifierFeature::none());
			assert_eq!(output, expected);
		}
	}

	#[test]
	fn test_removes_trivia_whitespace() {
		for (input, expected) in [
			("a  ,  b {color: red}", "a,b{color:red}"),
			("a{color: rgb(255, 128, 0)}", "a{color:rgb(255,128,0)}"),
			("a{margin:  0   0 }", "a{margin:0 0}"),
			("a{color:red}\n\nb{color:blue}", "a{color:red}b{color:blue}"),
			("@media screen {\n\ta {\n\t\tcolor: red;\n\t}\n}", "@media screen{a{color:red}}"),
		] {
			let (output, _) = minify(input, CssMinifierFeature::none());
			assert_eq!(output, expected);
		}
	}
}