Skip to main content

csskit_transform/
reduce_shorthand_values.rs

1use crate::prelude::*;
2use css_ast::{
3	BorderColorStyleValue, BorderRadiusStyleValue, BorderStyleStyleValue, BorderWidthStyleValue, ColumnGapStyleValue,
4	DeclarationValue, GapStyleValue, InsetStyleValue, MarginStyleValue, OverflowStyleValue, PaddingStyleValue,
5	RowGapStyleValue, VisitNode, Visitable,
6};
7use css_parse::{Declaration, SemanticEq};
8
9/// Removes redundant parts from values of CSS shorthand declarations, for example `margin: 1px 1px 1px 1px` becomes
10/// `margin: 1px`.
11pub struct ReduceShorthandValues<'a, 'ctx, N: Visitable + NodeWithMetadata<CssMetadata>> {
12	pub transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>,
13}
14
15impl<'a, 'ctx, N> ReduceShorthandValues<'a, 'ctx, N>
16where
17	N: Visitable + NodeWithMetadata<CssMetadata>,
18{
19	fn delete<T: ToSpan>(&self, value: &T) {
20		let span = value.to_span();
21		self.transformer.clear_pending_edits(span);
22		self.transformer.delete(span);
23	}
24
25	fn reduce_two<T>(&self, first: &T, second: &Option<T>)
26	where
27		T: SemanticEq + ToSpan,
28	{
29		if let Some(second) = second
30			&& second.semantic_eq(first)
31		{
32			self.delete(second);
33		}
34	}
35
36	fn reduce_four<T>(&self, first: &T, second: &Option<T>, third: &Option<T>, fourth: &Option<T>)
37	where
38		T: SemanticEq + ToSpan,
39	{
40		if let Some(fourth) = fourth {
41			let Some(second) = second else { return };
42			if !fourth.semantic_eq(second) {
43				return;
44			}
45			self.delete(fourth);
46		}
47		if let Some(third) = third {
48			if !third.semantic_eq(first) {
49				return;
50			}
51			self.delete(third);
52		}
53		self.reduce_two(first, second);
54	}
55}
56
57impl<'a, 'ctx, N> Transform<'a, 'ctx, CssMetadata, N, CssMinifierFeature> for ReduceShorthandValues<'a, 'ctx, N>
58where
59	N: Visitable + NodeWithMetadata<CssMetadata>,
60{
61	fn skips_subtree(metadata: &CssMetadata) -> bool {
62		!metadata.has_shorthands()
63	}
64
65	fn new(transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>) -> Self {
66		Self { transformer }
67	}
68}
69
70#[visitor]
71impl<'a, 'ctx, N> Visit for ReduceShorthandValues<'a, 'ctx, N>
72where
73	N: Visitable + NodeWithMetadata<CssMetadata>,
74{
75	fn enter_declaration<'b, T: DeclarationValue<'b, CssMetadata>>(
76		&mut self,
77		declaration: &Declaration<'b, T, CssMetadata>,
78		_query: VisitNode,
79	) -> VisitFlow {
80		if declaration.metadata().has_substitution() {
81			return VisitFlow::SKIP_CHILDREN;
82		}
83		VisitFlow::DESCEND
84	}
85
86	fn visit_margin_style_value(&mut self, value: &MarginStyleValue) {
87		self.reduce_four(&value.0, &value.1, &value.2, &value.3);
88	}
89
90	fn visit_padding_style_value(&mut self, value: &PaddingStyleValue) {
91		self.reduce_four(&value.0, &value.1, &value.2, &value.3);
92	}
93
94	fn visit_inset_style_value(&mut self, value: &InsetStyleValue) {
95		self.reduce_four(&value.0, &value.1, &value.2, &value.3);
96	}
97
98	fn visit_border_color_style_value(&mut self, value: &BorderColorStyleValue) {
99		self.reduce_four(&value.0, &value.1, &value.2, &value.3);
100	}
101
102	fn visit_border_style_style_value(&mut self, value: &BorderStyleStyleValue) {
103		self.reduce_four(&value.0, &value.1, &value.2, &value.3);
104	}
105
106	fn visit_border_width_style_value(&mut self, value: &BorderWidthStyleValue) {
107		self.reduce_four(&value.0, &value.1, &value.2, &value.3);
108	}
109	fn visit_border_radius_style_value(&mut self, value: &BorderRadiusStyleValue) {
110		self.reduce_four(&value.0, &value.1, &value.2, &value.3);
111		if let Some((_, first, second, third, fourth)) = &value.4 {
112			self.reduce_four(first, second, third, fourth);
113		}
114	}
115
116	fn visit_gap_style_value(&mut self, value: &GapStyleValue) {
117		if let Some(second) = &value.1
118			&& gap_values_equal(&value.0, second)
119		{
120			self.delete(second);
121		}
122	}
123
124	fn visit_overflow_style_value(&mut self, value: &OverflowStyleValue) {
125		self.reduce_two(&value.0, &value.1);
126	}
127}
128
129pub(crate) fn gap_values_equal<'a>(first: &RowGapStyleValue<'a>, second: &ColumnGapStyleValue<'a>) -> bool {
130	match (first, second) {
131		(RowGapStyleValue::Normal(first), ColumnGapStyleValue::Normal(second)) => first.semantic_eq(second),
132		(RowGapStyleValue::LengthPercentage(first), ColumnGapStyleValue::LengthPercentage(second)) => {
133			first.semantic_eq(second)
134		}
135		(RowGapStyleValue::LineWidth(first), ColumnGapStyleValue::LineWidth(second)) => first.semantic_eq(second),
136		_ => false,
137	}
138}
139
140#[cfg(test)]
141mod tests {
142	use crate::test_helpers::{assert_no_transform, assert_transform};
143	use css_ast::{CssAtomSet, StyleSheet};
144
145	#[test]
146	fn reduces_four_value_shorthands() {
147		assert_transform!(
148			CssMinifierFeature::ReduceShorthandValues,
149			CssAtomSet,
150			StyleSheet,
151			"a { margin: 1px 1px 1px 1px; padding: 1px 2px 1px 2px; inset: 1px 2px 3px 2px; }",
152			"a { margin: 1px; padding: 1px 2px; inset: 1px 2px 3px; }"
153		);
154	}
155
156	#[test]
157	fn reduces_border_and_overflow_shorthands() {
158		assert_transform!(
159			CssMinifierFeature::ReduceShorthandValues,
160			CssAtomSet,
161			StyleSheet,
162			"a { border-color: red red red red; border-radius: 10px 10px 10px 10px / 5px 5px 5px 5px; border-style: solid solid; border-width: 1px 2px 1px 2px; gap: 1px 1px; overflow: hidden hidden; }",
163			"a { border-color: red; border-radius: 10px / 5px; border-style: solid; border-width: 1px 2px; gap: 1px; overflow: hidden; }"
164		);
165	}
166
167	#[test]
168	fn keeps_irreducible_shorthands() {
169		assert_no_transform!(
170			CssMinifierFeature::ReduceShorthandValues,
171			CssAtomSet,
172			StyleSheet,
173			"a { margin: 1px 2px 3px 4px; }"
174		);
175	}
176
177	#[test]
178	fn keeps_shorthands_using_substitution() {
179		assert_no_transform!(
180			CssMinifierFeature::ReduceShorthandValues,
181			CssAtomSet,
182			StyleSheet,
183			"a { margin: var(--a) var(--a); gap: var(--g) var(--g); }"
184		);
185	}
186
187	#[test]
188	fn keeps_declarations_intact() {
189		assert_no_transform!(
190			CssMinifierFeature::ReduceShorthandValues,
191			CssAtomSet,
192			StyleSheet,
193			"a { border-width: 1px; border-style: solid; border-color: red; margin-top: 1px; margin: 2px; }"
194		);
195	}
196}