csskit_transform/
reduce_colors.rs

1use crate::prelude::*;
2use chromashift::{Hex, Named, Srgb};
3use css_ast::{Color, ToChromashift, Visitable};
4
5pub struct ReduceColors<'a, 'ctx, N: Visitable + NodeWithMetadata<CssMetadata>> {
6	pub transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>,
7}
8
9impl<'a, 'ctx, N> Transform<'a, 'ctx, CssMetadata, N, CssMinifierFeature> for ReduceColors<'a, 'ctx, N>
10where
11	N: Visitable + NodeWithMetadata<CssMetadata>,
12{
13	fn may_change(features: CssMinifierFeature, _node: &N) -> bool {
14		features.contains(CssMinifierFeature::ReduceColors)
15	}
16
17	fn new(transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>) -> Self {
18		Self { transformer }
19	}
20}
21
22impl<'a, 'ctx, N> Visit for ReduceColors<'a, 'ctx, N>
23where
24	N: Visitable + NodeWithMetadata<CssMetadata>,
25{
26	fn visit_color(&mut self, color: &Color) {
27		let Some(chroma_color) = color.to_chromashift() else {
28			return;
29		};
30		let len = color.to_span().len() as usize;
31
32		let srgb = Srgb::from(chroma_color);
33		let Some(candidate) = [
34			Some(Hex::from(srgb).to_string()),
35			Named::try_from(chroma_color).ok().map(|named| named.to_string()),
36			Some(srgb.to_string()),
37		]
38		.into_iter()
39		.flatten()
40		.min_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b))) else {
41			return;
42		};
43
44		if candidate.len() < len {
45			self.transformer.replace_parsed::<Color>(color.to_span(), &candidate);
46		}
47	}
48}
49
50#[cfg(test)]
51mod tests {
52	use crate::test_helpers::{assert_no_transform, assert_transform};
53	use css_ast::{CssAtomSet, StyleSheet};
54
55	#[test]
56	fn reduces_full_length_hex() {
57		assert_transform!(
58			CssMinifierFeature::ReduceColors,
59			CssAtomSet,
60			StyleSheet,
61			"body { color: #ffffff; }",
62			"body { color: #fff; }"
63		);
64	}
65
66	#[test]
67	fn prefers_shorthand_hex_over_keyword() {
68		assert_transform!(
69			CssMinifierFeature::ReduceColors,
70			CssAtomSet,
71			StyleSheet,
72			"body { color: #000000; }",
73			"body { color: #000; }"
74		);
75	}
76
77	#[test]
78	fn prefers_named_over_rgb() {
79		assert_transform!(
80			CssMinifierFeature::ReduceColors,
81			CssAtomSet,
82			StyleSheet,
83			"body { color: rgb(210, 180, 140); }",
84			"body { color: tan; }"
85		);
86	}
87
88	#[test]
89	fn shortens_alpha_hex() {
90		assert_transform!(
91			CssMinifierFeature::ReduceColors,
92			CssAtomSet,
93			StyleSheet,
94			"body { color: rgba(255, 0, 0, 0.5); }",
95			"body { color: #ff000080; }"
96		);
97	}
98
99	#[test]
100	fn no_transform_when_already_short() {
101		assert_no_transform!(CssMinifierFeature::ReduceColors, CssAtomSet, StyleSheet, "body { color: red; }");
102	}
103
104	#[test]
105	fn no_transform_for_currentcolor() {
106		assert_no_transform!(CssMinifierFeature::ReduceColors, CssAtomSet, StyleSheet, "body { color: currentcolor; }");
107	}
108}