Bug 1850974 - Make :is(:host) work. r=zrhoffman

This should work per spec, see
https://github.com/w3c/csswg-drafts/issues/9509.

Tweak a bit the selector flags set up so that checking for :host
selectors during CascadeData rebuilds is cheap.

Differential Revision: https://phabricator.services.mozilla.com/D191570
This commit is contained in:
Emilio Cobos Álvarez 2023-10-27 14:29:38 +00:00
parent 2775ca27cd
commit f4542a93c1
12 changed files with 327 additions and 103 deletions

View File

@ -176,10 +176,12 @@ bitflags! {
const HAS_SLOTTED = 1 << 1;
const HAS_PART = 1 << 2;
const HAS_PARENT = 1 << 3;
const HAS_NON_FEATURELESS_COMPONENT = 1 << 4;
const HAS_HOST = 1 << 5;
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ToShmem)]
pub struct SpecificityAndFlags {
/// There are two free bits here, since we use ten bits for each specificity
/// kind (id, class, element).
@ -188,33 +190,6 @@ pub struct SpecificityAndFlags {
pub(crate) flags: SelectorFlags,
}
impl SpecificityAndFlags {
#[inline]
pub fn specificity(&self) -> u32 {
self.specificity
}
#[inline]
pub fn has_pseudo_element(&self) -> bool {
self.flags.intersects(SelectorFlags::HAS_PSEUDO)
}
#[inline]
pub fn has_parent_selector(&self) -> bool {
self.flags.intersects(SelectorFlags::HAS_PARENT)
}
#[inline]
pub fn is_slotted(&self) -> bool {
self.flags.intersects(SelectorFlags::HAS_SLOTTED)
}
#[inline]
pub fn is_part(&self) -> bool {
self.flags.intersects(SelectorFlags::HAS_PART)
}
}
const MAX_10BIT: u32 = (1u32 << 10) - 1;
#[derive(Add, AddAssign, Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
@ -276,9 +251,12 @@ where
flags.insert(SelectorFlags::HAS_PSEUDO);
specificity.element_selectors += 1
},
Component::LocalName(..) => specificity.element_selectors += 1,
Component::LocalName(..) => {
flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
specificity.element_selectors += 1
},
Component::Slotted(ref selector) => {
flags.insert(SelectorFlags::HAS_SLOTTED);
flags.insert(SelectorFlags::HAS_SLOTTED | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
specificity.element_selectors += 1;
// Note that due to the way ::slotted works we only compete with
// other ::slotted rules, so the above rule doesn't really
@ -287,21 +265,19 @@ where
//
// See: https://github.com/w3c/csswg-drafts/issues/1915
*specificity += Specificity::from(selector.specificity());
if selector.has_parent_selector() {
flags.insert(SelectorFlags::HAS_PARENT);
}
flags.insert(selector.flags());
},
Component::Host(ref selector) => {
flags.insert(SelectorFlags::HAS_HOST);
specificity.class_like_selectors += 1;
if let Some(ref selector) = *selector {
// See: https://github.com/w3c/csswg-drafts/issues/1915
*specificity += Specificity::from(selector.specificity());
if selector.has_parent_selector() {
flags.insert(SelectorFlags::HAS_PARENT);
}
flags.insert(selector.flags() - SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
}
},
Component::ID(..) => {
flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
specificity.id_selectors += 1;
},
Component::Class(..) |
@ -313,6 +289,7 @@ where
Component::Scope |
Component::Nth(..) |
Component::NonTSPseudoClass(..) => {
flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
specificity.class_like_selectors += 1;
},
Component::NthOf(ref nth_of_data) => {
@ -325,7 +302,7 @@ where
specificity.class_like_selectors += 1;
let sf = selector_list_specificity_and_flags(nth_of_data.selectors().iter());
*specificity += Specificity::from(sf.specificity);
flags.insert(sf.flags);
flags.insert(sf.flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
},
// https://drafts.csswg.org/selectors/#specificity-rules:
//
@ -344,7 +321,7 @@ where
Component::Has(ref relative_selectors) => {
let sf = relative_selector_list_specificity_and_flags(relative_selectors);
*specificity += Specificity::from(sf.specificity);
flags.insert(sf.flags);
flags.insert(sf.flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
},
Component::ExplicitUniversalType |
Component::ExplicitAnyNamespace |
@ -354,6 +331,7 @@ where
Component::RelativeSelectorAnchor |
Component::Invalid(..) => {
// Does not affect specificity
flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
},
}
}
@ -377,9 +355,7 @@ pub(crate) fn selector_list_specificity_and_flags<'a, Impl: SelectorImpl>(
let mut flags = SelectorFlags::empty();
for selector in itr {
specificity = std::cmp::max(specificity, selector.specificity());
if selector.has_parent_selector() {
flags.insert(SelectorFlags::HAS_PARENT);
}
flags.insert(selector.flags());
}
SpecificityAndFlags { specificity, flags }
}

View File

@ -735,32 +735,32 @@ impl<Impl: SelectorImpl> Selector<Impl> {
#[inline]
pub fn specificity(&self) -> u32 {
self.0.header.specificity()
self.0.header.specificity
}
#[inline]
fn flags(&self) -> SelectorFlags {
pub(crate) fn flags(&self) -> SelectorFlags {
self.0.header.flags
}
#[inline]
pub fn has_pseudo_element(&self) -> bool {
self.0.header.has_pseudo_element()
self.flags().intersects(SelectorFlags::HAS_PSEUDO)
}
#[inline]
pub fn has_parent_selector(&self) -> bool {
self.0.header.has_parent_selector()
self.flags().intersects(SelectorFlags::HAS_PARENT)
}
#[inline]
pub fn is_slotted(&self) -> bool {
self.0.header.is_slotted()
self.flags().intersects(SelectorFlags::HAS_SLOTTED)
}
#[inline]
pub fn is_part(&self) -> bool {
self.0.header.is_part()
self.flags().intersects(SelectorFlags::HAS_PART)
}
#[inline]
@ -855,27 +855,12 @@ impl<Impl: SelectorImpl> Selector<Impl> {
}
}
/// Whether this selector is a featureless :host selector, with no
/// combinators to the left, and optionally has a pseudo-element to the
/// right.
/// Whether this selector is a featureless :host selector, with no combinators to the left, and
/// optionally has a pseudo-element to the right.
#[inline]
pub fn is_featureless_host_selector_or_pseudo_element(&self) -> bool {
let mut iter = self.iter();
if !self.has_pseudo_element() {
return iter.is_featureless_host_selector();
}
// Skip the pseudo-element.
for _ in &mut iter {}
match iter.next_sequence() {
None => return false,
Some(combinator) => {
debug_assert_eq!(combinator, Combinator::PseudoElement);
},
}
iter.is_featureless_host_selector()
let flags = self.flags();
flags.intersects(SelectorFlags::HAS_HOST) && !flags.intersects(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT)
}
/// Returns an iterator over this selector in matching order (right-to-left),
@ -955,27 +940,22 @@ impl<Impl: SelectorImpl> Selector<Impl> {
}
pub fn replace_parent_selector(&self, parent: &SelectorList<Impl>) -> Self {
// FIXME(emilio): Shouldn't allow replacing if parent has a pseudo-element selector
// or what not.
let flags = self.flags() - SelectorFlags::HAS_PARENT;
let mut specificity = Specificity::from(self.specificity());
let parent_specificity =
Specificity::from(selector_list_specificity_and_flags(parent.slice().iter()).specificity());
let parent_specificity_and_flags = selector_list_specificity_and_flags(parent.slice().iter());
// The specificity at this point will be wrong, we replace it by the correct one after the
// fact.
let specificity_and_flags = SpecificityAndFlags {
specificity: self.specificity(),
flags,
};
let mut specificity = Specificity::from(self.specificity());
let mut flags = self.flags() - SelectorFlags::HAS_PARENT;
fn replace_parent_on_selector_list<Impl: SelectorImpl>(
orig: &[Selector<Impl>],
parent: &SelectorList<Impl>,
specificity: &mut Specificity,
with_specificity: bool,
flags: &mut SelectorFlags,
propagate_specificity: bool,
flags_to_propagate: SelectorFlags,
) -> Option<SelectorList<Impl>> {
let mut any = false;
if !orig.iter().any(|s| s.has_parent_selector()) {
return None;
}
let result = SelectorList::from_iter(
orig
@ -984,23 +964,19 @@ impl<Impl: SelectorImpl> Selector<Impl> {
if !s.has_parent_selector() {
return s.clone();
}
any = true;
s.replace_parent_selector(parent)
})
);
if !any {
return None;
let result_specificity_and_flags =
selector_list_specificity_and_flags(result.slice().iter());
if propagate_specificity {
*specificity += Specificity::from(
result_specificity_and_flags.specificity -
selector_list_specificity_and_flags(orig.iter()).specificity,
);
}
if !with_specificity {
return Some(result);
}
*specificity += Specificity::from(
selector_list_specificity_and_flags(result.slice().iter()).specificity -
selector_list_specificity_and_flags(orig.iter()).specificity,
);
flags.insert(result_specificity_and_flags.flags.intersection(flags_to_propagate));
Some(result)
}
@ -1008,6 +984,8 @@ impl<Impl: SelectorImpl> Selector<Impl> {
orig: &[RelativeSelector<Impl>],
parent: &SelectorList<Impl>,
specificity: &mut Specificity,
flags: &mut SelectorFlags,
flags_to_propagate: SelectorFlags,
) -> Vec<RelativeSelector<Impl>> {
let mut any = false;
@ -1029,8 +1007,10 @@ impl<Impl: SelectorImpl> Selector<Impl> {
return result;
}
let result_specificity_and_flags = relative_selector_list_specificity_and_flags(&result);
flags.insert(result_specificity_and_flags.flags.intersection(flags_to_propagate));
*specificity += Specificity::from(
relative_selector_list_specificity_and_flags(&result).specificity -
result_specificity_and_flags.specificity -
relative_selector_list_specificity_and_flags(orig).specificity,
);
result
@ -1040,12 +1020,15 @@ impl<Impl: SelectorImpl> Selector<Impl> {
orig: &Selector<Impl>,
parent: &SelectorList<Impl>,
specificity: &mut Specificity,
flags: &mut SelectorFlags,
flags_to_propagate: SelectorFlags,
) -> Selector<Impl> {
if !orig.has_parent_selector() {
return orig.clone();
}
let new_selector = orig.replace_parent_selector(parent);
*specificity += Specificity::from(new_selector.specificity() - orig.specificity());
flags.insert(new_selector.flags().intersection(flags_to_propagate));
new_selector
}
@ -1053,7 +1036,8 @@ impl<Impl: SelectorImpl> Selector<Impl> {
// Implicit `&` plus descendant combinator.
let iter = self.iter_raw_match_order();
let len = iter.len() + 2;
specificity += parent_specificity;
specificity += Specificity::from(parent_specificity_and_flags.specificity);
flags.insert(parent_specificity_and_flags.flags);
let iter = iter
.cloned()
.chain(std::iter::once(Component::Combinator(
@ -1062,7 +1046,7 @@ impl<Impl: SelectorImpl> Selector<Impl> {
.chain(std::iter::once(Component::Is(
parent.clone()
)));
UniqueArc::from_header_and_iter_with_size(specificity_and_flags, iter, len)
UniqueArc::from_header_and_iter_with_size(Default::default(), iter, len)
} else {
let iter = self.iter_raw_match_order().map(|component| {
use self::Component::*;
@ -1090,7 +1074,8 @@ impl<Impl: SelectorImpl> Selector<Impl> {
Invalid(..) |
RelativeSelectorAnchor => component.clone(),
ParentSelector => {
specificity += parent_specificity;
specificity += Specificity::from(parent_specificity_and_flags.specificity);
flags.insert(parent_specificity_and_flags.flags);
Is(parent.clone())
},
Negation(ref selectors) => {
@ -1098,7 +1083,9 @@ impl<Impl: SelectorImpl> Selector<Impl> {
selectors.slice(),
parent,
&mut specificity,
/* with_specificity = */ true,
&mut flags,
/* propagate_specificity = */ true,
SelectorFlags::all(),
).unwrap_or_else(|| selectors.clone()))
},
Is(ref selectors) => {
@ -1106,7 +1093,9 @@ impl<Impl: SelectorImpl> Selector<Impl> {
selectors.slice(),
parent,
&mut specificity,
/* with_specificity = */ true,
&mut flags,
/* propagate_specificity = */ true,
SelectorFlags::all(),
).unwrap_or_else(|| selectors.clone()))
},
Where(ref selectors) => {
@ -1114,13 +1103,17 @@ impl<Impl: SelectorImpl> Selector<Impl> {
selectors.slice(),
parent,
&mut specificity,
/* with_specificity = */ false,
&mut flags,
/* propagate_specificity = */ false,
SelectorFlags::all(),
).unwrap_or_else(|| selectors.clone()))
},
Has(ref selectors) => Has(replace_parent_on_relative_selector_list(
selectors,
parent,
&mut specificity,
&mut flags,
SelectorFlags::all(),
)
.into_boxed_slice()),
@ -1128,13 +1121,17 @@ impl<Impl: SelectorImpl> Selector<Impl> {
selector,
parent,
&mut specificity,
&mut flags,
SelectorFlags::all() - SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
))),
NthOf(ref data) => {
let selectors = replace_parent_on_selector_list(
data.selectors(),
parent,
&mut specificity,
/* with_specificity = */ true,
&mut flags,
/* propagate_specificity = */ true,
SelectorFlags::all(),
);
NthOf(match selectors {
Some(s) => NthOfSelectorData::new(
@ -1148,12 +1145,17 @@ impl<Impl: SelectorImpl> Selector<Impl> {
selector,
parent,
&mut specificity,
&mut flags,
SelectorFlags::all(),
)),
}
});
UniqueArc::from_header_and_iter(specificity_and_flags, iter)
UniqueArc::from_header_and_iter(Default::default(), iter)
};
*items.header_mut() = SpecificityAndFlags {
specificity: specificity.into(),
flags,
};
items.header_mut().specificity = specificity.into();
Selector(items.shareable())
}
@ -1291,7 +1293,7 @@ impl<'a, Impl: 'a + SelectorImpl> SelectorIter<'a, Impl> {
#[inline]
pub(crate) fn is_featureless_host_selector(&mut self) -> bool {
self.selector_length() > 0 &&
self.all(|component| component.is_host()) &&
self.all(|component| component.matches_featureless_host()) &&
self.next_sequence().is_none()
}
@ -1957,6 +1959,22 @@ impl<Impl: SelectorImpl> Component<Impl> {
matches!(*self, Component::Host(..))
}
/// Returns true if this is a :host() selector.
#[inline]
pub fn matches_featureless_host(&self) -> bool {
match *self {
Component::Host(..) => true,
Component::Where(ref l) |
Component::Is(ref l) => {
// TODO(emilio): For now we use .all() rather than .any(), because not doing so
// brings up a fair amount of extra complexity (we can't make the decision on
// whether to walk out statically).
l.slice().iter().all(|i| i.is_featureless_host_selector_or_pseudo_element())
},
_ => false,
}
}
/// Returns the value as a combinator if applicable, None otherwise.
pub fn as_combinator(&self) -> Option<Combinator> {
match *self {

View File

@ -0,0 +1,23 @@
<!doctype html>
<meta charset="utf-8">
<title>:host and nesting (basic) </title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/css-nesting/#nest-selector">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
<p>Test passes if there is a filled green square.</p>
<div id="host"></div>
<script>
host.attachShadow({mode: "open"}).innerHTML = `
<style>
:host {
.nested {
width: 100px;
height: 100px;
background-color: green;
}
}
</style>
<div class="nested"></div>
`;
</script>

View File

@ -0,0 +1,22 @@
<!doctype html>
<meta charset="utf-8">
<title>:host and nesting (bare declarations)</title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/css-nesting/#nest-selector">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
<p>Test passes if there is a filled green square.</p>
<div id="host"></div>
<script>
host.attachShadow({mode: "open"}).innerHTML = `
<style>
:host {
@media (width >= 0) {
width: 100px;
height: 100px;
background-color: green;
}
}
</style>
`;
</script>

View File

@ -0,0 +1,26 @@
<!doctype html>
<meta charset="utf-8">
<title>:host and nesting (combined with something else)</title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/css-nesting/#nest-selector">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
<p>Test passes if there is a filled green square.</p>
<div id="host"></div>
<script>
host.attachShadow({mode: "open"}).innerHTML = `
<style>
.nested {
width: 100px;
height: 100px;
background-color: green;
}
:host(#not-host), #host {
.nested {
background-color: red;
}
}
</style>
<div class="nested"></div>
`;
</script>

View File

@ -0,0 +1,25 @@
<!doctype html>
<meta charset="utf-8">
<title>:host and nesting (combined with something else, bare declarations)</title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/css-nesting/#nest-selector">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
<p>Test passes if there is a filled green square.</p>
<div id="host"></div>
<script>
host.attachShadow({mode: "open"}).innerHTML = `
<style>
:host {
width: 100px;
height: 100px;
background-color: green;
}
:host(#not-host), #host {
@media (width >= 0) {
background-color: red;
}
}
</style>
`;
</script>

View File

@ -0,0 +1,24 @@
<!doctype html>
<meta charset="utf-8">
<title>:host and nesting (with pseudos)</title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/css-nesting/#nest-selector">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
<p>Test passes if there is a filled green square.</p>
<div id="host"></div>
<script>
host.attachShadow({mode: "open"}).innerHTML = `
<style>
:host {
&::before {
display: block;
content: "";
width: 100px;
height: 100px;
background-color: green;
}
}
</style>
`;
</script>

View File

@ -0,0 +1,21 @@
<!doctype html>
<meta charset="utf-8">
<title>:host and :is (basic) </title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/selectors/#featureless">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
<p>Test passes if there is a filled green square.</p>
<div id="host"></div>
<script>
host.attachShadow({mode: "open"}).innerHTML = `
<style>
:is(:host) .nested {
width: 100px;
height: 100px;
background-color: green;
}
</style>
<div class="nested"></div>
`;
</script>

View File

@ -0,0 +1,20 @@
<!doctype html>
<meta charset="utf-8">
<title>:host and :is (basic)</title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/selectors/#featureless">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
<p>Test passes if there is a filled green square.</p>
<div id="host"></div>
<script>
host.attachShadow({mode: "open"}).innerHTML = `
<style>
:is(:host) {
width: 100px;
height: 100px;
background-color: green;
}
</style>
`;
</script>

View File

@ -0,0 +1,24 @@
<!doctype html>
<meta charset="utf-8">
<title>:host and :is() (combined with something else)</title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/selectors/#featureless">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
<p>Test passes if there is a filled green square.</p>
<div id="host"></div>
<script>
host.attachShadow({mode: "open"}).innerHTML = `
<style>
.nested {
width: 100px;
height: 100px;
background-color: green;
}
:is(:host(#not-host), #host) .nested {
background-color: red;
}
</style>
<div class="nested"></div>
`;
</script>

View File

@ -0,0 +1,23 @@
<!doctype html>
<meta charset="utf-8">
<title>:host and :is (combined with something else)</title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/selectors/#featureless">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
<p>Test passes if there is a filled green square.</p>
<div id="host"></div>
<script>
host.attachShadow({mode: "open"}).innerHTML = `
<style>
:host {
width: 100px;
height: 100px;
background-color: green;
}
:is(:host(#not-host), #host) {
background-color: red;
}
</style>
`;
</script>

View File

@ -0,0 +1,22 @@
<!doctype html>
<meta charset="utf-8">
<title>:host and :is() (with pseudos)</title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/selectors/#featureless">
<link rel="match" href="/css/reference/ref-filled-green-100px-square-only.html">
<p>Test passes if there is a filled green square.</p>
<div id="host"></div>
<script>
host.attachShadow({mode: "open"}).innerHTML = `
<style>
:is(:host)::before {
display: block;
content: "";
width: 100px;
height: 100px;
background-color: green;
}
</style>
`;
</script>