diff --git a/src/lib.rs b/src/lib.rs index e40b868c..3004a5ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10325,6 +10325,974 @@ mod tests { ); } + #[test] + fn test_merge_common_custom_properties() { + test( + r#" + html { + --foreground: #000; + --foreground-rgb: 0,0,0; + --background: #fff; + } + + html[data-theme="dark"] { + --foreground: #000; + --foreground-rgb: 0,0,0; + --background: #222; + } + "#, + indoc! {r#" + html { + --background: #fff; + } + + html, html[data-theme="dark"] { + --foreground: #000; + --foreground-rgb: 0,0,0; + } + + html[data-theme="dark"] { + --background: #222; + } + "#}, + ); + + // Either original rule may become empty after extraction. + test( + r#" + .a { + --shared: long-value; + } + + .b { + --shared: long-value; + color: red; + } + "#, + indoc! {r#" + .a, .b { + --shared: long-value; + } + + .b { + color: red; + } + "#}, + ); + + test( + r#" + .a { + --shared: long-value; + color: red; + } + + .b { + --shared: long-value; + } + "#, + indoc! {r#" + .a { + color: red; + } + + .a, .b { + --shared: long-value; + } + "#}, + ); + + // Shared declarations retain their importance. + test( + r#" + .a { + --shared: long-value !important; + color: red; + } + + .b { + --shared: long-value !important; + color: blue; + } + "#, + indoc! {r#" + .a { + color: red; + } + + .a, .b { + --shared: long-value !important; + } + + .b { + color: #00f; + } + "#}, + ); + + // Variable references do not depend on declaration order. + test( + r#" + .a { + --shared: var(--color); + --color: red; + } + + .b { + --color: blue; + --shared: var(--color); + } + "#, + indoc! {r#" + .a { + --color: red; + } + + .a, .b { + --shared: var(--color); + } + + .b { + --color: blue; + } + "#}, + ); + + // The all shorthand does not reset custom properties. + test( + r#" + .a { + all: initial; + --shared: long-value; + } + + .b { + all: unset; + --shared: long-value; + } + "#, + indoc! {r#" + .a { + all: initial; + } + + .a, .b { + --shared: long-value; + } + + .b { + all: unset; + } + "#}, + ); + + // Rules can share declarations within the same conditional block. + test( + r#" + @media print { + .a { + --shared: long-value; + color: red; + } + + .b { + --shared: long-value; + color: blue; + } + } + "#, + indoc! {r#" + @media print { + .a { + color: red; + } + + .a, .b { + --shared: long-value; + } + + .b { + color: #00f; + } + } + "#}, + ); + + // Factor when the serialized declaration saves one byte over the extra selectors. + test( + r#" + .a { + --x: ab; + color: red; + } + + .b { + --x: ab; + color: green; + } + "#, + indoc! {r#" + .a { + color: red; + } + + .a, .b { + --x: ab; + } + + .b { + color: green; + } + "#}, + ); + } + + #[test] + fn test_merge_common_custom_properties_boundaries() { + for source in [ + // Names are case-sensitive. + indoc! {r#" + .a { + --shared: long-value; + color: red; + } + + .b { + --SHARED: long-value; + color: green; + } + "#}, + // Values must match. + indoc! {r#" + .a { + --shared: long-value; + color: red; + } + + .b { + --shared: other-value; + color: green; + } + "#}, + // Importance must match. + indoc! {r#" + .a { + color: red; + --shared: long-value !important; + } + + .b { + --shared: long-value; + color: green; + } + "#}, + // Repeated selectors would cost more than the shared declaration. + indoc! {r#" + .long-selector-a { + --x: 0; + color: red; + } + + .long-selector-b { + --x: 0; + color: green; + } + "#}, + // A tie is left unchanged. + indoc! {r#" + .a { + --x: 0; + color: red; + } + + .b { + --x: 0; + color: green; + } + "#}, + // Do not move declarations across intervening rules. + indoc! {r#" + .a { + --shared: long-value; + color: red; + } + + .c { + --shared: override; + } + + .b { + --shared: long-value; + color: green; + } + "#}, + // Do not move declarations across conditional boundaries. + indoc! {r#" + .a { + --shared: long-value; + color: red; + } + + @media print { + .b { + --shared: long-value; + color: green; + } + } + "#}, + // Ordinary declarations may have shorthand interactions. + indoc! {r#" + .a { + color: red; + padding-top: 20px; + } + + .b { + color: green; + padding-top: 20px; + } + "#}, + ] { + test(source, source); + } + + // Unsupported selectors must not invalidate the other rule in a selector list. + let source = indoc! {r#" + .a { + --shared: long-value; + color: red; + } + + .b:has(.c) { + --shared: long-value; + color: green; + } + "#}; + prefix_test( + source, + source, + Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }, + ); + + // CSS Modules can change selector and variable lengths when printing. + let source = indoc! {r#" + html { + --shared: long-value; + color: red; + } + + body { + --shared: long-value; + color: green; + } + "#}; + test_with_options( + source, + source, + ParserOptions { + css_modules: Some(Default::default()), + ..ParserOptions::default() + }, + ); + + // Nested selectors can expand to much longer selector lists when lowered. + for source in [ + indoc! {r#" + .parent { + &.a { + --shared: long-value; + color: red; + } + + &.b { + --shared: long-value; + color: green; + } + } + "#}, + indoc! {r#" + .parent { + :is(&, .a) { + --shared: long-value; + color: red; + } + + :is(&, .b) { + --shared: long-value; + color: green; + } + } + "#}, + ] { + test(source, source); + } + } + + #[test] + fn test_merge_common_custom_properties_revisit() { + // Adapted from cssnano's "should revisit a predecessor after replacing an adjacent pair". + let expected = indoc! {r#" + .a, .b { + --x: 1; + --color: red; + } + + .b { + --background: blue; + } + "#}; + test( + r#" + .a, .b { + --x: 1; + } + + .a { + --color: red; + } + + .b { + --color: red; + --background: blue; + } + "#, + expected, + ); + // Like cssnano's convergence test, a second pass must not find another merge. + test(expected, expected); + + // Extraction can also expose identical blocks of ordinary declarations. + let expected = indoc! {r#" + .a, .b { + color: red; + } + + .b, .c { + --shared: long-value; + } + + .c { + color: green; + } + "#}; + test( + r#" + .a { + color: red; + } + + .b { + --shared: long-value; + color: red; + } + + .c { + --shared: long-value; + color: green; + } + "#, + expected, + ); + test(expected, expected); + } + + #[test] + fn test_merge_common_custom_properties_selector_arguments() { + // Extend cssnano's unknown-selector and :host regression cases to nested arguments. + for source in [ + indoc! {r#" + .a { + --shared: very-long-shared-value; + color: red; + } + + :host(:nonsense) { + --shared: very-long-shared-value; + color: green; + } + "#}, + indoc! {r#" + .a { + --shared: very-long-shared-value; + color: red; + } + + ::slotted(:nonsense) { + --shared: very-long-shared-value; + color: green; + } + "#}, + indoc! {r#" + .a { + --shared: very-long-shared-value; + color: red; + } + + ::cue(:nonsense) { + --shared: very-long-shared-value; + color: green; + } + "#}, + indoc! {r#" + .a { + --shared: very-long-shared-value; + color: red; + } + + ::cue(.b .c) { + --shared: very-long-shared-value; + color: green; + } + "#}, + ] { + test(source, source); + } + } + + #[test] + fn test_merge_common_custom_properties_nested_has() { + // Nesting inside :has() must be detected just like nesting inside :is(). + let source = indoc! {r#" + .parent { + :has(&) { + --shared: very-long-shared-value; + color: red; + } + + :has(&.b) { + --shared: very-long-shared-value; + color: green; + } + } + "#}; + test(source, source); + } + + // Cases adapted from cssnano/postcss-merge-rules (MIT), with declarations + // changed to custom properties where appropriate: + // https://github.com/cssnano/cssnano/blob/13b2373173ce49c78b8a2015d8c3f6b025ccfc3e/packages/postcss-merge-rules/test/index.js + // + // Copyright (c) Ben Briggs (https://beneb.info) + // + // Permission is hereby granted, free of charge, to any person + // obtaining a copy of this software and associated documentation + // files (the "Software"), to deal in the Software without + // restriction, including without limitation the rights to use, + // copy, modify, merge, publish, distribute, sublicense, and/or sell + // copies of the Software, and to permit persons to whom the + // Software is furnished to do so, subject to the following + // conditions: + // + // The above copyright notice and this permission notice shall be + // included in all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + // OTHER DEALINGS IN THE SOFTWARE. + #[test] + fn test_merge_common_custom_properties_cssnano() { + // Different declaration order (cssnano test/index.js:40). + test( + r#" + h1 { + --color: red; + --line-height: 1.5; + --font-size: 2em; + } + + h2 { + --font-size: 2em; + --color: red; + --line-height: 1.5; + } + "#, + indoc! {r#" + h1, h2 { + --color: red; + --line-height: 1.5; + --font-size: 2em; + } + "#}, + ); + + // Partial merging across three rules (cssnano test/index.js:194). + test( + r#" + h1 { + --color: red; + --text-decoration: underline; + } + + h2 { + --text-decoration: underline; + --color: green; + } + + h3 { + --font-weight: bold; + --color: green; + } + "#, + indoc! {r#" + h1 { + --color: red; + } + + h1, h2 { + --text-decoration: underline; + } + + h2, h3 { + --color: green; + } + + h3 { + --font-weight: bold; + } + "#}, + ); + + // The middle rule becomes empty (cssnano test/index.js:226). + test( + r#" + .test-1 { + --margin-top: 10px; + --margin-bottom: 20px; + } + + .test-2 { + --margin-top: 10px; + } + + .another-test { + --margin-top: 10px; + --margin-bottom: 30px; + } + "#, + indoc! {r#" + .test-1 { + --margin-bottom: 20px; + } + + .test-1, .test-2, .another-test { + --margin-top: 10px; + } + + .another-test { + --margin-bottom: 30px; + } + "#}, + ); + + // Repeated declarations are already deduplicated by Lightning CSS (cssnano test/index.js:478). + test( + r#" + h1 { + --display: block; + --display: block; + } + + h2 { + --display: block; + --display: block; + } + "#, + indoc! {r#" + h1, h2 { + --display: block; + } + "#}, + ); + + // An overridden value must not be extracted (cssnano test/index.js:510). + test( + r#" + .a { + --font-family: Arial; + --font-family: Helvetica; + } + + .b { + --font-family: Arial; + } + "#, + indoc! {r#" + .a { + --font-family: Helvetica; + } + + .b { + --font-family: Arial; + } + "#}, + ); + + // Only the final matching value survives (cssnano test/index.js:646). + test( + r#" + .a { + --margin-left: 2px; + color: red; + } + + .b { + --margin-left: 2px; + --margin-left: 1px; + --margin-left: 2px; + } + "#, + indoc! {r#" + .a { + color: red; + } + + .a, .b { + --margin-left: 2px; + } + "#}, + ); + + // Custom properties with related-looking names are independent (cssnano test/propertyRelations.js). + test( + r#" + .a { + --font: serif; + --font-size: 12px; + color: red; + } + + .b { + --font: serif; + --font-size: 18px; + color: green; + } + "#, + indoc! {r#" + .a { + --font-size: 12px; + color: red; + } + + .a, .b { + --font: serif; + } + + .b { + --font-size: 18px; + color: green; + } + "#}, + ); + + // A predecessor merge preserves normal and important overrides independently. + test( + r#" + .a, .b { + --color: black; + --fallback: stable; + --color: green !important; + } + + .a { + --color: red; + --shared: long-value; + } + + .b { + --color: red; + --shared: long-value; + --background: blue; + } + "#, + indoc! {r#" + .a, .b { + --color: red; + --fallback: stable; + --shared: long-value; + --color: green !important; + } + + .b { + --background: blue; + } + "#}, + ); + + // Logical and physical declarations retain their order (cssnano test/index.js:1261). + test( + r#" + .a { + --shared: long-value; + margin-inline-start: 1px; + margin-top: 2px; + } + + .b { + --shared: long-value; + margin-inline-start: 3px; + margin-top: 4px; + } + "#, + indoc! {r#" + .a { + margin-inline-start: 1px; + margin-top: 2px; + } + + .a, .b { + --shared: long-value; + } + + .b { + margin-inline-start: 3px; + margin-top: 4px; + } + "#}, + ); + + // A forgiving selector argument may contain unsupported selectors. + test( + r#" + .a { + --shared: very-very-long-shared-value; + color: red; + } + + :is(:nonsense, .b) { + --shared: very-very-long-shared-value; + color: green; + } + "#, + indoc! {r#" + .a { + color: red; + } + + .a, :is(:nonsense, .b) { + --shared: very-very-long-shared-value; + } + + :is(:nonsense, .b) { + color: green; + } + "#}, + ); + } + + #[test] + fn test_merge_common_custom_properties_cssnano_boundaries() { + for source in [ + // The selector cost exceeds the saving (cssnano test/index.js:303). + indoc! {r#" + .test0 { + --color: red; + --border: none; + --margin: 0; + } + + .longlonglonglong { + --color: green; + --border: none; + --margin: 0; + } + "#}, + // Different container queries (cssnano test/index.js:164). + indoc! {r#" + @container (width >= 200px) { + .mobile { + --display: none; + } + } + + @container (width <= 100px) { + .notMobile { + --display: none; + } + } + "#}, + // Different nested contexts (cssnano test/index.js:1004). + indoc! {r#" + @media (width >= 48rem) { + .wrapper { + --display: block; + } + } + + @supports (display: flex) { + @media (width >= 48rem) { + .wrapper { + --display: flex; + } + } + } + "#}, + // Keyframes use a separate rule type (cssnano test/index.js:662). + indoc! {r#" + @keyframes foo { + 0% { + --shared: long-value; + --opacity: 0; + } + + to { + --shared: long-value; + --opacity: 1; + } + } + "#}, + // Different cascade layers must not share rules. + indoc! {r#" + @layer base { + .a { + --shared: long-value; + color: red; + } + } + + @layer theme { + .b { + --shared: long-value; + color: green; + } + } + "#}, + // Mixed vendor pseudo selectors (cssnano test/index.js:325). + indoc! {r#" + code :-ms-placeholder-shown { + --shared: very-long-shared-value; + color: red; + } + + code::-moz-placeholder { + --shared: very-long-shared-value; + color: green; + } + "#}, + // Unknown and known selectors (cssnano test/index.js:878). + indoc! {r#" + p { + --shared: long-value; + color: red; + } + + :nonsense { + --shared: long-value; + color: green; + } + "#}, + ] { + test(source, source); + } + } + #[test] fn test_merge_rules() { test( diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 5920215e..457214ba 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -70,10 +70,13 @@ use crate::dependencies::{Dependency, ImportDependency}; use crate::error::{MinifyError, ParserError, PrinterError, PrinterErrorKind}; use crate::parser::{parse_rule_list, parse_style_block, DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser}; use crate::prefixes::Feature; +use crate::printer::{Printer, PrinterOptions}; +use crate::properties::custom::CustomPropertyName; +use crate::properties::Property; use crate::rules::keyframes::KeyframesName; -use crate::selector::{is_compatible, is_equivalent, Component, Selector, SelectorList}; +use crate::selector::{has_nesting, is_compatible, is_equivalent, Component, Selector, SelectorList}; use crate::stylesheet::ParserOptions; -use crate::targets::{should_compile, TargetsWithSupportsScope}; +use crate::targets::{should_compile, Targets, TargetsWithSupportsScope}; use crate::traits::{AtRuleParser, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; @@ -1051,11 +1054,239 @@ impl<'i, T: Clone> CssRuleList<'i, T> { } } - self.0 = rules; + // The duplicate-rule keys borrow rules and their declarations. Finish using + // them before extracting declarations into new rules. + drop(style_rules); + self.0 = if context.css_modules { + rules + } else { + factor_custom_properties(rules, context.targets.current) + }; Ok(()) } } +fn factor_custom_properties<'i, T>(mut rules: Vec>, targets: Targets) -> Vec> { + if rules.len() < 2 { + return rules; + } + + let mut result = Vec::with_capacity(rules.len()); + rules.reverse(); + while let Some(mut rule) = rules.pop() { + if matches!(rule, CssRule::Ignored) { + continue; + } + + if let (Some(CssRule::Style(previous)), CssRule::Style(current)) = (result.last_mut(), &mut rule) { + if merge_after_custom_property_extraction(previous, current, targets) { + rules.push(result.pop().unwrap()); + continue; + } + if let Some(shared) = extract_common_custom_properties(previous, current, targets) { + let previous_is_empty = previous.is_empty(); + if !current.is_empty() { + rules.push(rule); + } + rules.push(CssRule::Style(shared)); + let previous = result.pop().unwrap(); + if !previous_is_empty { + rules.push(previous); + } + // A rewrite can enable a merge with the predecessor. Revisit only the + // changed neighborhood. Every rewrite strictly reduces serialized size. + continue; + } + } + result.push(rule); + } + result +} + +fn merge_after_custom_property_extraction<'i, T>( + previous: &mut StyleRule<'i, T>, + current: &mut StyleRule<'i, T>, + targets: Targets, +) -> bool { + let only_custom_properties = |declarations: &DeclarationBlock<'i>| { + declarations.iter().all(|(property, _)| { + matches!(property, Property::Custom(custom) if matches!(custom.name, CustomPropertyName::Custom(_))) + }) + }; + if (previous.selectors != current.selectors && previous.declarations != current.declarations) + || !previous.rules.0.is_empty() + || !current.rules.0.is_empty() + || previous.loc.source_index != current.loc.source_index + || (previous.vendor_prefix | current.vendor_prefix).intersects(!VendorPrefix::None) + || previous.selectors.0.iter().any(has_nesting) + || current.selectors.0.iter().any(has_nesting) + || !previous.is_compatible(targets) + || !current.is_compatible(targets) + { + return false; + } + + if previous.declarations == current.declarations { + if previous.selectors != current.selectors { + previous.selectors.0.extend(current.selectors.0.drain(..)); + } + previous.vendor_prefix |= current.vendor_prefix; + return true; + } + + if !(only_custom_properties(&previous.declarations) || only_custom_properties(¤t.declarations)) { + return false; + } + + // At least one block contains only custom properties, so ordinary properties + // cannot interact across the blocks. Resolve repeated custom properties within + // each importance level without re-running handlers that generate fallbacks. + for (previous, current) in [ + ( + &mut previous.declarations.declarations, + &mut current.declarations.declarations, + ), + ( + &mut previous.declarations.important_declarations, + &mut current.declarations.important_declarations, + ), + ] { + let mut indices = HashMap::new(); + for (index, property) in previous.iter().enumerate() { + if let Property::Custom(custom) = property { + indices.insert(custom.name.clone(), index); + } + } + for property in current.drain(..) { + if let Property::Custom(custom) = &property { + if let Some(index) = indices.get(&custom.name) { + previous[*index] = property; + continue; + } + indices.insert(custom.name.clone(), previous.len()); + } + previous.push(property); + } + } + previous.vendor_prefix |= current.vendor_prefix; + true +} + +fn extract_common_custom_properties<'i, T>( + previous: &mut StyleRule<'i, T>, + current: &mut StyleRule<'i, T>, + targets: Targets, +) -> Option> { + if !previous.rules.0.is_empty() + || !current.rules.0.is_empty() + || previous.loc.source_index != current.loc.source_index + || (previous.vendor_prefix | current.vendor_prefix).intersects(!VendorPrefix::None) + { + return None; + } + + // Custom properties have no shorthand interactions, and `all` does not reset + // them. Only equal names, values, and importance can move into the shared rule. + let mut properties = HashMap::new(); + for (property, important) in previous.declarations.iter() { + if let Property::Custom(custom) = property { + if matches!(custom.name, CustomPropertyName::Custom(_)) { + properties.insert((&custom.name, important), property); + } + } + } + if properties.is_empty() { + return None; + } + + let mut common = HashSet::new(); + let mut css = String::new(); + let mut printer = Printer::new( + &mut css, + PrinterOptions { + minify: true, + targets, + ..PrinterOptions::default() + }, + ); + for (property, important) in current.declarations.iter() { + if let Property::Custom(custom) = property { + if properties + .get(&(&custom.name, important)) + .is_some_and(|other| **other == *property) + { + property.to_css(&mut printer, important).ok()?; + printer.write_char(';').ok()?; + common.insert((custom.name.clone(), important)); + } + } + } + if common.is_empty() + || previous.selectors.0.iter().any(has_nesting) + || current.selectors.0.iter().any(has_nesting) + || !previous.is_compatible(targets) + || !current.is_compatible(targets) + { + return None; + } + + let shared_size = css.len() - 1; + let options = || PrinterOptions { + minify: true, + targets, + ..PrinterOptions::default() + }; + let previous_selectors_size = previous.selectors.to_css_string(options()).ok()?.len(); + let current_selectors_size = current.selectors.to_css_string(options()).ok()?.len(); + let previous_remains = previous.declarations.len() > common.len(); + let current_remains = current.declarations.len() > common.len(); + + // Account for the extra selector list and braces, and for semicolons removed + // from the original blocks. Do not factor when the minified output would grow. + let overhead = match (previous_remains, current_remains) { + (true, true) => previous_selectors_size + current_selectors_size + 1, + (false, true) => current_selectors_size, + (true, false) => previous_selectors_size, + (false, false) => 0, + }; + if shared_size <= overhead { + return None; + } + + let mut declarations = DeclarationBlock::new(); + for (previous, current, shared, important) in [ + ( + &mut previous.declarations.declarations, + &mut current.declarations.declarations, + &mut declarations.declarations, + false, + ), + ( + &mut previous.declarations.important_declarations, + &mut current.declarations.important_declarations, + &mut declarations.important_declarations, + true, + ), + ] { + let is_common = |property: &Property<'i>| match property { + Property::Custom(custom) => common.contains(&(custom.name.clone(), important)), + _ => false, + }; + shared.extend(previous.extract_if(.., |property| is_common(property))); + current.retain(|property| !is_common(property)); + } + + let mut selectors = previous.selectors.clone(); + selectors.0.extend(current.selectors.0.iter().cloned()); + Some(StyleRule { + selectors, + vendor_prefix: previous.vendor_prefix | current.vendor_prefix, + declarations, + rules: CssRuleList(vec![]), + loc: previous.loc, + }) +} + fn merge_style_rules<'i, T>( style: &mut StyleRule<'i, T>, last_style_rule: &mut StyleRule<'i, T>, diff --git a/src/selector.rs b/src/selector.rs index 956592ff..971e3a94 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -61,6 +61,29 @@ pub type SelectorList<'i> = parcel_selectors::SelectorList<'i, Selectors>; pub type Selector<'i> = parcel_selectors::parser::Selector<'i, Selectors>; /// An individual component within a selector. pub type Component<'i> = parcel_selectors::parser::Component<'i, Selectors>; + +/// Whether a selector contains nesting, including inside a functional pseudo class. +pub(crate) fn has_nesting(selector: &Selector) -> bool { + struct WithoutNesting; + impl<'i> parcel_selectors::visitor::SelectorVisitor<'i> for WithoutNesting { + type Impl = Selectors; + + fn visit_simple_selector(&mut self, component: &Component<'i>) -> bool { + match component { + Component::Nesting => false, + // SelectorVisitor does not descend into these arguments by default. + Component::Has(selectors) | Component::Any(_, selectors) => selectors.iter().all(|s| s.visit(self)), + Component::PseudoElement( + PseudoElement::CueFunction { selector } | PseudoElement::CueRegionFunction { selector }, + ) => Selector::visit(selector, self), + _ => true, + } + } + } + + !selector.visit(&mut WithoutNesting) +} + /// A combinator. pub use parcel_selectors::parser::Combinator; @@ -2101,7 +2124,15 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { continue; } - Component::Scope | Component::Host(_) | Component::Slotted(_) => Feature::Shadowdomv1, + Component::Host(Some(selector)) | Component::Slotted(selector) => { + // These arguments are not forgiving: an unsupported selector inside + // the function invalidates the entire outer selector list as well. + if !is_compatible(std::slice::from_ref(selector), targets) { + return false; + } + Feature::Shadowdomv1 + } + Component::Scope | Component::Host(None) => Feature::Shadowdomv1, Component::Part(_) => Feature::PartPseudo, @@ -2182,7 +2213,15 @@ pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool { PseudoElement::Marker => Feature::MarkerPseudo, PseudoElement::Backdrop(prefix) if *prefix == VendorPrefix::None => Feature::Dialog, PseudoElement::Cue => Feature::Cue, - PseudoElement::CueFunction { selector: _ } => Feature::CueFunction, + PseudoElement::CueFunction { selector } => { + if selector.has_combinator() + || selector.has_pseudo_element() + || !is_compatible(std::slice::from_ref(selector), targets) + { + return false; + } + Feature::CueFunction + } PseudoElement::ViewTransition | PseudoElement::ViewTransitionNew { .. } | PseudoElement::ViewTransitionOld { .. }