swc_css_minifier/compressor/
rules.rs

1use std::mem::take;
2
3use rustc_hash::FxHashMap;
4use swc_atoms::Atom;
5use swc_common::{util::take::Take, EqIgnoreSpan, Span, Spanned};
6use swc_css_ast::*;
7use swc_css_visit::{Visit, VisitMutWith, VisitWith};
8
9use super::Compressor;
10
11enum ParentNode<'a> {
12    Stylesheet(&'a mut Stylesheet),
13    SimpleBlock(&'a mut SimpleBlock),
14}
15
16#[derive(Eq, Hash, PartialEq)]
17enum Name {
18    CounterStyle(Atom),
19    // We need to keep prefixed keyframes, i.e. `@-webkit-keyframes`
20    Keyframes(Atom, Atom),
21}
22
23struct CompatibilityChecker {
24    pub allow_to_merge: bool,
25}
26
27impl Default for CompatibilityChecker {
28    fn default() -> Self {
29        CompatibilityChecker {
30            allow_to_merge: true,
31        }
32    }
33}
34
35// TODO improve me https://github.com/cssnano/cssnano/blob/master/packages/postcss-merge-rules/src/lib/ensureCompatibility.js#L62, need browserslist
36impl Visit for CompatibilityChecker {
37    fn visit_pseudo_class_selector(&mut self, _n: &PseudoClassSelector) {
38        self.allow_to_merge = false;
39    }
40
41    fn visit_pseudo_element_selector(&mut self, _n: &PseudoElementSelector) {
42        self.allow_to_merge = false;
43    }
44
45    fn visit_attribute_selector(&mut self, n: &AttributeSelector) {
46        if n.modifier.is_some() {
47            self.allow_to_merge = false;
48        }
49    }
50}
51
52impl Compressor {
53    fn get_at_rule_name(&self, at_rule: &AtRule) -> Atom {
54        match &at_rule.name {
55            AtRuleName::Ident(Ident { value, .. }) => value.clone(),
56            AtRuleName::DashedIdent(DashedIdent { value, .. }) => value.clone(),
57        }
58    }
59
60    fn is_same_declaration_name(&self, left: &Declaration, right: &Declaration) -> bool {
61        match (&left.name, &right.name) {
62            (
63                DeclarationName::Ident(Ident {
64                    value: left_value, ..
65                }),
66                DeclarationName::Ident(Ident {
67                    value: right_value, ..
68                }),
69            ) => left_value.eq_ignore_ascii_case(right_value),
70            (
71                DeclarationName::DashedIdent(DashedIdent {
72                    value: left_value, ..
73                }),
74                DeclarationName::DashedIdent(DashedIdent {
75                    value: right_value, ..
76                }),
77            ) => left_value == right_value,
78            _ => false,
79        }
80    }
81
82    fn collect_names(&self, at_rule: &AtRule, names: &mut FxHashMap<Name, isize>) {
83        let Some(prelude) = &at_rule.prelude else {
84            return;
85        };
86
87        match &**prelude {
88            AtRulePrelude::CounterStylePrelude(CustomIdent { value: name, .. }) => {
89                names
90                    .entry(Name::CounterStyle(name.clone()))
91                    .and_modify(|mana| *mana += 1)
92                    .or_insert(1);
93            }
94            prelude => {
95                let name = match prelude {
96                    AtRulePrelude::KeyframesPrelude(KeyframesName::CustomIdent(custom_ident)) => {
97                        &custom_ident.value
98                    }
99                    AtRulePrelude::KeyframesPrelude(KeyframesName::Str(s)) => &s.value,
100                    _ => return,
101                };
102
103                names
104                    .entry(Name::Keyframes(
105                        self.get_at_rule_name(at_rule),
106                        name.clone(),
107                    ))
108                    .and_modify(|mana| *mana += 1)
109                    .or_insert(1);
110            }
111        }
112    }
113
114    fn discard_overridden(
115        &self,
116        parent_node: ParentNode,
117        names: &mut FxHashMap<Name, isize>,
118        remove_rules_list: &mut Vec<usize>,
119    ) {
120        let mut discarder = |at_rule: &AtRule| {
121            let Some(prelude) = &at_rule.prelude else {
122                return true;
123            };
124
125            match &**prelude {
126                AtRulePrelude::CounterStylePrelude(CustomIdent { value: name, .. }) => {
127                    if let Some(counter) = names.get_mut(&Name::CounterStyle(name.clone())) {
128                        if *counter > 1 {
129                            *counter -= 1;
130
131                            false
132                        } else {
133                            true
134                        }
135                    } else {
136                        false
137                    }
138                }
139                prelude => {
140                    let name = match prelude {
141                        AtRulePrelude::KeyframesPrelude(KeyframesName::CustomIdent(
142                            custom_ident,
143                        )) => &custom_ident.value,
144                        AtRulePrelude::KeyframesPrelude(KeyframesName::Str(s)) => &s.value,
145                        _ => return true,
146                    };
147
148                    let counter = names.get_mut(&Name::Keyframes(
149                        self.get_at_rule_name(at_rule),
150                        name.clone(),
151                    ));
152
153                    if let Some(counter) = counter {
154                        if *counter > 1 {
155                            *counter -= 1;
156
157                            false
158                        } else {
159                            true
160                        }
161                    } else {
162                        false
163                    }
164                }
165            }
166        };
167
168        match parent_node {
169            ParentNode::Stylesheet(stylesheet) => {
170                for index in 0..stylesheet.rules.len() {
171                    let node = stylesheet.rules.get(index);
172
173                    if let Some(Rule::AtRule(at_rule)) = node {
174                        if !discarder(at_rule) {
175                            remove_rules_list.push(index);
176                        }
177                    }
178                }
179            }
180            ParentNode::SimpleBlock(simple_block) => {
181                for index in 0..simple_block.value.len() {
182                    let node = simple_block.value.get(index);
183
184                    if let Some(ComponentValue::AtRule(at_rule)) = node {
185                        if !discarder(at_rule) {
186                            remove_rules_list.push(index);
187                        }
188                    }
189                }
190            }
191        }
192    }
193
194    fn merge_selector_list(
195        &self,
196        left: &mut SelectorList,
197        right: &mut SelectorList,
198    ) -> SelectorList {
199        let mut children = left.children.take();
200
201        children.extend(right.children.take());
202
203        SelectorList {
204            span: Span::new(left.span_lo(), right.span_hi()),
205            children,
206        }
207    }
208
209    fn merge_relative_selector_list(
210        &self,
211        left: &mut RelativeSelectorList,
212        right: &mut RelativeSelectorList,
213    ) -> RelativeSelectorList {
214        let mut children = left.children.take();
215
216        children.extend(right.children.take());
217
218        RelativeSelectorList {
219            span: Span::new(left.span_lo(), right.span_hi()),
220            children,
221        }
222    }
223
224    fn merge_simple_block(&self, left: &mut SimpleBlock, right: &mut SimpleBlock) -> SimpleBlock {
225        let mut value = left.value.take();
226
227        value.extend(right.value.take());
228
229        SimpleBlock {
230            span: Span::new(left.span_lo(), right.span_hi()),
231            name: left.name.clone(),
232            value,
233        }
234    }
235
236    fn can_merge_qualified_rules(&self, left: &QualifiedRule, right: &QualifiedRule) -> bool {
237        match (&left.prelude, &right.prelude) {
238            (
239                QualifiedRulePrelude::SelectorList(left_selector_list),
240                QualifiedRulePrelude::SelectorList(right_selector_list),
241            ) => {
242                let mut checker = CompatibilityChecker::default();
243
244                left_selector_list.visit_with(&mut checker);
245                right_selector_list.visit_with(&mut checker);
246
247                checker.allow_to_merge
248            }
249            (
250                QualifiedRulePrelude::RelativeSelectorList(left_relative_selector_list),
251                QualifiedRulePrelude::RelativeSelectorList(right_relative_selector_list),
252            ) => {
253                let mut checker = CompatibilityChecker::default();
254
255                left_relative_selector_list.visit_with(&mut checker);
256                right_relative_selector_list.visit_with(&mut checker);
257
258                checker.allow_to_merge
259            }
260            _ => false,
261        }
262    }
263
264    fn try_merge_qualified_rules(
265        &mut self,
266        left: &mut QualifiedRule,
267        right: &mut QualifiedRule,
268    ) -> Option<QualifiedRule> {
269        if !self.can_merge_qualified_rules(left, right) {
270            return None;
271        }
272
273        // Merge when declarations are exactly equal
274        // e.g. h1 { color: red } h2 { color: red }
275        if left.block.eq_ignore_span(&right.block) {
276            match (&mut left.prelude, &mut right.prelude) {
277                (
278                    QualifiedRulePrelude::SelectorList(prev_selector_list),
279                    QualifiedRulePrelude::SelectorList(current_selector_list),
280                ) => {
281                    let selector_list =
282                        self.merge_selector_list(prev_selector_list, current_selector_list);
283                    let mut qualified_rule = QualifiedRule {
284                        span: Span::new(left.span_lo(), right.span_hi()),
285                        prelude: QualifiedRulePrelude::SelectorList(selector_list),
286                        block: left.block.take(),
287                    };
288
289                    qualified_rule.visit_mut_children_with(self);
290
291                    return Some(qualified_rule);
292                }
293                (
294                    QualifiedRulePrelude::RelativeSelectorList(prev_relative_selector_list),
295                    QualifiedRulePrelude::RelativeSelectorList(current_relative_selector_list),
296                ) => {
297                    let relative_selector_list = self.merge_relative_selector_list(
298                        prev_relative_selector_list,
299                        current_relative_selector_list,
300                    );
301                    let mut qualified_rule = QualifiedRule {
302                        span: Span::new(left.span_lo(), right.span_hi()),
303                        prelude: QualifiedRulePrelude::RelativeSelectorList(relative_selector_list),
304                        block: left.block.take(),
305                    };
306
307                    qualified_rule.visit_mut_children_with(self);
308
309                    return Some(qualified_rule);
310                }
311                _ => {}
312            }
313        }
314
315        // Merge when both selectors are exactly equal
316        // e.g. a { color: blue } a { font-weight: bold }
317        if left.prelude.eq_ignore_span(&right.prelude) {
318            let block = self.merge_simple_block(&mut left.block, &mut right.block);
319            let mut qualified_rule = QualifiedRule {
320                span: Span::new(left.span_lo(), right.span_hi()),
321                prelude: left.prelude.take(),
322                block,
323            };
324
325            qualified_rule.visit_mut_children_with(self);
326
327            return Some(qualified_rule);
328        }
329
330        // Partial merge: check if the rule contains a subset of the last; if
331        // so create a joined selector with the subset, if smaller.
332        // TODO improve me
333
334        None
335    }
336
337    fn is_mergeable_at_rule(&self, at_rule: &AtRule) -> bool {
338        let name = match &at_rule.name {
339            AtRuleName::Ident(Ident { value, .. }) => value,
340            _ => return false,
341        };
342
343        matches!(
344            &**name,
345            "media" | "supports" | "container" | "layer" | "nest"
346        )
347    }
348
349    fn try_merge_at_rule(&mut self, left: &mut AtRule, right: &mut AtRule) -> Option<AtRule> {
350        // Merge when both at-rule's prelude is exactly equal
351        // e.g.
352        // @media print { .color { color: red; } }
353        // @media print { .color { color: blue; } }
354        if left.prelude.eq_ignore_span(&right.prelude) {
355            if let Some(left_block) = &mut left.block {
356                if let Some(right_block) = &mut right.block {
357                    let block = self.merge_simple_block(left_block, right_block);
358                    let mut at_rule = AtRule {
359                        span: Span::new(left.span.span_lo(), right.span.span_lo()),
360                        name: left.name.clone(),
361                        prelude: left.prelude.take(),
362                        block: Some(block),
363                    };
364
365                    at_rule.visit_mut_children_with(self);
366
367                    return Some(at_rule);
368                }
369            }
370        }
371
372        None
373    }
374
375    pub(super) fn compress_stylesheet(&mut self, stylesheet: &mut Stylesheet) {
376        let mut names: FxHashMap<Name, isize> = Default::default();
377        let mut prev_rule_idx = None;
378        let mut remove_rules_list = Vec::new();
379        let mut prev_index = 0;
380
381        for index in 0..stylesheet.rules.len() {
382            // We need two &mut
383            let (a, b) = stylesheet.rules.split_at_mut(index);
384
385            let mut prev_rule = match prev_rule_idx {
386                Some(idx) => a.get_mut(idx),
387                None => None,
388            };
389            let rule = match b.first_mut() {
390                Some(v) => v,
391                None => continue,
392            };
393
394            let result = match rule {
395                Rule::AtRule(at_rule)
396                    if at_rule
397                        .name
398                        .as_ident()
399                        .map(|ident| !need_keep_by_name(&ident.value))
400                        .unwrap_or_default()
401                        && at_rule
402                            .block
403                            .as_ref()
404                            .map(|block| block.value.is_empty())
405                            .unwrap_or_default() =>
406                {
407                    false
408                }
409                Rule::QualifiedRule(qualified_rule) if qualified_rule.block.value.is_empty() => {
410                    false
411                }
412                Rule::AtRule(at_rule)
413                    if self.is_mergeable_at_rule(at_rule)
414                        && matches!(prev_rule, Some(Rule::AtRule(_))) =>
415                {
416                    if let Some(Rule::AtRule(prev_rule)) = &mut prev_rule {
417                        if let Some(at_rule) = self.try_merge_at_rule(prev_rule, at_rule) {
418                            *rule = Rule::AtRule(Box::new(at_rule));
419
420                            remove_rules_list.push(prev_index);
421                        }
422                    }
423
424                    true
425                }
426                Rule::QualifiedRule(qualified_rule)
427                    if matches!(prev_rule, Some(Rule::QualifiedRule(_))) =>
428                {
429                    if let Some(Rule::QualifiedRule(prev_rule)) = &mut prev_rule {
430                        if let Some(qualified_rule) =
431                            self.try_merge_qualified_rules(prev_rule, qualified_rule)
432                        {
433                            *rule = Rule::QualifiedRule(Box::new(qualified_rule));
434
435                            remove_rules_list.push(prev_index);
436                        }
437                    }
438
439                    true
440                }
441                _ => {
442                    if let Rule::AtRule(rule) = rule {
443                        self.collect_names(rule, &mut names);
444                    }
445
446                    true
447                }
448            };
449
450            if result {
451                match rule {
452                    Rule::AtRule(at_rule) if self.is_mergeable_at_rule(at_rule) => {
453                        prev_index = index;
454                        prev_rule_idx = Some(index);
455                    }
456                    Rule::QualifiedRule(_) => {
457                        prev_index = index;
458                        prev_rule_idx = Some(index);
459                    }
460                    _ => {
461                        prev_rule_idx = None;
462                    }
463                }
464            }
465
466            if !result {
467                remove_rules_list.push(index);
468            }
469        }
470
471        if !names.is_empty() {
472            self.discard_overridden(
473                ParentNode::Stylesheet(stylesheet),
474                &mut names,
475                &mut remove_rules_list,
476            );
477        }
478
479        if !remove_rules_list.is_empty() {
480            stylesheet.rules = take(&mut stylesheet.rules)
481                .into_iter()
482                .enumerate()
483                .filter_map(|(idx, value)| {
484                    if remove_rules_list.contains(&idx) {
485                        None
486                    } else {
487                        Some(value)
488                    }
489                })
490                .collect::<Vec<_>>();
491
492            // To merge
493            // .foo {
494            //     background: red;
495            // }
496            //
497            // .bar {
498            //     background: red;
499            // }
500            //
501            // .foo {
502            //     color: green;
503            // }
504            //
505            // .bar {
506            //     color: green;
507            // }
508            self.compress_stylesheet(stylesheet);
509        }
510    }
511
512    pub(super) fn compress_simple_block(&mut self, simple_block: &mut SimpleBlock) {
513        let mut names: FxHashMap<Name, isize> = Default::default();
514        let mut prev_rule_idx = None;
515        let mut remove_rules_list = Vec::new();
516        let mut prev_index = 0;
517
518        for index in 0..simple_block.value.len() {
519            // We need two &mut
520            let (a, b) = simple_block.value.split_at_mut(index);
521
522            let mut prev_rule = match prev_rule_idx {
523                Some(idx) => a.get_mut(idx),
524                None => None,
525            };
526            let rule = match b.first_mut() {
527                Some(v) => v,
528                None => continue,
529            };
530
531            let result = match rule {
532                ComponentValue::AtRule(at_rule)
533                    if at_rule
534                        .block
535                        .as_ref()
536                        .map(|block| block.value.is_empty())
537                        .unwrap_or_default() =>
538                {
539                    false
540                }
541                ComponentValue::QualifiedRule(qualified_rule)
542                    if qualified_rule.block.value.is_empty() =>
543                {
544                    false
545                }
546                ComponentValue::KeyframeBlock(keyframe_block)
547                    if keyframe_block.block.value.is_empty() =>
548                {
549                    false
550                }
551                ComponentValue::AtRule(at_rule)
552                    if prev_rule.is_some() && self.is_mergeable_at_rule(at_rule) =>
553                {
554                    if let Some(ComponentValue::AtRule(prev_rule)) = &mut prev_rule {
555                        if let Some(at_rule) = self.try_merge_at_rule(prev_rule, at_rule) {
556                            *rule = ComponentValue::AtRule(Box::new(at_rule));
557
558                            remove_rules_list.push(prev_index);
559                        }
560                    }
561
562                    true
563                }
564                ComponentValue::QualifiedRule(qualified_rule) if prev_rule.is_some() => {
565                    if let Some(ComponentValue::QualifiedRule(prev_rule)) = &mut prev_rule {
566                        if let Some(qualified_rule) =
567                            self.try_merge_qualified_rules(prev_rule, qualified_rule)
568                        {
569                            *rule = ComponentValue::QualifiedRule(Box::new(qualified_rule));
570
571                            remove_rules_list.push(prev_index);
572                        }
573                    }
574
575                    true
576                }
577                ComponentValue::Declaration(declaration) if prev_rule.is_some() => {
578                    if let Some(ComponentValue::Declaration(prev_rule)) = &mut prev_rule {
579                        if self.is_same_declaration_name(prev_rule, declaration)
580                            && prev_rule.value.eq_ignore_span(&declaration.value)
581                        {
582                            remove_rules_list.push(prev_index);
583                        }
584                    }
585
586                    true
587                }
588                _ => {
589                    if let ComponentValue::AtRule(rule) = rule {
590                        self.collect_names(rule, &mut names);
591                    }
592
593                    true
594                }
595            };
596
597            if result {
598                match rule {
599                    ComponentValue::AtRule(at_rule) if self.is_mergeable_at_rule(at_rule) => {
600                        prev_index = index;
601                        prev_rule_idx = Some(index);
602                    }
603                    ComponentValue::QualifiedRule(_) => {
604                        prev_index = index;
605                        prev_rule_idx = Some(index);
606                    }
607                    ComponentValue::Declaration(_) => {
608                        prev_index = index;
609                        prev_rule_idx = Some(index);
610                    }
611                    _ => {
612                        prev_rule_idx = None;
613                    }
614                }
615            }
616
617            if !result {
618                remove_rules_list.push(index);
619            }
620        }
621
622        if !names.is_empty() {
623            self.discard_overridden(
624                ParentNode::SimpleBlock(simple_block),
625                &mut names,
626                &mut remove_rules_list,
627            );
628        }
629
630        if !remove_rules_list.is_empty() {
631            simple_block.value = take(&mut simple_block.value)
632                .into_iter()
633                .enumerate()
634                .filter_map(|(idx, value)| {
635                    if remove_rules_list.contains(&idx) {
636                        None
637                    } else {
638                        Some(value)
639                    }
640                })
641                .collect::<Vec<_>>();
642
643            // To merge
644            // .foo {
645            //     & .foo {
646            //         background: red;
647            //     }
648            //
649            //     & .bar {
650            //         background: red;
651            //     }
652            //
653            //     & .foo {
654            //         color: green;
655            //     }
656            //
657            //     & .bar {
658            //         color: green;
659            //     }
660            // }
661            self.compress_simple_block(simple_block);
662        }
663    }
664}
665
666#[inline]
667fn need_keep_by_name(name: &Atom) -> bool {
668    *name == "color-profile"
669}