use crate::{
ReduceCharsetRule, ReduceColors, ReduceLengths, ReduceShorthandValues, ReduceTimeUnits, ReduceUrls,
RemoveInertNodes, RemoveOverriddenDeclarations, transformer,
};
use bitmask_enum::bitmask;
use css_ast::{CssMetadata, Visitable};
transformer!(
pub enum CssMinifierFeature[CssMetadata, Visitable] {
ReduceCharsetRule,
ReduceColors,
ReduceLengths,
ReduceTimeUnits,
ReduceUrls,
ReduceShorthandValues,
RemoveOverriddenDeclarations,
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);
}
}
}