style: Add simple parsing and matching support for :has

Parsing is behind a config value `layout.css.has-selectors.enabled`. This
change does not support p:has(> a) combinators, but will handle them
gracefully, just not matching on them.

Differential Revision: https://phabricator.services.mozilla.com/D149515
This commit is contained in:
Tiaan Louw 2022-06-20 08:53:02 +00:00 committed by Martin Robinson
parent dcdf9f33d5
commit 3d0cf4dbf9
8 changed files with 85 additions and 10 deletions

View file

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

View file

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

View file

@ -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<E: Element>(
selectors: &[Selector<E::Impl>],
element: &E,
context: &mut MatchingContext<E::Impl>,
) -> 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")
},

View file

@ -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<Impl: SelectorImpl> {
///
/// Same comment as above re. the argument.
Is(Box<[Selector<Impl>]>),
/// The `:has` pseudo-class.
///
/// https://drafts.csswg.org/selectors/#has-pseudo
///
/// Same comment as above re. the argument.
Has(Box<[Selector<Impl>]>),
/// An implementation-dependent pseudo-element selector.
PseudoElement(#[cfg_attr(feature = "shmem", shmem(field_bound))] Impl::PseudoElement),
@ -1589,11 +1600,12 @@ impl<Impl: SelectorImpl> ToCss for Component<Impl> {
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
}

View file

@ -60,6 +60,9 @@ pub trait Element: Sized + Clone + Debug {
/// Skips non-element nodes
fn next_sibling_element(&self) -> Option<Self>;
/// Skips non-element nodes
fn first_element_child(&self) -> Option<Self>;
fn is_html_element_in_html_document(&self) -> bool;
fn has_local_name(&self, local_name: &<Self::Impl as SelectorImpl>::BorrowedLocalName) -> bool;

View file

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

View file

@ -1844,6 +1844,18 @@ impl<'le> ::selectors::Element for GeckoElement<'le> {
None
}
#[inline]
fn first_element_child(&self) -> Option<Self> {
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));

View file

@ -285,6 +285,11 @@ where
Some(Self::new(sibling, self.snapshot_map))
}
fn first_element_child(&self) -> Option<Self> {
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()