csskit_transform/
reduce_lengths.rs1use crate::prelude::*;
2use css_ast::{DeclarationValue, Length, QueryableNode, UnitlessZeroResolves, Visitable};
3use css_parse::Declaration;
4use std::cell::Cell;
5
6pub struct ReduceLengths<'a, 'ctx, N: Visitable + NodeWithMetadata<CssMetadata>> {
7 pub transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>,
8 unitless_zero_resolves: Cell<UnitlessZeroResolves>,
11}
12
13impl<'a, 'ctx, N> Transform<'a, 'ctx, CssMetadata, N, CssMinifierFeature> for ReduceLengths<'a, 'ctx, N>
14where
15 N: Visitable + NodeWithMetadata<CssMetadata>,
16{
17 fn may_change(features: CssMinifierFeature, _node: &N) -> bool {
18 features.contains(CssMinifierFeature::ReduceLengths)
19 }
20
21 fn new(transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>) -> Self {
22 Self { transformer, unitless_zero_resolves: Cell::new(UnitlessZeroResolves::Length) }
23 }
24}
25
26impl<'a, 'ctx, N> Visit for ReduceLengths<'a, 'ctx, N>
27where
28 N: Visitable + NodeWithMetadata<CssMetadata>,
29{
30 fn visit_declaration<'b, T: DeclarationValue<'b, CssMetadata> + QueryableNode>(
31 &mut self,
32 decl: &Declaration<'b, T, CssMetadata>,
33 ) {
34 self.unitless_zero_resolves.set(decl.metadata().unitless_zero_resolves);
35 }
36
37 fn exit_declaration<'b, T: DeclarationValue<'b, CssMetadata> + QueryableNode>(
38 &mut self,
39 _decl: &Declaration<'b, T, CssMetadata>,
40 ) {
41 self.unitless_zero_resolves.set(UnitlessZeroResolves::Length);
42 }
43
44 fn visit_length(&mut self, length: &Length) {
45 enum ResolvedType {
46 UnitlessZero,
47 UnitedZero,
48 Resolved(f32),
49 Unresolved,
50 }
51
52 let resolved = match length {
53 Length::Zero(_) => ResolvedType::UnitlessZero,
54 _ if Into::<f32>::into(*length) == 0.0 => ResolvedType::UnitedZero,
55 _ => {
56 if let Some(px) = length.to_px() {
57 ResolvedType::Resolved(px)
58 } else {
59 ResolvedType::Unresolved
60 }
61 }
62 };
63
64 let can_reduce_to_unitless = self.unitless_zero_resolves.get() == UnitlessZeroResolves::Length;
65
66 if can_reduce_to_unitless && matches!(resolved, ResolvedType::UnitedZero | ResolvedType::Resolved(0.0)) {
67 self.transformer.replace(length, self.transformer.parse_value::<Length>("0"));
68 } else if let ResolvedType::Resolved(px) = resolved {
69 let replacement = bumpalo::format!(in self.transformer.bump(), "{}px", px);
70 let original_span = length.to_span();
71 let original_len = (original_span.end().0 - original_span.start().0) as usize;
72 if replacement.len() <= original_len {
73 self.transformer.replace(length, self.transformer.parse_value::<Length>(replacement.into_bump_str()));
74 }
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use crate::test_helpers::{assert_no_transform, assert_transform};
82 use css_ast::{CssAtomSet, StyleSheet};
83
84 #[test]
85 fn test_reduce_zero_lengths() {
86 assert_transform!(
87 CssMinifierFeature::ReduceLengths,
88 CssAtomSet,
89 StyleSheet,
90 "body { width: 0px; height: 0rem; margin: 0em; }",
91 "body { width: 0; height: 0; margin: 0; }"
92 );
93 }
94
95 #[test]
96 fn test_length_shortening_guard() {
97 assert_transform!(
98 CssMinifierFeature::ReduceLengths,
99 CssAtomSet,
100 StyleSheet,
101 "div { font-size: 12pt; }",
102 "div { font-size: 16px; }"
103 );
104 }
105
106 #[test]
107 fn test_length_noop() {
108 assert_no_transform!(CssMinifierFeature::ReduceLengths, CssAtomSet, StyleSheet, "body { width: 10rem; }");
109 }
110
111 #[test]
112 fn test_unitless_zero_resolves_to_number() {
113 assert_no_transform!(CssMinifierFeature::ReduceLengths, CssAtomSet, StyleSheet, "body { line-height: 0px; }");
115 assert_no_transform!(CssMinifierFeature::ReduceLengths, CssAtomSet, StyleSheet, "body { tab-size: 0px; }");
117 assert_no_transform!(
119 CssMinifierFeature::ReduceLengths,
120 CssAtomSet,
121 StyleSheet,
122 "body { width: calc(100px - 0px); }"
123 );
124 }
125
126 #[test]
127 fn test_unitless_zero_resolves_to_length() {
128 assert_transform!(
129 CssMinifierFeature::ReduceLengths,
130 CssAtomSet,
131 StyleSheet,
132 "div { width: 0px; }",
133 "div { width: 0; }"
134 );
135 }
136}