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
This commit is contained in:
David Shin 2024-05-30 15:23:41 +00:00
Родитель 336fcf3956
Коммит caf6d830a8
9 изменённых файлов: 257 добавлений и 48 удалений

Просмотреть файл

@ -791,6 +791,7 @@ where
Component::Root |
Component::Empty |
Component::Scope |
Component::ImplicitScope |
Component::ParentSelector |
Component::Nth(..) |
Component::Host(None) |

Просмотреть файл

@ -97,17 +97,19 @@ impl<Impl: SelectorImpl> SelectorBuilder<Impl> {
mut spec: SpecificityAndFlags,
parse_relative: ParseRelative,
) -> ThinArc<SpecificityAndFlags, Component<Impl>> {
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<Impl: SelectorImpl> SelectorBuilder<Impl> {
// 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<u32> 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:

Просмотреть файл

@ -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(),
},

Просмотреть файл

@ -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<Impl: SelectorImpl> SelectorList<Impl> {
/// 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.
/// <https://drafts.csswg.org/selectors/#grouping>
///
@ -760,6 +758,16 @@ impl<Impl: SelectorImpl> Selector<Impl> {
))
}
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<Impl: SelectorImpl> Selector<Impl> {
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<Impl: SelectorImpl> Selector<Impl> {
Root |
Empty |
Scope |
ImplicitScope |
Nth(..) |
NonTSPseudoClass(..) |
PseudoElement(..) |
@ -1924,6 +1938,14 @@ pub enum Component<Impl: SelectorImpl> {
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<Impl>),
@ -2252,13 +2274,14 @@ impl<Impl: SelectorImpl> ToCss for Selector<Impl> {
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<Impl: SelectorImpl> ToCss for Component<Impl> {
},
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<String>,
}

Просмотреть файл

@ -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<StyleRuleInclusion> 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<SelectorImpl>; 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<ScopeBounds>,
}
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<SelectorImpl>>) -> &SelectorList<SelectorImpl> {
lazy_static! {
static ref SCOPE: SelectorList<SelectorImpl> = {
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 <slot> 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;

Просмотреть файл

@ -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

Просмотреть файл

@ -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');

Просмотреть файл

@ -2,6 +2,7 @@
<title>@scope and Nesting: Parsing inner style rules with relative selector syntax</title>
<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scoped-rules">
<link rel="help" href="https://drafts.csswg.org/css-nesting/#nesting">
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/10196">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<main id=main></main>
@ -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');
}
</script>

Просмотреть файл

@ -1,11 +1,12 @@
<!DOCTYPE html>
<title>@scope - specificty</title>
<title>@scope - specificity</title>
<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style id=style>
</style>
<main id=main>
<style id=styleImplicit></style>
<div id=a class=a>
<div id=b class=b>
</div>
@ -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 <descendant-combinator>`
// 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');
</script>