swc_css_minifier/compressor/
rules.rs1use 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 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
35impl 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 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 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 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 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 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 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 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 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}