diff --git a/crates/swc_css_compat/src/compiler/mod.rs b/crates/swc_css_compat/src/compiler/mod.rs index c8904c857a55..edd95695e8fe 100644 --- a/crates/swc_css_compat/src/compiler/mod.rs +++ b/crates/swc_css_compat/src/compiler/mod.rs @@ -1,7 +1,8 @@ use swc_common::{Spanned, DUMMY_SP}; use swc_css_ast::{ - AbsoluteColorBase, AtRule, ComponentValue, MediaAnd, MediaCondition, MediaConditionAllType, - MediaConditionWithoutOr, MediaInParens, MediaQuery, Rule, SupportsCondition, + AbsoluteColorBase, AtRule, ComponentValue, CompoundSelector, MediaAnd, MediaCondition, + MediaConditionAllType, MediaConditionWithoutOr, MediaInParens, MediaQuery, Rule, + SupportsCondition, }; use swc_css_visit::{VisitMut, VisitMutWith}; @@ -14,6 +15,7 @@ mod color_space_separated_parameters; mod custom_media; mod legacy_rgb_and_hsl; mod media_query_ranges; +mod selector_not; mod utils; /// Compiles a modern CSS file to a CSS file which works with old browsers. @@ -123,6 +125,18 @@ impl VisitMut for Compiler { } } + fn visit_mut_compound_selector(&mut self, n: &mut CompoundSelector) { + n.visit_mut_children_with(self); + + if self.in_supports_condition { + return; + } + + if self.c.process.contains(Features::SELECTOR_NOT) { + self.process_selector_not(n); + } + } + fn visit_mut_component_value(&mut self, n: &mut ComponentValue) { n.visit_mut_children_with(self); diff --git a/crates/swc_css_compat/src/compiler/selector_not.rs b/crates/swc_css_compat/src/compiler/selector_not.rs new file mode 100644 index 000000000000..5b99b97681d8 --- /dev/null +++ b/crates/swc_css_compat/src/compiler/selector_not.rs @@ -0,0 +1,55 @@ +use swc_atoms::js_word; +use swc_css_ast::{ + CompoundSelector, PseudoClassSelector, PseudoClassSelectorChildren, SelectorList, + SubclassSelector, +}; + +use crate::compiler::Compiler; + +impl Compiler { + pub(crate) fn process_selector_not(&mut self, n: &mut CompoundSelector) { + let has_not = n.subclass_selectors.iter().any(|n| matches!(n, SubclassSelector::PseudoClass(PseudoClassSelector { name, children: Some(children), ..}) if name.value == js_word!("not") + && matches!(children.get(0), Some(PseudoClassSelectorChildren::SelectorList(selector_list)) if selector_list.children.len() > 1))); + + if !has_not { + return; + } + + let mut new_subclass_selectors = Vec::with_capacity(n.subclass_selectors.len()); + + for selector in &mut n.subclass_selectors.drain(..) { + match selector { + SubclassSelector::PseudoClass(PseudoClassSelector { + span, + name, + children: Some(children), + .. + }) if name.value == js_word!("not") + && matches!(children.get(0), Some(PseudoClassSelectorChildren::SelectorList(selector_list)) if selector_list.children.len() > 1) => + { + if let Some(PseudoClassSelectorChildren::SelectorList(selector_list)) = + children.get(0) + { + for child in &selector_list.children { + new_subclass_selectors.push(SubclassSelector::PseudoClass( + PseudoClassSelector { + span, + name: name.clone(), + children: Some(vec![ + PseudoClassSelectorChildren::SelectorList(SelectorList { + span: child.span, + children: vec![child.clone()], + }), + ]), + }, + )); + } + } + } + _ => new_subclass_selectors.push(selector), + } + } + + n.subclass_selectors = new_subclass_selectors; + } +} diff --git a/crates/swc_css_compat/src/feature.rs b/crates/swc_css_compat/src/feature.rs index 05984fc04f9e..84d0d55865d5 100644 --- a/crates/swc_css_compat/src/feature.rs +++ b/crates/swc_css_compat/src/feature.rs @@ -9,5 +9,6 @@ bitflags! { const COLOR_ALPHA_PARAMETER = 1 << 4; const COLOR_SPACE_SEPARATED_PARAMETERS = 1 << 5; const COLOR_LEGACY_RGB_AND_HSL = 1 << 6; + const SELECTOR_NOT = 1 << 7; } } diff --git a/crates/swc_css_compat/tests/fixture.rs b/crates/swc_css_compat/tests/fixture.rs index 6197c8fa571e..f9f56845c4ca 100644 --- a/crates/swc_css_compat/tests/fixture.rs +++ b/crates/swc_css_compat/tests/fixture.rs @@ -62,7 +62,6 @@ fn test_nesting(input: PathBuf, suffix: Option<&str>) { }; testing::run_test(false, |cm, _| { - // let fm = cm.load_file(&input).unwrap(); let mut ss = parse_stylesheet(&fm); @@ -87,7 +86,6 @@ fn test_custom_media_query(input: PathBuf) { let output = input.with_extension("expect.css"); testing::run_test(false, |cm, _| { - // let fm = cm.load_file(&input).unwrap(); let mut ss = parse_stylesheet(&fm); @@ -109,7 +107,6 @@ fn test_media_query_ranges(input: PathBuf) { let output = input.with_extension("expect.css"); testing::run_test(false, |cm, _| { - // let fm = cm.load_file(&input).unwrap(); let mut ss = parse_stylesheet(&fm); @@ -131,7 +128,6 @@ fn test_color_hex_alpha(input: PathBuf) { let output = input.with_extension("expect.css"); testing::run_test(false, |cm, _| { - // let fm = cm.load_file(&input).unwrap(); let mut ss = parse_stylesheet(&fm); @@ -153,7 +149,6 @@ fn test_color_space_separated_function_notation(input: PathBuf) { let output = input.with_extension("expect.css"); testing::run_test(false, |cm, _| { - // let fm = cm.load_file(&input).unwrap(); let mut ss = parse_stylesheet(&fm); @@ -171,3 +166,24 @@ fn test_color_space_separated_function_notation(input: PathBuf) { }) .unwrap(); } + +#[testing::fixture("tests/selector-not/**/*.css", exclude("expect.css"))] +fn test_selector_not(input: PathBuf) { + let output = input.with_extension("expect.css"); + + testing::run_test(false, |cm, _| { + let fm = cm.load_file(&input).unwrap(); + let mut ss = parse_stylesheet(&fm); + + ss.visit_mut_with(&mut Compiler::new(Config { + process: Features::SELECTOR_NOT, + })); + + let s = print_stylesheet(&ss); + + NormalizedOutput::from(s).compare_to_file(&output).unwrap(); + + Ok(()) + }) + .unwrap(); +} diff --git a/crates/swc_css_compat/tests/selector-not/input.css b/crates/swc_css_compat/tests/selector-not/input.css new file mode 100644 index 000000000000..e9165d1f429b --- /dev/null +++ b/crates/swc_css_compat/tests/selector-not/input.css @@ -0,0 +1,101 @@ +body { + order: 1; +} + +body, Not { + order: 2; +} + +em[attr=:not], +em[attr=":not"] { + order: 3; +} + +em[attr~=:not], +em[attr~=":not"] { + order: 4; +} + +em[not=abc], +em[not="abc"] { + order: 5; +} + +:not { + order: 6; +} + +:not(a, b) { + order: 7; +} + +:nOt(a, b) { + order: 7.1; +} + +tag:not(.class, .class2) { + order: 8; +} + +tag :not(tag2, tag3) { + order: 9; +} + +tag :not(tag2, tag3) :not(tag4, tag5) { + order: 10; +} + +tag :not(tag2, tag3) :not(tag4, tag5), test { + order: 11; +} + +tag :not(tag2 :not(tag4, tag5), tag3) { + order: 12; +} + +.foo:not(:nth-child(-n+2), .bar) { + order: 13; +} + +a:not(.b, + .c) { + order: 14; +} + +.foo:not(:hover, :focus)::before { + order: 15; +} + +.foo\\:not-italic { + order: 16; +} + +.foo\\:not-italic:not(:hover, :focus) { + order: 17; +} + +:not :dir(ltr) { + order: 18; +} + +:not(something > complex, other) { + order: 19; +} + +div:not([style*="(120, 60, 12"]) { + order: 20; +} + +@supports selector(:not(something > complex, other)) { + :not(something > complex, other) { + order: 19; + } +} + +:not(h1, h2, h3) { + color: red; +} + +:not(h1) { + color: red; +} \ No newline at end of file diff --git a/crates/swc_css_compat/tests/selector-not/input.expect.css b/crates/swc_css_compat/tests/selector-not/input.expect.css new file mode 100644 index 000000000000..263a3adc020f --- /dev/null +++ b/crates/swc_css_compat/tests/selector-not/input.expect.css @@ -0,0 +1,79 @@ +body { + order: 1; +} +body, +Not { + order: 2; +} +em[attr=:not], +em[attr=":not"] { + order: 3; +} +em[attr~=:not], +em[attr~=":not"] { + order: 4; +} +em[not=abc], +em[not="abc"] { + order: 5; +} +:not { + order: 6; +} +:not(a):not(b) { + order: 7; +} +:nOt(a):nOt(b) { + order: 7.1; +} +tag:not(.class):not(.class2) { + order: 8; +} +tag :not(tag2):not(tag3) { + order: 9; +} +tag :not(tag2):not(tag3) :not(tag4):not(tag5) { + order: 10; +} +tag :not(tag2):not(tag3) :not(tag4):not(tag5), +test { + order: 11; +} +tag :not(tag2 :not(tag4):not(tag5)):not(tag3) { + order: 12; +} +.foo:not(:nth-child(-n+2)):not(.bar) { + order: 13; +} +a:not(.b):not(.c) { + order: 14; +} +.foo:not(:hover):not(:focus)::before { + order: 15; +} +.foo\\:not-italic { + order: 16; +} +.foo\\:not-italic:not(:hover):not(:focus) { + order: 17; +} +:not :dir(ltr) { + order: 18; +} +:not(something > complex):not(other) { + order: 19; +} +div:not([style*="(120, 60, 12"]) { + order: 20; +} +@supports selector(:not(something > complex, other)) { + :not(something > complex):not(other) { + order: 19; + } +} +:not(h1):not(h2):not(h3) { + color: red; +} +:not(h1) { + color: red; +}