csskit_transform/
reduce_lengths.rs

1use 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	/// Tracks how unitless zero resolves in the current declaration context. When visiting inside a declaration where
9	/// unitless zero resolves to Number, we skip the converting zero lengths to unitless zero.
10	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		// line-height is not safe to reduce to `0` as they're semantically different.
114		assert_no_transform!(CssMinifierFeature::ReduceLengths, CssAtomSet, StyleSheet, "body { line-height: 0px; }");
115		// tab-size is not safe to reduce to `0` as they're semantically different.
116		assert_no_transform!(CssMinifierFeature::ReduceLengths, CssAtomSet, StyleSheet, "body { tab-size: 0px; }");
117		// calc is not safe to reduce to `0` as it changes the return type
118		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}