Skip to main content

csskit_transform/
remove_overridden_declarations.rs

1use crate::prelude::*;
2use css_ast::{CssAtomSet, NestedGroupRule, StyleRule, StyleValue, VisitNode, Visitable};
3use css_parse::{AtomSet, Cursor, Declaration};
4
5/// Removes declarations which a later declaration of the same block fully overrides, for example the `margin-top` of
6/// `margin-top: 1px; margin: 2px`.
7///
8/// A declaration is overridden when every property it sets is also set, or reset, by a later declaration of equal or
9/// greater importance.
10pub struct RemoveOverriddenDeclarations<'a, 'ctx, N: Visitable + NodeWithMetadata<CssMetadata>> {
11	pub transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>,
12}
13
14impl<'a, 'ctx, N> RemoveOverriddenDeclarations<'a, 'ctx, N>
15where
16	N: Visitable + NodeWithMetadata<CssMetadata>,
17{
18	fn delete<T: ToSpan>(&self, value: &T) {
19		let span = value.to_span();
20		self.transformer.clear_pending_edits(span);
21		self.transformer.delete(span);
22	}
23
24	fn property_name<'s>(declaration: &Declaration<'s, StyleValue<'s>, CssMetadata>) -> CssAtomSet {
25		let cursor: Cursor = declaration.name.into();
26		CssAtomSet::from_bits(cursor.token().atom_bits())
27	}
28
29	fn later_declaration_overrides<'s>(
30		earlier: &Declaration<'s, StyleValue<'s>, CssMetadata>,
31		later: &Declaration<'s, StyleValue<'s>, CssMetadata>,
32	) -> bool {
33		if earlier.important.is_some() && later.important.is_none()
34			|| later.is_unknown()
35			|| later.metadata().has_substitution()
36		{
37			return false;
38		}
39		let earlier_name = Self::property_name(earlier);
40		let later_name = Self::property_name(later);
41		if earlier_name == later_name {
42			return false;
43		}
44		if let Some(later) = StyleValue::shorthand_by_name(later_name) {
45			let covered = match StyleValue::shorthand_by_name(earlier_name) {
46				// `all` expresses no property of its own, and the properties it resets are not recorded,
47				// so no later declaration is known to cover it.
48				Some(earlier) if earlier.longhands.is_empty() => false,
49				Some(earlier) => earlier.longhands.iter().all(|longhand| later.longhands.contains(longhand)),
50				None => later.longhands.contains(&earlier_name),
51			};
52			if covered {
53				return true;
54			}
55		}
56		StyleValue::longhand_by_name(earlier_name).is_some_and(|earlier| earlier.reset_by.contains(&later_name))
57	}
58
59	fn remove_overridden<'b, 's, I>(&self, declarations: I)
60	where
61		I: Clone + Iterator<Item = &'b Declaration<'s, StyleValue<'s>, CssMetadata>>,
62		's: 'b,
63	{
64		let mut declarations = declarations;
65		while let Some(declaration) = declarations.next() {
66			if declaration.is_unknown() {
67				continue;
68			}
69			if declarations.clone().any(|later| Self::later_declaration_overrides(declaration, later)) {
70				self.delete(declaration);
71			}
72		}
73	}
74}
75
76impl<'a, 'ctx, N> Transform<'a, 'ctx, CssMetadata, N, CssMinifierFeature> for RemoveOverriddenDeclarations<'a, 'ctx, N>
77where
78	N: Visitable + NodeWithMetadata<CssMetadata>,
79{
80	fn skips_subtree(metadata: &CssMetadata) -> bool {
81		!metadata.has_shorthands()
82	}
83
84	fn new(transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>) -> Self {
85		Self { transformer }
86	}
87}
88
89#[visitor]
90impl<'a, 'ctx, N> Visit for RemoveOverriddenDeclarations<'a, 'ctx, N>
91where
92	N: Visitable + NodeWithMetadata<CssMetadata>,
93{
94	fn exit_style_rule(&mut self, rule: &StyleRule) {
95		self.remove_overridden(rule.rule.block.declarations.iter());
96		for nested_rule in &rule.rule.block.rules {
97			if let NestedGroupRule::Declarations(group) = nested_rule {
98				let declarations = || {
99					group.declarations.iter().filter_map(|item| match item {
100						css_parse::DeclarationOrBad::Declaration(declaration) => Some(declaration),
101						css_parse::DeclarationOrBad::Bad(_) => None,
102					})
103				};
104				self.remove_overridden(declarations());
105			}
106		}
107	}
108}
109
110#[cfg(test)]
111mod tests {
112	use crate::test_helpers::{assert_no_transform, assert_transform};
113	use css_ast::{CssAtomSet, StyleSheet};
114
115	#[test]
116	fn removes_declarations_overridden_by_later_shorthands() {
117		assert_transform!(
118			CssMinifierFeature::RemoveOverriddenDeclarations,
119			CssAtomSet,
120			StyleSheet,
121			"a { margin-top: 1px; margin: 2px; border-image: url(border.png) 30; border: 1px solid; font-weight: bold; font: 16px serif; }",
122			"a { margin: 2px; border: 1px solid; font: 16px serif; }"
123		);
124	}
125
126	#[test]
127	fn keeps_important_declarations_before_normal_shorthands() {
128		assert_no_transform!(
129			CssMinifierFeature::RemoveOverriddenDeclarations,
130			CssAtomSet,
131			StyleSheet,
132			"a { margin-top: 1px !important; margin: 2px; }"
133		);
134	}
135
136	#[test]
137	fn keeps_declarations_before_substituted_shorthands() {
138		assert_no_transform!(
139			CssMinifierFeature::RemoveOverriddenDeclarations,
140			CssAtomSet,
141			StyleSheet,
142			"a { margin-top: 1px; margin: var(--margin); }"
143		);
144	}
145
146	#[test]
147	fn keeps_declarations_a_later_shorthand_does_not_cover() {
148		assert_no_transform!(
149			CssMinifierFeature::RemoveOverriddenDeclarations,
150			CssAtomSet,
151			StyleSheet,
152			"a { margin-top: 1px; padding: 2px; }"
153		);
154	}
155}