From caf6d830a8d7ae5ddc32234b027f455c9ac11efe Mon Sep 17 00:00:00 2001 From: David Shin Date: Thu, 30 May 2024 15:23:41 +0000 Subject: [PATCH] Bug 1886441: Part 3 - Implement substitutions in `@scope`. r=firefox-style-system-reviewers,emilio `:scope` gets implicitly added if not present, without contributing specificity (See https://github.com/w3c/csswg-drafts/issues/10196). `&` is replaced with `scope-start` selector, or `:scope` if it not specified. Differential Revision: https://phabricator.services.mozilla.com/D207780 --- servo/components/malloc_size_of/lib.rs | 1 + servo/components/selectors/builder.rs | 47 ++++++-- servo/components/selectors/matching.rs | 2 +- servo/components/selectors/parser.rs | 111 +++++++++++++++--- servo/components/style/stylist.rs | 83 ++++++++++++- .../css-cascade/scope-specificity.html.ini | 25 ++-- .../css/css-cascade/at-scope-parsing.html | 6 +- .../css-cascade/at-scope-relative-syntax.html | 5 +- .../css/css-cascade/scope-specificity.html | 25 +++- 9 files changed, 257 insertions(+), 48 deletions(-) diff --git a/servo/components/malloc_size_of/lib.rs b/servo/components/malloc_size_of/lib.rs index 32d7f03bfbe1..887a85e97dc0 100644 --- a/servo/components/malloc_size_of/lib.rs +++ b/servo/components/malloc_size_of/lib.rs @@ -791,6 +791,7 @@ where Component::Root | Component::Empty | Component::Scope | + Component::ImplicitScope | Component::ParentSelector | Component::Nth(..) | Component::Host(None) | diff --git a/servo/components/selectors/builder.rs b/servo/components/selectors/builder.rs index c70655418512..a8e3e6d6ed64 100644 --- a/servo/components/selectors/builder.rs +++ b/servo/components/selectors/builder.rs @@ -97,17 +97,19 @@ impl SelectorBuilder { mut spec: SpecificityAndFlags, parse_relative: ParseRelative, ) -> ThinArc> { - let implicit_parent = parse_relative.needs_implicit_parent_selector() && - !spec.flags.contains(SelectorFlags::HAS_PARENT); - - let parent_selector_and_combinator; - let implicit_parent = if implicit_parent { - spec.flags.insert(SelectorFlags::HAS_PARENT); - parent_selector_and_combinator = [ + let implicit_addition = match parse_relative { + ParseRelative::ForNesting if !spec.flags.intersects(SelectorFlags::HAS_PARENT) => Some((Component::ParentSelector, SelectorFlags::HAS_PARENT)), + ParseRelative::ForScope if !spec.flags.intersects(SelectorFlags::HAS_SCOPE | SelectorFlags::HAS_PARENT) => Some((Component::ImplicitScope, SelectorFlags::HAS_SCOPE)), + _ => None, + }; + let implicit_selector_and_combinator; + let implicit_selector = if let Some((component, flag)) = implicit_addition { + spec.flags.insert(flag); + implicit_selector_and_combinator = [ Component::Combinator(Combinator::Descendant), - Component::ParentSelector, + component, ]; - &parent_selector_and_combinator[..] + &implicit_selector_and_combinator[..] } else { &[] }; @@ -115,11 +117,11 @@ impl SelectorBuilder { // As an optimization, for a selector without combinators, we can just keep the order // as-is. if self.last_compound_start.is_none() { - return Arc::from_header_and_iter(spec, ExactChain(self.components.drain(..), implicit_parent.iter().cloned())); + return Arc::from_header_and_iter(spec, ExactChain(self.components.drain(..), implicit_selector.iter().cloned())); } self.reverse_last_compound(); - Arc::from_header_and_iter(spec, ExactChain(self.components.drain(..).rev(), implicit_parent.iter().cloned())) + Arc::from_header_and_iter(spec, ExactChain(self.components.drain(..).rev(), implicit_selector.iter().cloned())) } } @@ -178,6 +180,7 @@ bitflags! { const HAS_PARENT = 1 << 3; const HAS_NON_FEATURELESS_COMPONENT = 1 << 4; const HAS_HOST = 1 << 5; + const HAS_SCOPE = 1 << 6; } } @@ -221,6 +224,18 @@ pub(crate) struct Specificity { element_selectors: u32, } +impl Specificity { + // Return the specficity of a single class-like selector. + #[inline] + pub fn single_class_like() -> Self { + Specificity { + id_selectors: 0, + class_like_selectors: 1, + element_selectors: 0, + } + } +} + impl From for Specificity { #[inline] fn from(value: u32) -> Specificity { @@ -311,10 +326,18 @@ where Component::Root | Component::Empty | Component::Scope | + Component::ImplicitScope | Component::Nth(..) | Component::NonTSPseudoClass(..) => { + if matches!(*simple_selector, Component::Scope | Component::ImplicitScope) { + flags.insert(SelectorFlags::HAS_SCOPE); + } flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); - specificity.class_like_selectors += 1; + if !matches!(*simple_selector, Component::ImplicitScope) { + // Implicit :scope does not add specificity. See + // https://github.com/w3c/csswg-drafts/issues/10196 + specificity.class_like_selectors += 1; + } }, Component::NthOf(ref nth_of_data) => { // https://drafts.csswg.org/selectors/#specificity-rules: diff --git a/servo/components/selectors/matching.rs b/servo/components/selectors/matching.rs index 6eaafb303865..25aedd47d988 100644 --- a/servo/components/selectors/matching.rs +++ b/servo/components/selectors/matching.rs @@ -1194,7 +1194,7 @@ where Component::Host(ref selector) => { return matches_host(element, selector.as_ref(), &mut context.shared, rightmost); }, - Component::ParentSelector | Component::Scope => match context.shared.scope_element { + Component::ParentSelector | Component::Scope | Component::ImplicitScope => match context.shared.scope_element { Some(ref scope_element) => element.opaque() == *scope_element, None => element.is_root(), }, diff --git a/servo/components/selectors/parser.rs b/servo/components/selectors/parser.rs index 025ac2ca57b2..f3a28849f1ce 100644 --- a/servo/components/selectors/parser.rs +++ b/servo/components/selectors/parser.rs @@ -445,19 +445,17 @@ pub enum ParseRelative { No, } -impl ParseRelative { - #[inline] - pub(crate) fn needs_implicit_parent_selector(self) -> bool { - matches!(self, Self::ForNesting) - } -} - impl SelectorList { /// Returns a selector list with a single `&` pub fn ampersand() -> Self { Self::from_one(Selector::ampersand()) } + /// Returns a selector list with a single `:scope` + pub fn scope() -> Self { + Self::from_one(Selector::scope()) + } + /// Parse a comma-separated list of Selectors. /// /// @@ -760,6 +758,16 @@ impl Selector { )) } + fn scope() -> Self { + Self(ThinArc::from_header_and_iter( + SpecificityAndFlags { + specificity: Specificity::single_class_like().into(), + flags: SelectorFlags::HAS_SCOPE, + }, + std::iter::once(Component::Scope), + )) + } + #[inline] pub fn specificity(&self) -> u32 { self.0.header.specificity @@ -780,6 +788,11 @@ impl Selector { self.flags().intersects(SelectorFlags::HAS_PARENT) } + #[inline] + pub fn has_scope_selector(&self) -> bool { + self.flags().intersects(SelectorFlags::HAS_SCOPE) + } + #[inline] pub fn is_slotted(&self) -> bool { self.flags().intersects(SelectorFlags::HAS_SLOTTED) @@ -1081,6 +1094,7 @@ impl Selector { Root | Empty | Scope | + ImplicitScope | Nth(..) | NonTSPseudoClass(..) | PseudoElement(..) | @@ -1924,6 +1938,14 @@ pub enum Component { Root, Empty, Scope, + /// :scope added implicitly into scoped rules (i.e. In `@scope`) not + /// explicitly using `:scope` or `&` selectors. + /// + /// https://drafts.csswg.org/css-cascade-6/#scoped-rules + /// + /// Unlike the normal `:scope` selector, this does not add any specificity. + /// See https://github.com/w3c/csswg-drafts/issues/10196 + ImplicitScope, ParentSelector, Nth(NthSelectorData), NthOf(NthOfSelectorData), @@ -2252,13 +2274,14 @@ impl ToCss for Selector { debug_assert!(!combinators_exhausted); // https://drafts.csswg.org/cssom/#serializing-selectors - if compound.is_empty() { - continue; - } - if let Component::RelativeSelectorAnchor = compound.first().unwrap() { + let first_compound = match compound.first() { + None => continue, + Some(c) => c, + }; + if matches!(first_compound, Component::RelativeSelectorAnchor | Component::ImplicitScope) { debug_assert!( compound.len() == 1, - "RelativeLeft should only be a simple selector" + "RelativeSelectorAnchor/ImplicitScope should only be a simple selector" ); combinators.next().unwrap().to_css_relative(dest)?; continue; @@ -2530,7 +2553,7 @@ impl ToCss for Component { }, NonTSPseudoClass(ref pseudo) => pseudo.to_css(dest), Invalid(ref css) => dest.write_str(css), - RelativeSelectorAnchor => Ok(()), + RelativeSelectorAnchor | ImplicitScope => Ok(()), } } } @@ -2613,7 +2636,10 @@ where let selector = match parse_relative { ParseRelative::ForHas | ParseRelative::No => unreachable!(), ParseRelative::ForNesting => Component::ParentSelector, - ParseRelative::ForScope => Component::Scope, + // See https://github.com/w3c/csswg-drafts/issues/10196 + // Implicitly added `:scope` does not add specificity + // for non-relative selectors, so do the same. + ParseRelative::ForScope => Component::ImplicitScope, }; builder.push_simple_selector(selector); builder.push_combinator(combinator); @@ -4442,6 +4468,63 @@ pub mod tests { assert_eq!(iter.next_sequence(), None); } + #[test] + fn test_parse_implicit_scope() { + assert_eq!( + parse_relative_expected(".foo", ParseRelative::ForScope, None).unwrap(), + SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ImplicitScope, + Component::Combinator(Combinator::Descendant), + Component::Class(DummyAtom::from("foo")), + ], + specificity(0, 1, 0), + SelectorFlags::HAS_SCOPE | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )]) + ); + + assert_eq!( + parse_relative_expected(":scope .foo", ParseRelative::ForScope, None).unwrap(), + SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Scope, + Component::Combinator(Combinator::Descendant), + Component::Class(DummyAtom::from("foo")), + ], + specificity(0, 2, 0), + SelectorFlags::HAS_SCOPE | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )]) + ); + + assert_eq!( + parse_relative_expected("> .foo", ParseRelative::ForScope, Some("> .foo")).unwrap(), + SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ImplicitScope, + Component::Combinator(Combinator::Child), + Component::Class(DummyAtom::from("foo")), + ], + specificity(0, 1, 0), + SelectorFlags::HAS_SCOPE | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )]) + ); + + assert_eq!( + parse_relative_expected(".foo :scope > .bar", ParseRelative::ForScope, None).unwrap(), + SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Class(DummyAtom::from("foo")), + Component::Combinator(Combinator::Descendant), + Component::Scope, + Component::Combinator(Combinator::Child), + Component::Class(DummyAtom::from("bar")), + ], + specificity(0, 3, 0), + SelectorFlags::HAS_SCOPE | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )]) + ); + } + struct TestVisitor { seen: Vec, } diff --git a/servo/components/style/stylist.rs b/servo/components/style/stylist.rs index 599deb5e6e4f..c670bf838c0a 100644 --- a/servo/components/style/stylist.rs +++ b/servo/components/style/stylist.rs @@ -39,6 +39,7 @@ use crate::stylesheets::container_rule::ContainerCondition; use crate::stylesheets::import_rule::ImportLayer; use crate::stylesheets::keyframes_rule::KeyframesAnimation; use crate::stylesheets::layer_rule::{LayerName, LayerOrder}; +use crate::stylesheets::scope_rule::ScopeBounds; #[cfg(feature = "gecko")] use crate::stylesheets::{ CounterStyleRule, FontFaceRule, FontFeatureValuesRule, FontPaletteValuesRule, @@ -578,12 +579,13 @@ impl From for RuleInclusion { } /// A struct containing state from ancestor rules like @layer / @import / -/// @container / nesting. +/// @container / nesting / @scope. struct ContainingRuleState { layer_name: LayerName, layer_id: LayerId, container_condition_id: ContainerConditionId, in_starting_style: bool, + scope_condition_id: ScopeConditionId, ancestor_selector_lists: SmallVec<[SelectorList; 2]>, } @@ -595,6 +597,7 @@ impl Default for ContainingRuleState { container_condition_id: ContainerConditionId::none(), in_starting_style: false, ancestor_selector_lists: Default::default(), + scope_condition_id: ScopeConditionId::none(), } } } @@ -605,6 +608,7 @@ struct SavedContainingRuleState { layer_id: LayerId, container_condition_id: ContainerConditionId, in_starting_style: bool, + scope_condition_id: ScopeConditionId, } impl ContainingRuleState { @@ -615,6 +619,7 @@ impl ContainingRuleState { layer_id: self.layer_id, container_condition_id: self.container_condition_id, in_starting_style: self.in_starting_style, + scope_condition_id: self.scope_condition_id, } } @@ -627,6 +632,7 @@ impl ContainingRuleState { self.layer_id = saved.layer_id; self.container_condition_id = saved.container_condition_id; self.in_starting_style = saved.in_starting_style; + self.scope_condition_id = saved.scope_condition_id; } } @@ -2362,6 +2368,23 @@ impl ScopeConditionId { } } +#[derive(Clone, Debug, MallocSizeOf)] +struct ScopeConditionReference { + parent: ScopeConditionId, + // TODO(dshin): With replaced parent selectors, these may be unique... + #[ignore_malloc_size_of = "Arc inside"] + condition: Option, +} + +impl ScopeConditionReference { + const fn none() -> Self { + Self { + parent: ScopeConditionId::none(), + condition: None, + } + } +} + /// Data resulting from performing the CSS cascade that is specific to a given /// origin. /// @@ -2468,6 +2491,9 @@ pub struct CascadeData { /// The list of container conditions, indexed by their id. container_conditions: SmallVec<[ContainerConditionReference; 1]>, + /// The list of scope conditions, indexed by their id. + scope_conditions: SmallVec<[ScopeConditionReference; 1]>, + /// Effective media query results cached from the last rebuild. effective_media_query_results: EffectiveMediaQueryResults, @@ -2485,6 +2511,22 @@ pub struct CascadeData { num_declarations: usize, } +fn parent_selector_for_scope(parent: Option<&SelectorList>) -> &SelectorList { + lazy_static! { + static ref SCOPE: SelectorList = { + let list = SelectorList::scope(); + list.slice() + .iter() + .for_each(|selector| selector.mark_as_intentionally_leaked()); + list + }; + }; + match parent { + Some(l) => l, + None => &SCOPE, + } +} + impl CascadeData { /// Creates an empty `CascadeData`. pub fn new() -> Self { @@ -2510,6 +2552,7 @@ impl CascadeData { layer_id: Default::default(), layers: smallvec::smallvec![CascadeLayer::root()], container_conditions: smallvec::smallvec![ContainerConditionReference::none()], + scope_conditions: smallvec::smallvec![ScopeConditionReference::none()], extra_data: ExtraStyleData::default(), effective_media_query_results: EffectiveMediaQueryResults::new(), rules_source_order: 0, @@ -2867,7 +2910,11 @@ impl CascadeData { debug_assert!(!has_nested_rules); debug_assert_eq!(stylesheet.contents().origin, Origin::UserAgent); debug_assert_eq!(containing_rule_state.layer_id, LayerId::root()); - // TODO(dshin, bug 1886441): Because we precompute pseudos, we cannot possibly calculate scope proximity. + // Because we precompute pseudos, we cannot possibly calculate scope proximity. + debug_assert_eq!( + containing_rule_state.scope_condition_id, + ScopeConditionId::none() + ); precomputed_pseudo_element_decls .as_mut() .expect("Expected precomputed declarations for the UA level") @@ -2902,7 +2949,7 @@ impl CascadeData { containing_rule_state.layer_id, containing_rule_state.container_condition_id, containing_rule_state.in_starting_style, - ScopeConditionId::none(), + containing_rule_state.scope_condition_id, ); if collect_replaced_selectors { @@ -2968,6 +3015,10 @@ impl CascadeData { // NOTE(emilio): It's fine to look at :host and then at // ::slotted(..), since :host::slotted(..) could never // possibly match, as is not a valid shadow host. + // TODO(dshin, bug 1886441): If we have a featureless + // `:scope`, we need to add to `host_rules` (Which should + // be then rennamed `featureless_rules`). + // See https://github.com/w3c/csswg-drafts/issues/9025 let rules = if rule .selector .is_featureless_host_selector_or_pseudo_element() @@ -3194,6 +3245,30 @@ impl CascadeData { CssRule::StartingStyle(..) => { containing_rule_state.in_starting_style = true; }, + CssRule::Scope(ref rule) => { + let id = ScopeConditionId(self.scope_conditions.len() as u16); + let replaced = { + let start = rule.bounds.start.as_ref().map(|selector| { + match containing_rule_state.ancestor_selector_lists.last() { + Some(s) => selector.replace_parent_selector(s), + None => selector.clone(), + } + }); + let end = rule.bounds + .end + .as_ref() + .map(|selector| selector.replace_parent_selector( + parent_selector_for_scope(start.as_ref()))); + containing_rule_state.ancestor_selector_lists.push(parent_selector_for_scope(start.as_ref()).clone()); + ScopeBounds { start, end } + }; + + self.scope_conditions.push(ScopeConditionReference { + parent: containing_rule_state.scope_condition_id, + condition: Some(replaced), + }); + containing_rule_state.scope_condition_id = id; + }, // We don't care about any other rule. _ => {}, } @@ -3386,6 +3461,8 @@ impl CascadeData { self.container_conditions.clear(); self.container_conditions .push(ContainerConditionReference::none()); + self.scope_conditions.clear(); + self.scope_conditions.push(ScopeConditionReference::none()); self.extra_data.clear(); self.rules_source_order = 0; self.num_selectors = 0; diff --git a/testing/web-platform/meta/css/css-cascade/scope-specificity.html.ini b/testing/web-platform/meta/css/css-cascade/scope-specificity.html.ini index f2161e653092..7c2007a1c487 100644 --- a/testing/web-platform/meta/css/css-cascade/scope-specificity.html.ini +++ b/testing/web-platform/meta/css/css-cascade/scope-specificity.html.ini @@ -1,24 +1,33 @@ [scope-specificity.html] - [@scope (#main) { .b { } }] + [@scope (#main) { .b { } } and .b] expected: FAIL - [@scope (#main) to (.b) { .a { } }] + [@scope (#main) to (.b) { .a { } } and .a] expected: FAIL - [@scope (#main, .foo, .bar) { #a { } }] + [@scope (#main, .foo, .bar) { #a { } } and #a] expected: FAIL - [@scope (#main) { div.b { } }] + [@scope (#main) { div.b { } } and div.b] expected: FAIL - [@scope (#main) { :scope .b { } }] + [@scope (#main) { :scope .b { } } and .a .b] expected: FAIL - [@scope (#main) { & .b { } }] + [@scope (#main) { & .b { } } and #main .b] expected: FAIL - [@scope (#main) { div .b { } }] + [@scope (#main) { div .b { } } and div .b] expected: FAIL - [@scope (#main) { @scope (.a) { .b { } } }] + [@scope (#main) { @scope (.a) { .b { } } } and .b] + expected: FAIL + + [@scope (#main) { :scope .b { } } and :scope .b] + expected: FAIL + + [@scope { & .b { } } and :scope .b] + expected: FAIL + + [@scope (#main) { > .a { } } and :where(#main) > .a] expected: FAIL diff --git a/testing/web-platform/tests/css/css-cascade/at-scope-parsing.html b/testing/web-platform/tests/css/css-cascade/at-scope-parsing.html index 8390738dd8a8..e984c1dcc29e 100644 --- a/testing/web-platform/tests/css/css-cascade/at-scope-parsing.html +++ b/testing/web-platform/tests/css/css-cascade/at-scope-parsing.html @@ -42,9 +42,9 @@ test_valid('@scope to (.a)'); test_valid('@scope (.a) to (&)'); test_valid('@scope (.a) to (& > &)'); - test_valid('@scope (.a) to (> .b)', '@scope (.a) to (:scope > .b)'); - test_valid('@scope (.a) to (+ .b)', '@scope (.a) to (:scope + .b)'); - test_valid('@scope (.a) to (~ .b)', '@scope (.a) to (:scope ~ .b)'); + test_valid('@scope (.a) to (> .b)'); + test_valid('@scope (.a) to (+ .b)'); + test_valid('@scope (.a) to (~ .b)'); test_valid('@scope ()', '@scope'); test_valid('@scope to ()', '@scope'); test_valid('@scope () to ()', '@scope'); diff --git a/testing/web-platform/tests/css/css-cascade/at-scope-relative-syntax.html b/testing/web-platform/tests/css/css-cascade/at-scope-relative-syntax.html index 274d9afbebec..8b0e41ab13e1 100644 --- a/testing/web-platform/tests/css/css-cascade/at-scope-relative-syntax.html +++ b/testing/web-platform/tests/css/css-cascade/at-scope-relative-syntax.html @@ -2,6 +2,7 @@ @scope and Nesting: Parsing inner style rules with relative selector syntax +
@@ -60,9 +61,9 @@ for (const method of Object.keys(create_method)) { test_inner(['@scope' , '.nest'], method, '> .foo', '& > .foo'); - test_inner(['.nest', '@scope'], method, '> .foo', ':scope > .foo'); + test_inner(['.nest', '@scope'], method, '> .foo'); test_inner(['@scope' , '.nest', '@media screen'], method, '> .foo', '& > .foo'); - test_inner(['.nest', '@scope', '@media screen'], method, '> .foo', ':scope > .foo'); + test_inner(['.nest', '@scope', '@media screen'], method, '> .foo'); } diff --git a/testing/web-platform/tests/css/css-cascade/scope-specificity.html b/testing/web-platform/tests/css/css-cascade/scope-specificity.html index 0f48c605a852..fa103ff3742f 100644 --- a/testing/web-platform/tests/css/css-cascade/scope-specificity.html +++ b/testing/web-platform/tests/css/css-cascade/scope-specificity.html @@ -1,11 +1,12 @@ -@scope - specificty +@scope - specificity
+
@@ -36,8 +37,13 @@ function format_scoped_rule(scoped_selector, declarations) { // Verify that the specificity of 'scoped_selector' is the same // as the specificity of 'ref_selector'. Both selectors must select // an element within #main. -function test_scope_specificity(scoped_selector, ref_selector) { - test(() => { +function test_scope_specificity(scoped_selector, ref_selector, style) { + if (style === undefined) { + style = document.getElementById("style"); + } + test(t => { + t.add_cleanup(() => { style.textContent = ''; }); + let element = main.querySelector(ref_selector); assert_not_equals(element, null); @@ -62,16 +68,25 @@ function test_scope_specificity(scoped_selector, ref_selector) { // cause the unscoped rule to win instead. style.textContent = `div${ref_rule} ${scoped_rule}`; assert_equals(getComputedStyle(element).zIndex, '2', 'unscoped + scoped'); - }, format_scoped_rule(scoped_selector, '')); + }, format_scoped_rule(scoped_selector, '') + ' and ' + ref_selector); } +// Selectors within @scope implicitly have `:scope ` +// added, but no specificity associated with it is added. +// See https://github.com/w3c/csswg-drafts/issues/10196 test_scope_specificity(['@scope (#main)', '.b'], '.b'); test_scope_specificity(['@scope (#main) to (.b)', '.a'], '.a'); test_scope_specificity(['@scope (#main, .foo, .bar)', '#a'], '#a'); test_scope_specificity(['@scope (#main)', 'div.b'], 'div.b'); test_scope_specificity(['@scope (#main)', ':scope .b'], '.a .b'); +// Inherit the specificity of the scope-start selector. test_scope_specificity(['@scope (#main)', '& .b'], '#main .b'); test_scope_specificity(['@scope (#main)', 'div .b'], 'div .b'); test_scope_specificity(['@scope (#main)', '@scope (.a)', '.b'], '.b'); - +// Explicit `:scope` adds specficity. +test_scope_specificity(['@scope (#main)', ':scope .b'], ':scope .b'); +// Using & in scoped style with implicit scope root uses `:scope`, which adds specificity +test_scope_specificity(['@scope', '& .b'], ':scope .b', styleImplicit); +// Using relative selector syntax does not add specificity +test_scope_specificity(['@scope (#main)', '> .a'], ':where(#main) > .a');