csskit_transform/
reduce_colors.rs1use 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}