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
9pub 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}