1use crate::prelude::*;
2use css_ast::{CssTypes, DeclarationValue, Length, MathFunction, UnitlessZeroResolves, VisitNode, Visitable};
3use css_parse::{Declaration, format_in};
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 math_depth: Cell<u32>,
15}
16
17impl<'a, 'ctx, N> Transform<'a, 'ctx, CssMetadata, N, CssMinifierFeature> for ReduceLengths<'a, 'ctx, N>
18where
19 N: Visitable + NodeWithMetadata<CssMetadata>,
20{
21 fn skips_subtree(metadata: &CssMetadata) -> bool {
22 !metadata.has_value_kinds(CssTypes::Length)
23 }
24
25 fn new(transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>) -> Self {
26 Self { transformer, unitless_zero_resolves: Cell::new(UnitlessZeroResolves::Length), math_depth: Cell::new(0) }
27 }
28}
29
30#[visitor]
31impl<'a, 'ctx, N> Visit for ReduceLengths<'a, 'ctx, N>
32where
33 N: Visitable + NodeWithMetadata<CssMetadata>,
34{
35 fn consider_node(&self, node: VisitNode) -> VisitFlow {
36 if node.node_id.is_some() && Self::skips_subtree(&node.subtree_metadata()) {
37 return VisitFlow::SKIP_CHILDREN;
38 }
39 VisitFlow::DESCEND
40 }
41
42 fn enter_declaration<'b, T: DeclarationValue<'b, CssMetadata>>(
43 &mut self,
44 decl: &Declaration<'b, T, CssMetadata>,
45 _query: VisitNode,
46 ) {
47 self.unitless_zero_resolves.set(decl.metadata().unitless_zero_resolves);
48 }
49
50 fn exit_declaration<'b, T: DeclarationValue<'b, CssMetadata>>(
51 &mut self,
52 _decl: &Declaration<'b, T, CssMetadata>,
53 _query: VisitNode,
54 ) {
55 self.unitless_zero_resolves.set(UnitlessZeroResolves::Length);
56 }
57
58 fn visit_math_function<'b, T>(&mut self, _math: &MathFunction<'b, T>) {
59 self.math_depth.set(self.math_depth.get() + 1);
60 }
61
62 fn exit_math_function<'b, T>(&mut self, _math: &MathFunction<'b, T>) {
63 self.math_depth.set(self.math_depth.get().saturating_sub(1));
64 }
65
66 fn visit_length(&mut self, length: &Length) {
67 if self.math_depth.get() > 0 {
69 return;
70 }
71
72 enum ResolvedType {
73 UnitlessZero,
74 UnitedZero,
75 Resolved(f32),
76 Unresolved,
77 }
78
79 let resolved = match length {
80 Length::Zero(_) => ResolvedType::UnitlessZero,
81 _ if Into::<f32>::into(*length) == 0.0 => ResolvedType::UnitedZero,
82 _ => {
83 if let Some(px) = length.to_px() {
84 ResolvedType::Resolved(px)
85 } else {
86 ResolvedType::Unresolved
87 }
88 }
89 };
90
91 let can_reduce_to_unitless = self.unitless_zero_resolves.get() == UnitlessZeroResolves::Length;
92
93 if can_reduce_to_unitless && matches!(resolved, ResolvedType::UnitedZero | ResolvedType::Resolved(0.0)) {
94 self.transformer.replace(length, self.transformer.parse_value::<Length>("0"));
95 } else if let ResolvedType::Resolved(px) = resolved {
96 let replacement = format_in!(in self.transformer.alloc(), "{}px", px);
97 let original_span = length.to_span();
98 let original_len = (original_span.end().0 - original_span.start().0) as usize;
99 if replacement.len() <= original_len {
100 self.transformer.replace(length, self.transformer.parse_value::<Length>(replacement.into_str()));
101 }
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use crate::test_helpers::{assert_no_transform, assert_transform};
109 use css_ast::{CssAtomSet, StyleSheet};
110
111 #[test]
112 fn test_reduce_zero_lengths() {
113 assert_transform!(
114 CssMinifierFeature::ReduceLengths,
115 CssAtomSet,
116 StyleSheet,
117 "body { width: 0px; height: 0rem; margin: 0em; }",
118 "body { width: 0; height: 0; margin: 0; }"
119 );
120 }
121
122 #[test]
123 fn test_length_shortening_guard() {
124 assert_transform!(
125 CssMinifierFeature::ReduceLengths,
126 CssAtomSet,
127 StyleSheet,
128 "div { font-size: 12pt; }",
129 "div { font-size: 16px; }"
130 );
131 }
132
133 #[test]
134 fn test_length_noop() {
135 assert_no_transform!(CssMinifierFeature::ReduceLengths, CssAtomSet, StyleSheet, "body { width: 10rem; }");
136 }
137
138 #[test]
139 fn test_unitless_zero_resolves_to_number() {
140 assert_no_transform!(CssMinifierFeature::ReduceLengths, CssAtomSet, StyleSheet, "body { line-height: 0px; }");
142 assert_no_transform!(CssMinifierFeature::ReduceLengths, CssAtomSet, StyleSheet, "body { tab-size: 0px; }");
144 assert_no_transform!(
146 CssMinifierFeature::ReduceLengths,
147 CssAtomSet,
148 StyleSheet,
149 "body { width: calc(100px - 0px); }"
150 );
151 }
152
153 #[test]
154 fn test_unitless_zero_resolves_to_length() {
155 assert_transform!(
156 CssMinifierFeature::ReduceLengths,
157 CssAtomSet,
158 StyleSheet,
159 "div { width: 0px; }",
160 "div { width: 0; }"
161 );
162 }
163}