Bug 1879349 - Improve featureless host matching. r=dshin

Remove host-multiple-006 because it's a bit-identical copy of
host-multiple-002.html.

Add a test for the specific descendant combinator (bug 1950290).

Differential Revision: https://phabricator.services.mozilla.com/D240033
This commit is contained in:
Emilio Cobos Álvarez
2025-02-28 19:53:05 +00:00
parent 58eede6355
commit 73f801c434
11 changed files with 329 additions and 305 deletions

View File

@@ -184,9 +184,8 @@ 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;
const HAS_SCOPE = 1 << 6;
const HAS_HOST = 1 << 4;
const HAS_SCOPE = 1 << 5;
}
}
@@ -306,13 +305,10 @@ where
}
},
Component::LocalName(..) => {
flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
specificity.element_selectors += 1
},
Component::Slotted(ref selector) => {
flags.insert(
SelectorFlags::HAS_SLOTTED | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
);
flags.insert(SelectorFlags::HAS_SLOTTED);
if !for_nesting_parent {
specificity.element_selectors += 1;
// Note that due to the way ::slotted works we only compete with
@@ -331,11 +327,10 @@ where
if let Some(ref selector) = *selector {
// See: https://github.com/w3c/csswg-drafts/issues/1915
*specificity += Specificity::from(selector.specificity());
flags.insert(selector.flags() - SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
flags.insert(selector.flags());
}
},
Component::ID(..) => {
flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
specificity.id_selectors += 1;
},
Component::Class(..) |
@@ -346,7 +341,6 @@ where
Component::Empty |
Component::Nth(..) |
Component::NonTSPseudoClass(..) => {
flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
specificity.class_like_selectors += 1;
},
Component::Scope | Component::ImplicitScope => {
@@ -365,7 +359,7 @@ where
specificity.class_like_selectors += 1;
let sf = selector_list_specificity_and_flags(nth_of_data.selectors().iter(), for_nesting_parent);
*specificity += Specificity::from(sf.specificity);
flags.insert(sf.flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
flags.insert(sf.flags);
},
// https://drafts.csswg.org/selectors/#specificity-rules:
//
@@ -384,7 +378,7 @@ where
Component::Has(ref relative_selectors) => {
let sf = relative_selector_list_specificity_and_flags(relative_selectors, for_nesting_parent);
*specificity += Specificity::from(sf.specificity);
flags.insert(sf.flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
flags.insert(sf.flags);
},
Component::ExplicitUniversalType |
Component::ExplicitAnyNamespace |
@@ -394,7 +388,6 @@ where
Component::RelativeSelectorAnchor |
Component::Invalid(..) => {
// Does not affect specificity
flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
},
}
}

View File

@@ -176,6 +176,9 @@ where
/// Whether there are any rules inside @starting-style.
pub has_starting_style: bool,
/// Whether we're currently matching a featureless element.
pub featureless: bool,
/// The current nesting level of selectors that we're matching.
nesting_level: usize,
@@ -253,6 +256,7 @@ where
matching_for_invalidation,
scope_element: None,
current_host: None,
featureless: false,
nesting_level: 0,
in_negation: false,
pseudo_element_matching_fn: None,
@@ -373,6 +377,31 @@ where
self.visited_handling
}
/// Runs F with a different featureless element flag.
#[inline]
pub fn with_featureless<F, R>(
&mut self,
featureless: bool,
f: F,
) -> R
where
F: FnOnce(&mut Self) -> R,
{
let orig = self.featureless;
self.featureless = featureless;
let result = f(self);
self.featureless = orig;
result
}
/// Returns whether the currently matching element is acting as a featureless element (e.g.,
/// because we've crossed a shadow boundary). This is used to implement the :host selector
/// rules properly.
#[inline]
pub fn featureless(&self) -> bool {
self.featureless
}
/// Runs F with a different VisitedHandlingMode.
#[inline]
pub fn with_visited_handling_mode<F, R>(

View File

@@ -9,7 +9,7 @@ use crate::attr::{
use crate::bloom::{BloomFilter, BLOOM_HASH_MASK};
use crate::kleene_value::KleeneValue;
use crate::parser::{
AncestorHashes, Combinator, Component, FeaturelessHostMatches, LocalName, NthSelectorData,
AncestorHashes, Combinator, Component, MatchesFeaturelessHost, LocalName, NthSelectorData,
RelativeSelectorMatchHint,
};
use crate::parser::{
@@ -376,6 +376,11 @@ where
start_offset
);
debug_assert!(
!local_context.shared.featureless(),
"Invalidating featureless element somehow?"
);
for component in iter {
let result = matches_simple_selector(component, element, &mut local_context);
debug_assert!(result != KleeneValue::Unknown, "Returned unknown in non invalidation context?");
@@ -758,66 +763,47 @@ where
Some(current_slot)
}
struct NextElement<E> {
next_element: Option<E>,
featureless: bool,
}
impl<E> NextElement<E> {
#[inline(always)]
fn new(next_element: Option<E>, featureless: bool) -> Self {
Self { next_element, featureless }
}
}
#[inline(always)]
fn next_element_for_combinator<E>(
element: &E,
combinator: Combinator,
selector: &SelectorIter<E::Impl>,
context: &MatchingContext<E::Impl>,
) -> Option<E>
) -> NextElement<E>
where
E: Element,
{
match combinator {
Combinator::NextSibling | Combinator::LaterSibling => element.prev_sibling_element(),
Combinator::NextSibling | Combinator::LaterSibling => NextElement::new(
element.prev_sibling_element(),
false,
),
Combinator::Child | Combinator::Descendant => {
match element.parent_element() {
Some(e) => return Some(e),
None => {},
if let Some(parent) = element.parent_element() {
return NextElement::new(Some(parent), false);
}
if !element.parent_node_is_shadow_root() {
return None;
}
// https://drafts.csswg.org/css-scoping/#host-element-in-tree:
//
// For the purpose of Selectors, a shadow host also appears in
// its shadow tree, with the contents of the shadow tree treated
// as its children. (In other words, the shadow host is treated as
// replacing the shadow root node.)
//
// and also:
//
// When considered within its own shadow trees, the shadow host is
// featureless. Only the :host, :host(), and :host-context()
// pseudo-classes are allowed to match it.
//
// Since we know that the parent is a shadow root, we necessarily
// are in a shadow tree of the host, and the next selector will only
// match if the selector is a featureless :host selector.
let matches_featureless_host = selector.clone().is_featureless_host_selector();
if matches_featureless_host.intersects(FeaturelessHostMatches::FOR_HOST) {
// May not match the inner selector, but we can't really call that here.
return element.containing_shadow_host()
} else if matches_featureless_host.intersects(FeaturelessHostMatches::FOR_SCOPE) {
let host = element.containing_shadow_host();
// If this element's shadow host matches the `:scope` element, we should
// treat the `:scope` selector as featureless.
// See https://github.com/w3c/csswg-drafts/issues/9025.
if context.scope_element.is_some() &&
context.scope_element.clone() == host.clone().map(|e| e.opaque())
{
return host;
}
return None;
let element = if element.parent_node_is_shadow_root() {
element.containing_shadow_host()
} else {
return None;
}
None
};
NextElement::new(element, true)
},
Combinator::Part => host_for_part(element, context),
Combinator::SlotAssignment => assigned_slot(element, context),
Combinator::PseudoElement => element.pseudo_element_originating_element(),
Combinator::Part => NextElement::new(host_for_part(element, context), false),
Combinator::SlotAssignment => NextElement::new(assigned_slot(element, context), false),
Combinator::PseudoElement => NextElement::new(element.pseudo_element_originating_element(), false),
}
}
@@ -856,8 +842,13 @@ where
};
let combinator = selector_iter.next_sequence();
if combinator.map_or(false, |c| c.is_sibling()) {
if context.needs_selector_flags() {
if let Some(c) = combinator {
if context.featureless() && !c.is_pseudo_element() {
// A featureless element shouldn't match any further combinator.
// TODO(emilio): Maybe we could avoid the compound matching more eagerly.
return SelectorMatchingResult::NotMatchedGlobally;
}
if c.is_sibling() && context.needs_selector_flags() {
element.apply_selector_flags(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS);
}
}
@@ -914,25 +905,27 @@ where
visited_handling = VisitedHandlingMode::AllLinksUnvisited;
}
element = match next_element_for_combinator(&element, combinator, &selector_iter, &context)
{
let NextElement { next_element, featureless } = next_element_for_combinator(&element, combinator, &context);
element = match next_element {
None => return candidate_not_found,
Some(next_element) => next_element,
Some(e) => e,
};
let result = context.with_visited_handling_mode(visited_handling, |context| {
matches_complex_selector_internal(
selector_iter.clone(),
&element,
context,
rightmost,
first_subject_compound,
)
context.with_featureless(featureless, |context| {
matches_complex_selector_internal(
selector_iter.clone(),
&element,
context,
rightmost,
first_subject_compound,
)
})
});
match (result, combinator) {
// Return the status immediately.
(SelectorMatchingResult::Matched | SelectorMatchingResult::Unknown, _) => {
match result {
SelectorMatchingResult::Matched | SelectorMatchingResult::Unknown => {
// Return the status immediately.
debug_assert!(
matches_compound_selector.to_bool(true),
"Compound didn't match?"
@@ -945,36 +938,43 @@ where
}
// Something returned unknown, so return unknown.
return SelectorMatchingResult::Unknown;
},
(SelectorMatchingResult::NotMatchedGlobally, _) | (_, Combinator::NextSibling) => {
return result;
},
// Upgrade the failure status to
// NotMatchedAndRestartFromClosestDescendant.
(_, Combinator::PseudoElement) | (_, Combinator::Child) => {
return SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant;
},
// If the failure status is
// NotMatchedAndRestartFromClosestDescendant and combinator is
// Combinator::LaterSibling, give up this Combinator::LaterSibling
// matching and restart from the closest descendant combinator.
(
SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant,
Combinator::LaterSibling,
) => {
return result;
},
// The Combinator::Descendant combinator and the status is
// NotMatchedAndRestartFromClosestLaterSibling or
// NotMatchedAndRestartFromClosestDescendant, or the
// Combinator::LaterSibling combinator and the status is
// NotMatchedAndRestartFromClosestDescendant, we can continue to
// matching on the next candidate element.
}
SelectorMatchingResult::NotMatchedGlobally => return result,
_ => {},
}
if featureless {
// A featureless element didn't match the selector, we can stop matching now rather
// than looking at following elements for our combinator.
return SelectorMatchingResult::NotMatchedGlobally;
}
match combinator {
Combinator::NextSibling => return result,
Combinator::PseudoElement | Combinator::Child => {
// Upgrade the failure status to NotMatchedAndRestartFromClosestDescendant.
return SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant;
}
Combinator::LaterSibling => {
// If the failure status is NotMatchedAndRestartFromClosestDescendant and combinator is
// LaterSibling, give up this LaterSibling matching and restart from the closest
// descendant combinator.
if matches!(result, SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant) {
return result;
}
}
_ => {
// The Descendant combinator and the status is
// NotMatchedAndRestartFromClosestLaterSibling or
// NotMatchedAndRestartFromClosestDescendant, or the
// LaterSibling combinator and the status is
// NotMatchedAndRestartFromClosestDescendant, we can continue to
// matching on the next candidate element.
//
// TODO(emilio): is this sound with Combinator::Part and others? It seems we could
// early reject if we haven't matched rather than keeping going?
}
}
}
}
@@ -1049,10 +1049,10 @@ where
if host != element.opaque() {
return KleeneValue::False;
}
selector.map_or(KleeneValue::True, |selector| {
context
.nest(|context| matches_complex_selector(selector.iter(), element, context, rightmost))
})
let Some(selector) = selector else { return KleeneValue::True };
context.nest(|context| context.with_featureless(false, |context| {
matches_complex_selector(selector.iter(), element, context, rightmost)
}))
}
fn matches_slotted<E>(
@@ -1104,6 +1104,62 @@ where
)
}
/// There are relatively few selectors in a given compound that may match a featureless element.
/// Instead of adding a check to every selector that may not match, we handle it here in an out of
/// line path.
pub(crate) fn compound_matches_featureless_host<Impl: SelectorImpl>(iter: &mut SelectorIter<Impl>, scope_matches_featureless_host: bool) -> MatchesFeaturelessHost {
let mut matches = MatchesFeaturelessHost::Only;
for component in iter {
match component {
Component::Scope | Component::ImplicitScope if scope_matches_featureless_host => {},
// :host only matches featureless elements.
Component::Host(..) => {},
// Pseudo-elements are allowed to match as well.
Component::PseudoElement(..) => {},
// We allow logical pseudo-classes, but we'll fail matching of the inner selectors if
// necessary.
Component::Is(ref l) | Component::Where(ref l) => {
let mut any_yes = false;
let mut any_no = false;
for selector in l.slice() {
match selector.matches_featureless_host(scope_matches_featureless_host) {
MatchesFeaturelessHost::Never => {
any_no = true;
}
MatchesFeaturelessHost::Yes => {
any_yes = true;
any_no = true;
}
MatchesFeaturelessHost::Only => {
any_yes = true;
}
}
}
if !any_yes {
return MatchesFeaturelessHost::Never;
}
if any_no {
// Potentially downgrade since we might match non-featureless elements too.
matches = MatchesFeaturelessHost::Yes;
}
},
Component::Negation(ref l) => {
// For now preserving behavior, see
// https://github.com/w3c/csswg-drafts/issues/10179 for existing resolutions that
// tweak this behavior.
for selector in l.slice() {
if selector.matches_featureless_host(scope_matches_featureless_host) != MatchesFeaturelessHost::Only {
return MatchesFeaturelessHost::Never;
}
}
},
// Other components don't match the host scope.
_ => return MatchesFeaturelessHost::Never,
}
}
matches
}
/// Determines whether the given element matches the given compound selector.
#[inline]
fn matches_compound_selector<E>(
@@ -1115,6 +1171,9 @@ fn matches_compound_selector<E>(
where
E: Element,
{
if context.featureless() && compound_matches_featureless_host(&mut selector_iter.clone(), /* scope_matches_featureless_host = */ true) == MatchesFeaturelessHost::Never {
return KleeneValue::False;
}
let quirks_data = if context.quirks_mode() == QuirksMode::Quirks {
Some(selector_iter.clone())
} else {

View File

@@ -777,27 +777,23 @@ pub fn namespace_empty_string<Impl: SelectorImpl>() -> Impl::NamespaceUrl {
type SelectorData<Impl> = ThinArc<SpecificityAndFlags, Component<Impl>>;
bitflags! {
/// What kind of selectors potentially matching featureless shawdow host are present.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FeaturelessHostMatches: u8 {
/// This selector matches featureless shadow host via `:host`.
const FOR_HOST = 1 << 0;
/// This selector matches featureless shadow host via `:scope`.
/// Featureless match applies only if we're:
/// 1) In a scoping context, AND
/// 2) The scope is a shadow host.
const FOR_SCOPE = 1 << 1;
}
/// Whether a selector may match a featureless host element, and whether it may match other
/// elements.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MatchesFeaturelessHost {
/// The selector may match a featureless host, but also a non-featureless element.
Yes,
/// The selector is guaranteed to never match a non-featureless host element.
Only,
/// The selector never matches a featureless host.
Never,
}
impl FeaturelessHostMatches {
fn insert_not_empty(&mut self, other: Self) -> bool {
if other.is_empty() {
return false;
}
self.insert(other);
true
impl MatchesFeaturelessHost {
/// Whether we may match.
#[inline]
pub fn may_match(self) -> bool {
return !matches!(self, Self::Never)
}
}
@@ -942,6 +938,36 @@ impl<Impl: SelectorImpl> Selector<Impl> {
})
}
/// Whether this selector may match a featureless shadow host, with no combinators to the
/// left, and optionally has a pseudo-element to the right.
#[inline]
pub fn matches_featureless_host(&self, scope_matches_featureless_host: bool) -> MatchesFeaturelessHost {
let flags = self.flags();
if !flags.intersects(SelectorFlags::HAS_HOST | SelectorFlags::HAS_SCOPE) {
return MatchesFeaturelessHost::Never;
}
let mut iter = self.iter();
if flags.intersects(SelectorFlags::HAS_PSEUDO) {
for _ in &mut iter {
// Skip over pseudo-elements
}
match iter.next_sequence() {
Some(c) if c.is_pseudo_element() => {},
_ => {
debug_assert!(false, "Pseudo selector without pseudo combinator?");
return MatchesFeaturelessHost::Never;
}
}
}
let compound_matches = crate::matching::compound_matches_featureless_host(&mut iter, scope_matches_featureless_host);
if iter.next_sequence().is_some() {
return MatchesFeaturelessHost::Never;
}
return compound_matches;
}
/// Returns an iterator over this selector in matching order (right-to-left).
/// When a combinator is reached, the iterator will return None, and
/// next_sequence() may be called to continue to the next sequence.
@@ -977,25 +1003,6 @@ impl<Impl: SelectorImpl> Selector<Impl> {
}
}
/// Whether this selector matches a featureless shadow host, with no combinators to the left, and
/// optionally has a pseudo-element to the right.
#[inline]
pub fn matches_featureless_host_selector_or_pseudo_element(&self) -> FeaturelessHostMatches {
let flags = self.flags();
let mut result = FeaturelessHostMatches::empty();
if flags.intersects(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT) {
return result;
}
if flags.intersects(SelectorFlags::HAS_HOST) {
result.insert(FeaturelessHostMatches::FOR_HOST);
}
if flags.intersects(SelectorFlags::HAS_SCOPE) {
result.insert(FeaturelessHostMatches::FOR_SCOPE);
}
result
}
/// Returns an iterator over this selector in matching order (right-to-left),
/// skipping the rightmost |offset| Components.
#[inline]
@@ -1244,7 +1251,7 @@ impl<Impl: SelectorImpl> Selector<Impl> {
parent,
&mut specificity,
&mut flags,
forbidden_flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
forbidden_flags,
))),
NthOf(ref data) => {
let selectors = replace_parent_on_selector_list(
@@ -1417,28 +1424,6 @@ impl<'a, Impl: 'a + SelectorImpl> SelectorIter<'a, Impl> {
self.next_combinator.take()
}
/// Whether this selector is a featureless selector matching the shadow host, with no
/// combinators to the left.
#[inline]
pub(crate) fn is_featureless_host_selector(&mut self) -> FeaturelessHostMatches {
if self.selector_length() == 0 {
return FeaturelessHostMatches::empty();
}
let mut result = FeaturelessHostMatches::empty();
while let Some(c) = self.next() {
let component_matches = c.matches_featureless_host();
if component_matches.is_empty() {
return FeaturelessHostMatches::empty();
}
result.insert(component_matches);
}
if self.next_sequence().is_some() {
FeaturelessHostMatches::empty()
} else {
result
}
}
#[inline]
pub(crate) fn matches_for_stateless_pseudo_element(&mut self) -> bool {
let first = match self.next() {
@@ -2121,32 +2106,6 @@ impl<Impl: SelectorImpl> Component<Impl> {
matches!(*self, Component::Host(..))
}
/// Returns if this component can match a featureless shadow host, and if so,
/// via which selector.
#[inline]
pub fn matches_featureless_host(&self) -> FeaturelessHostMatches {
match *self {
Component::Host(..) => FeaturelessHostMatches::FOR_HOST,
Component::Scope | Component::ImplicitScope => FeaturelessHostMatches::FOR_SCOPE,
Component::Where(ref l) | Component::Is(ref l) => {
debug_assert!(l.len() > 0, "Zero length selector?");
// TODO(emilio): For now we require that everything in logical combination can match
// the featureless shadow host, because not doing so brings up a fair amount of extra
// complexity (we can't make the decision on whether to walk out statically).
let mut result = FeaturelessHostMatches::empty();
for i in l.slice() {
if !result.insert_not_empty(
i.matches_featureless_host_selector_or_pseudo_element()
) {
return FeaturelessHostMatches::empty();
}
}
result
},
_ => FeaturelessHostMatches::empty(),
}
}
/// Returns the value as a combinator if applicable, None otherwise.
pub fn as_combinator(&self) -> Option<Combinator> {
match *self {
@@ -4051,7 +4010,7 @@ pub mod tests {
lower_name: DummyAtom::from("eeÉ"),
})],
specificity(0, 0, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4065,7 +4024,7 @@ pub mod tests {
}),
],
specificity(0, 0, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
// When the default namespace is not set, *| should be elided.
@@ -4078,7 +4037,7 @@ pub mod tests {
lower_name: DummyAtom::from("e"),
})],
specificity(0, 0, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
// When the default namespace is set, *| should _not_ be elided (as foo
@@ -4099,7 +4058,7 @@ pub mod tests {
}),
],
specificity(0, 0, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4107,7 +4066,7 @@ pub mod tests {
Ok(SelectorList::from_vec(vec![Selector::from_vec(
vec![Component::ExplicitUniversalType],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4118,7 +4077,7 @@ pub mod tests {
Component::ExplicitUniversalType,
],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4126,7 +4085,7 @@ pub mod tests {
Ok(SelectorList::from_vec(vec![Selector::from_vec(
vec![Component::ExplicitUniversalType],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4140,7 +4099,7 @@ pub mod tests {
Component::ExplicitUniversalType,
],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4151,7 +4110,7 @@ pub mod tests {
Component::NonTSPseudoClass(PseudoClass::Lang("en-US".to_owned())),
],
specificity(0, 2, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4159,7 +4118,7 @@ pub mod tests {
Ok(SelectorList::from_vec(vec![Selector::from_vec(
vec![Component::ID(DummyAtom::from("bar"))],
specificity(1, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4174,7 +4133,7 @@ pub mod tests {
Component::ID(DummyAtom::from("bar")),
],
specificity(1, 1, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4190,7 +4149,7 @@ pub mod tests {
Component::ID(DummyAtom::from("bar")),
],
specificity(1, 1, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
// Default namespace does not apply to attribute selectors
@@ -4204,7 +4163,7 @@ pub mod tests {
local_name_lower: DummyAtom::from("foo"),
}],
specificity(0, 1, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert!(parse_ns("svg|circle", &parser).is_err());
@@ -4222,7 +4181,7 @@ pub mod tests {
}),
],
specificity(0, 0, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4233,7 +4192,7 @@ pub mod tests {
Component::ExplicitUniversalType,
],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
// Default namespace does not apply to attribute selectors
@@ -4252,7 +4211,7 @@ pub mod tests {
},
],
specificity(0, 1, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
// Default namespace does apply to type selectors
@@ -4267,7 +4226,7 @@ pub mod tests {
}),
],
specificity(0, 0, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4278,7 +4237,7 @@ pub mod tests {
Component::ExplicitUniversalType,
],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4289,7 +4248,7 @@ pub mod tests {
Component::ExplicitUniversalType,
],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
// Default namespace applies to universal and type selectors inside :not and :matches,
@@ -4302,11 +4261,11 @@ pub mod tests {
Component::Negation(SelectorList::from_vec(vec![Selector::from_vec(
vec![Component::Class(DummyAtom::from("cl"))],
specificity(0, 1, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)])),
],
specificity(0, 1, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4320,11 +4279,11 @@ pub mod tests {
Component::ExplicitUniversalType,
],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]),),
],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4341,11 +4300,11 @@ pub mod tests {
}),
],
specificity(0, 0, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)])),
],
specificity(0, 0, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4358,7 +4317,7 @@ pub mod tests {
case_sensitivity: ParsedCaseSensitivity::CaseSensitive,
}],
specificity(0, 1, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
// https://github.com/mozilla/servo/issues/1723
@@ -4382,7 +4341,7 @@ pub mod tests {
Component::NonTSPseudoClass(PseudoClass::Hover),
],
specificity(0, 1, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO,
SelectorFlags::HAS_PSEUDO,
)]))
);
assert_eq!(
@@ -4395,7 +4354,7 @@ pub mod tests {
Component::NonTSPseudoClass(PseudoClass::Hover),
],
specificity(0, 2, 1),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO,
SelectorFlags::HAS_PSEUDO,
)]))
);
assert!(parse("::before:hover:lang(foo)").is_err());
@@ -4419,7 +4378,7 @@ pub mod tests {
Component::PseudoElement(PseudoElement::After),
],
specificity(0, 0, 2),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO,
SelectorFlags::HAS_PSEUDO,
)]))
);
assert_eq!(
@@ -4431,7 +4390,7 @@ pub mod tests {
Component::Class(DummyAtom::from("ok")),
],
(1 << 20) + (1 << 10) + (0 << 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
parser.default_ns = None;
@@ -4446,11 +4405,11 @@ pub mod tests {
Selector::from_vec(
vec![Component::ExplicitUniversalType],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)
]))],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
assert_eq!(
@@ -4463,11 +4422,11 @@ pub mod tests {
Component::ExplicitUniversalType,
],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)
]))],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
// *| should be elided if there is no default namespace.
@@ -4479,11 +4438,11 @@ pub mod tests {
Selector::from_vec(
vec![Component::ExplicitUniversalType],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)
]))],
specificity(0, 0, 0),
SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::empty(),
)]))
);
@@ -4526,7 +4485,7 @@ pub mod tests {
Component::Class(DummyAtom::from("bar")),
],
(1 << 20) + (1 << 10) + (0 << 0),
SelectorFlags::HAS_PARENT | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT
SelectorFlags::HAS_PARENT
)]))
);
@@ -4610,7 +4569,7 @@ pub mod tests {
Component::Class(DummyAtom::from("foo")),
],
specificity(0, 1, 0),
SelectorFlags::HAS_SCOPE | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::HAS_SCOPE,
)])
);
@@ -4623,7 +4582,7 @@ pub mod tests {
Component::Class(DummyAtom::from("foo")),
],
specificity(0, 2, 0),
SelectorFlags::HAS_SCOPE | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::HAS_SCOPE
)])
);
@@ -4636,7 +4595,7 @@ pub mod tests {
Component::Class(DummyAtom::from("foo")),
],
specificity(0, 1, 0),
SelectorFlags::HAS_SCOPE | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::HAS_SCOPE
)])
);
@@ -4651,26 +4610,11 @@ pub mod tests {
Component::Class(DummyAtom::from("bar")),
],
specificity(0, 3, 0),
SelectorFlags::HAS_SCOPE | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
SelectorFlags::HAS_SCOPE
)])
);
}
#[test]
fn test_featureless() {
let featureless = parse(":host, :scope").unwrap();
assert_eq!(featureless.slice().len(), 2);
for selector in featureless.slice() {
assert!(!selector.flags().intersects(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT));
}
let non_featureless = parse(":host.foo, :scope.foo, :host .foo, :scope .foo").unwrap();
assert_eq!(non_featureless.slice().len(), 4);
for selector in non_featureless.slice() {
assert!(selector.flags().intersects(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT));
}
}
struct TestVisitor {
seen: Vec<String>,
}

View File

@@ -366,7 +366,10 @@ where
let cascade_level = CascadeLevel::AuthorNormal {
shadow_cascade_order,
};
debug_assert!(!collector.context.featureless(), "How?");
collector.context.featureless = true;
collector.collect_rules_in_map(host_rules, cascade_level, style_data);
collector.context.featureless = false;
});
}

View File

@@ -67,7 +67,7 @@ use selectors::matching::{
};
use selectors::matching::{MatchingForInvalidation, VisitedHandlingMode};
use selectors::parser::{
AncestorHashes, Combinator, Component, FeaturelessHostMatches, Selector, SelectorIter,
AncestorHashes, Combinator, Component, MatchesFeaturelessHost, Selector, SelectorIter,
SelectorList,
};
use selectors::visitor::{SelectorListKind, SelectorVisitor};
@@ -2741,6 +2741,14 @@ fn parent_selector_for_scope(parent: Option<&SelectorList<SelectorImpl>>) -> &Se
}
}
fn scope_start_matches_shadow_host(start: &SelectorList<SelectorImpl>) -> bool {
// TODO(emilio): Should we carry a MatchesFeaturelessHost rather than a bool around?
// Pre-existing behavior with multiple selectors matches this tho.
start.slice().iter().any(|s| {
s.matches_featureless_host(true).may_match()
})
}
impl CascadeData {
/// Creates an empty `CascadeData`.
pub fn new() -> Self {
@@ -3034,10 +3042,7 @@ impl CascadeData {
}
(
ScopeTarget::Selector(&start.selectors),
start.selectors.slice().iter().any(|s| {
!s.matches_featureless_host_selector_or_pseudo_element()
.is_empty()
}),
scope_start_matches_shadow_host(&start.selectors),
)
} else {
let implicit_root = condition_ref.implicit_scope_root;
@@ -3362,28 +3367,27 @@ impl CascadeData {
vec.try_reserve(1)?;
vec.push(rule);
} else {
let scope_matches_shadow_host = containing_rule_state.scope_matches_shadow_host == ScopeMatchesShadowHost::Yes;
let matches_featureless_host_only = match rule.selector.matches_featureless_host(scope_matches_shadow_host) {
MatchesFeaturelessHost::Only => true,
MatchesFeaturelessHost::Yes => {
// We need to insert this in featureless_host_rules but also normal_rules.
self.featureless_host_rules
.get_or_insert_with(|| Box::new(Default::default()))
.for_insertion(pseudo_element)
.insert(rule.clone(), quirks_mode)?;
false
},
MatchesFeaturelessHost::Never => false,
};
// 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.
// :scope may match featureless shadow host if the scope
// root is the shadow root.
// See https://github.com/w3c/csswg-drafts/issues/9025
let potentially_matches_featureless_host = rule
.selector
.matches_featureless_host_selector_or_pseudo_element();
let matches_featureless_host = if potentially_matches_featureless_host
.intersects(FeaturelessHostMatches::FOR_HOST)
{
true
} else if potentially_matches_featureless_host
.intersects(FeaturelessHostMatches::FOR_SCOPE)
{
containing_rule_state.scope_matches_shadow_host ==
ScopeMatchesShadowHost::Yes
} else {
false
};
let rules = if matches_featureless_host {
let rules = if matches_featureless_host_only {
self.featureless_host_rules
.get_or_insert_with(|| Box::new(Default::default()))
} else if rule.selector.is_slotted() {
@@ -3685,10 +3689,7 @@ impl CascadeData {
let id = ScopeConditionId(self.scope_conditions.len() as u16);
let mut matches_shadow_host = false;
let implicit_scope_root = if let Some(start) = rule.bounds.start.as_ref() {
matches_shadow_host = start.slice().iter().any(|s| {
!s.matches_featureless_host_selector_or_pseudo_element()
.is_empty()
});
matches_shadow_host = scope_start_matches_shadow_host(start);
// Would be unused, but use the default as fallback.
StylistImplicitScopeRoot::default()
} else {