diff --git a/components/malloc_size_of/lib.rs b/components/malloc_size_of/lib.rs index 9a7be7423e6..4011746b300 100644 --- a/components/malloc_size_of/lib.rs +++ b/components/malloc_size_of/lib.rs @@ -695,7 +695,9 @@ where Component::Slotted(ref selector) | Component::Host(Some(ref selector)) => { selector.size_of(ops) }, - Component::Is(ref list) | Component::Where(ref list) => list.size_of(ops), + Component::Is(ref list) | + Component::Where(ref list) | + Component::Has(ref list) => list.size_of(ops), Component::PseudoElement(ref pseudo) => (*pseudo).size_of(ops), Component::Combinator(..) | Component::ExplicitAnyNamespace | diff --git a/components/selectors/builder.rs b/components/selectors/builder.rs index 6a3f5badf2b..59bb2750ec9 100644 --- a/components/selectors/builder.rs +++ b/components/selectors/builder.rs @@ -332,12 +332,14 @@ where Component::NonTSPseudoClass(..) => { specificity.class_like_selectors += 1; }, - Component::Negation(ref list) | Component::Is(ref list) => { + Component::Negation(ref list) | + Component::Is(ref list) | + Component::Has(ref list) => { // https://drafts.csswg.org/selectors/#specificity-rules: // - // The specificity of an :is() pseudo-class is replaced by the - // specificity of the most specific complex selector in its - // selector list argument. + // The specificity of an :is(), :not(), or :has() pseudo-class + // is replaced by the specificity of the most specific complex + // selector in its selector list argument. let mut max = 0; for selector in &**list { max = std::cmp::max(selector.specificity(), max); diff --git a/components/selectors/matching.rs b/components/selectors/matching.rs index c41ff0d362f..348e98b89c9 100644 --- a/components/selectors/matching.rs +++ b/components/selectors/matching.rs @@ -326,6 +326,32 @@ where matches!(result, SelectorMatchingResult::Matched) } +/// Traverse all descendents of the given element and return true as soon as any of them match +/// the given list of selectors. +fn has_children_matching( + selectors: &[Selector], + element: &E, + context: &mut MatchingContext, +) -> bool { + let mut current = element.first_element_child(); + + while let Some(el) = current { + for selector in selectors { + if matches_complex_selector(selector.iter(), &el, context) { + return true; + } + } + + if has_children_matching(selectors, &el, context) { + return true; + } + + current = el.next_sibling_element(); + } + + false +} + /// Whether the :hover and :active quirk applies. /// /// https://quirks.spec.whatwg.org/#the-active-and-hover-quirk @@ -833,6 +859,9 @@ where } true }), + Component::Has(ref list) => context.shared.nest(|context| { + has_children_matching(list, element, context) + }), Component::Combinator(_) => unsafe { debug_unreachable!("Shouldn't try to selector-match combinators") }, diff --git a/components/selectors/parser.rs b/components/selectors/parser.rs index 2720d723568..2fc5ad02d39 100644 --- a/components/selectors/parser.rs +++ b/components/selectors/parser.rs @@ -261,6 +261,11 @@ pub trait Parser<'i> { false } + /// Whether to parse the :has pseudo-class. + fn parse_has(&self) -> bool { + false + } + /// Whether the given function name is an alias for the `:is()` function. fn is_is_alias(&self, _name: &str) -> bool { false @@ -1116,6 +1121,12 @@ pub enum Component { /// /// Same comment as above re. the argument. Is(Box<[Selector]>), + /// The `:has` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#has-pseudo + /// + /// Same comment as above re. the argument. + Has(Box<[Selector]>), /// An implementation-dependent pseudo-element selector. PseudoElement(#[cfg_attr(feature = "shmem", shmem(field_bound))] Impl::PseudoElement), @@ -1589,11 +1600,12 @@ impl ToCss for Component { write_affine(dest, a, b)?; dest.write_char(')') }, - Is(ref list) | Where(ref list) | Negation(ref list) => { + Is(ref list) | Where(ref list) | Negation(ref list) | Has(ref list) => { match *self { Where(..) => dest.write_str(":where(")?, Is(..) => dest.write_str(":is(")?, Negation(..) => dest.write_str(":not(")?, + Has(..) => dest.write_str(":has(")?, _ => unreachable!(), } serialize_selector_list(list.iter(), dest)?; @@ -2253,7 +2265,7 @@ where Ok(empty) } -fn parse_is_or_where<'i, 't, P, Impl>( +fn parse_is_where_has<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, state: SelectorParsingState, @@ -2295,8 +2307,9 @@ where "nth-of-type" => return parse_nth_pseudo_class(parser, input, state, Component::NthOfType), "nth-last-child" => return parse_nth_pseudo_class(parser, input, state, Component::NthLastChild), "nth-last-of-type" => return parse_nth_pseudo_class(parser, input, state, Component::NthLastOfType), - "is" if parser.parse_is_and_where() => return parse_is_or_where(parser, input, state, Component::Is), - "where" if parser.parse_is_and_where() => return parse_is_or_where(parser, input, state, Component::Where), + "is" if parser.parse_is_and_where() => return parse_is_where_has(parser, input, state, Component::Is), + "where" if parser.parse_is_and_where() => return parse_is_where_has(parser, input, state, Component::Where), + "has" if parser.parse_has() => return parse_is_where_has(parser, input, state, Component::Has), "host" => { if !state.allows_tree_structural_pseudo_classes() { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); @@ -2310,7 +2323,7 @@ where } if parser.parse_is_and_where() && parser.is_is_alias(&name) { - return parse_is_or_where(parser, input, state, Component::Is); + return parse_is_where_has(parser, input, state, Component::Is); } if !state.allows_custom_functional_pseudo_classes() { @@ -2684,6 +2697,10 @@ pub mod tests { true } + fn parse_has(&self) -> bool { + true + } + fn parse_part(&self) -> bool { true } diff --git a/components/selectors/tree.rs b/components/selectors/tree.rs index c1f0f9341c7..7d6c1bed967 100644 --- a/components/selectors/tree.rs +++ b/components/selectors/tree.rs @@ -60,6 +60,9 @@ pub trait Element: Sized + Clone + Debug { /// Skips non-element nodes fn next_sibling_element(&self) -> Option; + /// Skips non-element nodes + fn first_element_child(&self) -> Option; + fn is_html_element_in_html_document(&self) -> bool; fn has_local_name(&self, local_name: &::BorrowedLocalName) -> bool; diff --git a/components/style/gecko/selector_parser.rs b/components/style/gecko/selector_parser.rs index 50de91d6c47..aee0ce7fe35 100644 --- a/components/style/gecko/selector_parser.rs +++ b/components/style/gecko/selector_parser.rs @@ -305,6 +305,11 @@ impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> { true } + #[inline] + fn parse_has(&self) -> bool { + static_prefs::pref!("layout.css.has-selector.enabled") + } + #[inline] fn parse_part(&self) -> bool { true diff --git a/components/style/gecko/wrapper.rs b/components/style/gecko/wrapper.rs index 19ee84fbc83..8381c525365 100644 --- a/components/style/gecko/wrapper.rs +++ b/components/style/gecko/wrapper.rs @@ -1844,6 +1844,18 @@ impl<'le> ::selectors::Element for GeckoElement<'le> { None } + #[inline] + fn first_element_child(&self) -> Option { + let mut child = self.as_node().first_child(); + while let Some(child_node) = child { + if let Some(el) = child_node.as_element() { + return Some(el); + } + child = child_node.next_sibling(); + } + None + } + fn set_selector_flags(&self, flags: ElementSelectorFlags) { debug_assert!(!flags.is_empty()); self.set_flags(selector_flags_to_node_flags(flags)); diff --git a/components/style/invalidation/element/element_wrapper.rs b/components/style/invalidation/element/element_wrapper.rs index 388097f53e8..4530c249c23 100644 --- a/components/style/invalidation/element/element_wrapper.rs +++ b/components/style/invalidation/element/element_wrapper.rs @@ -285,6 +285,11 @@ where Some(Self::new(sibling, self.snapshot_map)) } + fn first_element_child(&self) -> Option { + let child = self.element.first_element_child()?; + Some(Self::new(child, self.snapshot_map)) + } + #[inline] fn is_html_element_in_html_document(&self) -> bool { self.element.is_html_element_in_html_document()