Skip to main content

csskit_transform/
reduce_lengths.rs

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	/// 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	/// Depth of math-function nesting (`calc()`, `min()`, etc). Inside a math function a `0px`
12	/// carries its `<length>` type, whereas a unitless `0` is a `<number>`; reducing one to the
13	/// other changes the calc's resolved type, so zero-length reduction is suppressed while > 0.
14	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		// Inside a math function, `0px` and `0` differ in type; never reduce lengths here.
68		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		// line-height is not safe to reduce to `0` as they're semantically different.
141		assert_no_transform!(CssMinifierFeature::ReduceLengths, CssAtomSet, StyleSheet, "body { line-height: 0px; }");
142		// tab-size is not safe to reduce to `0` as they're semantically different.
143		assert_no_transform!(CssMinifierFeature::ReduceLengths, CssAtomSet, StyleSheet, "body { tab-size: 0px; }");
144		// calc is not safe to reduce to `0` as it changes the return type
145		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}