csskit_transform/
remove_overridden_declarations.rs1use crate::prelude::*;
2use css_ast::{CssAtomSet, NestedGroupRule, StyleRule, StyleValue, VisitNode, Visitable};
3use css_parse::{AtomSet, Cursor, Declaration};
4
5pub 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 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}