Bug 1886441: Part 6 - Implement scoped styles. r=firefox-style-system-reviewers,emilio

Differential Revision: https://phabricator.services.mozilla.com/D207783
This commit is contained in:
David Shin 2024-05-30 15:23:42 +00:00
Родитель 536dcf63c5
Коммит f4188d2589
19 изменённых файлов: 257 добавлений и 249 удалений

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

@ -391,12 +391,18 @@ impl SelectorMap<Rule> {
stylist,
include_starting_style
);
} else if let Some(scopes) = cascade_data.scope_condition_matches(
rule.scope_condition_id,
stylist,
element,
matching_context,
) {
} else {
// First element contains the closest matching scope, but the selector may match
// a scope root that is further out (e.g. `.foo > .foo .bar > .baz` for `scope-start`
// selector `.foo` and inner selector `.bar .baz`). This requires returning all
// potential scopes.
let scopes = cascade_data.scope_condition_matches(
rule.scope_condition_id,
stylist,
element,
matching_context,
);
// We had to gather scope roots first, since the scope element must change before the rule's
// selector is tested. Candidates are sorted in proximity.
for candidate in scopes {

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

@ -6,6 +6,8 @@
//!
//! [scope]: https://drafts.csswg.org/css-cascade-6/#scoped-styles
use crate::dom::TElement;
use crate::applicable_declarations::ScopeProximity;
use crate::parser::ParserContext;
use crate::selector_parser::{SelectorImpl, SelectorParser};
use crate::shared_lock::{
@ -16,7 +18,9 @@ use crate::stylesheets::CssRules;
use cssparser::{Parser, SourceLocation, ToCss};
#[cfg(feature = "gecko")]
use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalSizeOf, MallocUnconditionalShallowSizeOf};
use selectors::parser::{ParseRelative, SelectorList};
use selectors::context::MatchingContext;
use selectors::matching::{matches_selector, matches_selector_list};
use selectors::parser::{ParseRelative, Selector, SelectorList};
use selectors::OpaqueElement;
use servo_arc::Arc;
use std::fmt::{self, Write};
@ -183,4 +187,120 @@ impl ImplicitScopeRoot {
Self::ShadowHost(..) | Self::Constructed => true,
}
}
/// Return the scope root element, given the element to be styled.
pub fn element(&self, current_host: Option<OpaqueElement>) -> Option<OpaqueElement> {
match self {
Self::InLightTree(e) |
Self::InShadowTree(e) |
Self::ShadowHost(e) => Some(*e),
Self::Constructed => current_host,
}
}
}
/// Target of this scope.
pub enum ScopeTarget<'a> {
/// Target matches an element matching the specified selector list.
Selector(&'a SelectorList<SelectorImpl>),
/// Target matches only the specified element.
Element(OpaqueElement),
}
impl<'a> ScopeTarget<'a> {
/// Check if the given element is the scope.
pub fn check<E: TElement>(
&self,
element: E,
scope: Option<OpaqueElement>,
context: &mut MatchingContext<E::Impl>,
) -> bool {
match self {
Self::Selector(list) => context.nest_for_scope_condition(scope, |context| {
matches_selector_list(list, &element, context)
}),
Self::Element(e) => element.opaque() == *e,
}
}
}
/// A scope root candidate.
#[derive(Clone, Copy, Debug)]
pub struct ScopeRootCandidate {
/// This candidate's scope root.
pub root: OpaqueElement,
/// Ancestor hop from the element under consideration to this scope root.
pub proximity: ScopeProximity,
}
/// Collect potential scope roots for a given element and its scope target.
/// The check may not pass the ceiling, if specified.
pub fn collect_scope_roots<E>(
element: E,
ceiling: Option<OpaqueElement>,
context: &mut MatchingContext<E::Impl>,
target: &ScopeTarget,
matches_shadow_host: bool,
) -> Vec<ScopeRootCandidate>
where
E: TElement,
{
let mut result = vec![];
let mut parent = Some(element);
let mut proximity = 0usize;
while let Some(p) = parent {
if ceiling == Some(p.opaque()) {
break;
}
if target.check(p, ceiling, context) {
result.push(ScopeRootCandidate {
root: p.opaque(),
proximity: ScopeProximity::new(proximity),
});
// Note that we can't really break here - we need to consider
// ALL scope roots to figure out whch one didn't end.
}
parent = p.parent_element();
proximity += 1;
// We we got to the top of the shadow tree - keep going
// if we may match the shadow host.
if parent.is_none() && matches_shadow_host {
parent = p.containing_shadow_host();
}
}
result
}
/// Given the scope-end selector, check if the element is outside of the scope.
/// That is, check if any ancestor to the root matches the scope-end selector.
pub fn element_is_outside_of_scope<E>(
selector: &Selector<E::Impl>,
element: E,
root: OpaqueElement,
context: &mut MatchingContext<E::Impl>,
root_may_be_shadow_host: bool,
) -> bool
where
E: TElement,
{
let mut parent = Some(element);
context.nest_for_scope_condition(Some(root), |context| {
while let Some(p) = parent {
if matches_selector(selector, 0, None, &p, context) {
return true;
}
if p.opaque() == root {
// Reached the top, not lying outside of scope.
break;
}
parent = p.parent_element();
if parent.is_none() && root_may_be_shadow_host {
if let Some(host) = p.containing_shadow_host() {
// Pretty much an edge case where user specified scope-start and -end of :host
return host.opaque() == root;
}
}
}
return false;
})
}

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

@ -39,7 +39,10 @@ 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::{ImplicitScopeRoot, ScopeBounds};
use crate::stylesheets::scope_rule::{
collect_scope_roots, element_is_outside_of_scope, ImplicitScopeRoot, ScopeBounds,
ScopeRootCandidate, ScopeTarget,
};
#[cfg(feature = "gecko")]
use crate::stylesheets::{
CounterStyleRule, FontFaceRule, FontFeatureValuesRule, FontPaletteValuesRule,
@ -68,7 +71,6 @@ use selectors::parser::{
SelectorList,
};
use selectors::visitor::{SelectorListKind, SelectorVisitor};
use selectors::OpaqueElement;
use servo_arc::{Arc, ArcBorrow};
use smallvec::SmallVec;
use std::cmp::Ordering;
@ -2371,15 +2373,6 @@ impl ContainerConditionReference {
}
}
/// A scope root candidate.
#[derive(Clone, Copy, Debug)]
pub struct ScopeRootCandidate {
/// This candidate's scope root.
pub root: OpaqueElement,
/// Ancestor hop from the element under consideration to this scope root.
pub proximity: ScopeProximity,
}
/// The id of a given scope condition, a sequentially-increasing identifier
/// for a given style set.
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord)]
@ -2783,16 +2776,110 @@ impl CascadeData {
pub(crate) fn scope_condition_matches<E>(
&self,
_id: ScopeConditionId,
_stylist: &Stylist,
_element: E,
_context: &mut MatchingContext<E::Impl>,
) -> Option<Vec<ScopeRootCandidate>>
id: ScopeConditionId,
stylist: &Stylist,
element: E,
context: &mut MatchingContext<E::Impl>,
) -> Vec<ScopeRootCandidate>
where
E: TElement,
{
// TODO(dshin, bug 1886441)
None
let condition_ref = &self.scope_conditions[id.0 as usize];
let bounds = match condition_ref.condition {
None => return vec![],
Some(ref c) => c,
};
// Make sure the parent scopes ara evaluated first. This runs a bit counter to normal
// selector matching where rightmost selectors match first. However, this avoids having
// to traverse through descendants (i.e. Avoids tree traversal vs linear traversal).
let outer_scope_roots =
self.scope_condition_matches(condition_ref.parent, stylist, element, context);
let is_outermost_scope = condition_ref.parent == ScopeConditionId::none();
if !is_outermost_scope && outer_scope_roots.is_empty() {
return vec![];
}
let (root_target, matches_shadow_host) = if let Some(start) = bounds.start.as_ref() {
(
ScopeTarget::Selector(start),
start.slice().iter().any(|s| {
!s.matches_featureless_host_selector_or_pseudo_element()
.is_empty()
}),
)
} else {
let implicit_root = condition_ref
.implicit_scope_root
.as_ref()
.expect("No boundaries, no implicit root?");
match implicit_root {
StylistImplicitScopeRoot::Normal(r) => {
match r.element(context.current_host.clone()) {
None => return vec![],
Some(root) => (ScopeTarget::Element(root), r.matches_shadow_host()),
}
},
StylistImplicitScopeRoot::Cached(index) => {
use crate::dom::TShadowRoot;
let shadow_root = if let Some(root) = element.shadow_root() {
root
} else {
element.containing_shadow().expect("Not shadow host and not under shadow tree?")
};
match shadow_root.implicit_scope_for_sheet(*index) {
None => return vec![],
Some(root) => {
match root.element(context.current_host.clone()) {
None => return vec![],
Some(r) => (ScopeTarget::Element(r), root.matches_shadow_host()),
}
},
}
},
}
};
let potential_scope_roots = if is_outermost_scope {
collect_scope_roots(element, None, context, &root_target, matches_shadow_host)
} else {
let mut result = vec![];
for activation in &outer_scope_roots {
let mut this_result = collect_scope_roots(
element,
Some(activation.root),
context,
&root_target,
matches_shadow_host,
);
result.append(&mut this_result);
}
result
};
if potential_scope_roots.is_empty() {
return potential_scope_roots;
}
if let Some(end) = bounds.end.as_ref() {
let mut result = vec![];
// If any scope-end selector matches, we're not in scope.
for scope_root in potential_scope_roots {
if end.slice().iter().all(|selector| {
!element_is_outside_of_scope(
selector,
element,
scope_root.root,
context,
matches_shadow_host,
)
}) {
result.push(scope_root);
}
}
result
} else {
potential_scope_roots
}
}
fn did_finish_rebuild(&mut self) {

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

@ -1,3 +0,0 @@
[scope-container.html]
[Style rules within @container are scoped]
expected: FAIL

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

@ -1,57 +0,0 @@
[scope-evaluation.html]
[Single scope]
expected: FAIL
[Single scope with limit]
expected: FAIL
[Single scope, :scope pseudo in main selector]
expected: FAIL
[Single scope, :scope pseudo in to-selector]
expected: FAIL
[Multiple scopes, :scope pseudo in to-selector]
expected: FAIL
[Inner @scope with :scope in from-selector]
expected: FAIL
[Nested scopes]
expected: FAIL
[Nested scopes, with to-selector]
expected: FAIL
[:scope selecting itself]
expected: FAIL
[The scoping limit is not in scope]
expected: FAIL
[Simulated inclusive scoping limit]
expected: FAIL
[Selecting self with :scope]
expected: FAIL
[Relative selector inside @scope]
expected: FAIL
[Scope root with :has()]
expected: FAIL
[Scope can not match its own root without :scope]
expected: FAIL
[Multiple scopes from same @scope-rule, both limited]
expected: FAIL
[Nested scopes, reverse]
expected: FAIL
[Scope with no elements]
expected: FAIL
[Any scope limit makes the element out of scope]
expected: FAIL

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

@ -1,6 +0,0 @@
[scope-implicit-external.html]
[@scope with external stylesheet through link element]
expected: FAIL
[@scope with external stylesheet through @import]
expected: FAIL

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

@ -1,27 +1,3 @@
[scope-implicit.html]
[@scope without prelude implicitly scopes to parent of owner node]
expected: FAIL
[:scope can style implicit root]
expected: FAIL
[@scope works with two identical stylesheets]
expected: FAIL
[Implicit @scope with inner relative selector]
expected: FAIL
[Implicit @scope with inner nesting selector]
expected: FAIL
[Implicit @scope with limit]
expected: FAIL
[@scope with effectively empty :is() must not match anything]
expected: FAIL
[Implicit @scope has implicitly added :scope descendant combinator]
expected: FAIL
[Proximity calculation of multiple implicit @scope]
expected: FAIL

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

@ -1,3 +0,0 @@
[scope-layer.html]
[Style rules within @layer are scoped]
expected: FAIL

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

@ -1,3 +0,0 @@
[scope-media.html]
[Style rules within @media are scoped]
expected: FAIL

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

@ -1,51 +0,0 @@
[scope-nesting.html]
[Implicit :scope in <scope-end>]
expected: FAIL
[Relative selectors in <scope-end>]
expected: FAIL
[Nesting-selector in the scope's <stylesheet>]
expected: FAIL
[Nesting-selector within :scope rule]
expected: FAIL
[Nesting-selector within :scope rule (double nested)]
expected: FAIL
[@scope nested within style rule]
expected: FAIL
[Parent pseudo class within scope-start]
expected: FAIL
[Parent pseudo class within scope-end]
expected: FAIL
[Parent pseudo class within body of nested @scope]
expected: FAIL
[Implicit rule within nested @scope ]
expected: FAIL
[Implicit rule within nested @scope (proximity)]
expected: FAIL
[Nested :scope inside an :is]
expected: FAIL
[:scope within nested and scoped rule]
expected: FAIL
[:scope within nested and scoped rule (implied &)]
expected: FAIL
[:scope within nested and scoped rule (relative)]
expected: FAIL
[Scoped nested group rule]
expected: FAIL
[Nesting-selector in <scope-end>]
expected: FAIL

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

@ -1,8 +1,3 @@
[scope-proximity.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Alternating light/dark]
expected: FAIL
[Proximity wins over order of appearance]
expected: FAIL

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

@ -1,2 +0,0 @@
[scope-shadow-sharing.html]
expected: FAIL

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

@ -1,15 +1,3 @@
[scope-shadow.tentative.html]
[@scope can match :host(...)]
expected: FAIL
[:scope matches host via the scoping root]
expected: FAIL
[:scope within :is() matches host via the scoping root]
expected: FAIL
[Implicit @scope as direct child of shadow root]
expected: FAIL
[Implicit @scope in construted stylesheet]
expected: FAIL

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

@ -1,33 +0,0 @@
[scope-specificity.html]
[@scope (#main) { .b { } } and .b]
expected: FAIL
[@scope (#main) to (.b) { .a { } } and .a]
expected: FAIL
[@scope (#main, .foo, .bar) { #a { } } and #a]
expected: FAIL
[@scope (#main) { div.b { } } and div.b]
expected: FAIL
[@scope (#main) { :scope .b { } } and .a .b]
expected: FAIL
[@scope (#main) { & .b { } } and #main .b]
expected: FAIL
[@scope (#main) { div .b { } } and div .b]
expected: FAIL
[@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

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

@ -1,3 +0,0 @@
[scope-starting-style.html]
[Style rules within @starting-style are scoped]
expected: FAIL

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

@ -1,3 +0,0 @@
[scope-supports.html]
[Style rules within @supports are scoped]
expected: FAIL

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

@ -1,18 +0,0 @@
[scope-visited-cssom.html]
[:link as scoping root, :scope]
expected: FAIL
[:not(:visited) as scoping root, :scope]
expected: FAIL
[:visited as scoping limit]
expected: FAIL
[:not(:link) as scoping limit]
expected: FAIL
[:visited as scoping root]
expected: FAIL
[:not(:link) as scoping root]
expected: FAIL

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

@ -554,7 +554,7 @@ test_scope(document.currentScript, () => {
<div class=a>
<span id="in"></span>
<div class=b>
<span id="out"</span>
<span id="out"></span>
<div class=c></div>
</div>
</div>

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

@ -121,3 +121,21 @@ test_scope(document.currentScript, () => {
assert_equals(getComputedStyle(item).borderColor, 'rgb(0, 128, 0)');
}, 'Specificity wins over proximity');
</script>
<template>
<style>
@scope (.foo) {
.bar span[id] { border-color:green; }
}
</style>
<div class=foo>
<div class="foo bar">
<span id=item></span>
</div>
</div>
</template>
<script>
test_scope(document.currentScript, () => {
assert_equals(getComputedStyle(item).borderColor, 'rgb(0, 128, 0)');
}, 'Identical root with further proximity is not ignored');
</script>