mirror of
https://github.com/servo/servo.git
synced 2025-08-03 20:50:07 +01:00
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:
parent
dcdf9f33d5
commit
3d0cf4dbf9
8 changed files with 85 additions and 10 deletions
|
@ -695,7 +695,9 @@ where
|
||||||
Component::Slotted(ref selector) | Component::Host(Some(ref selector)) => {
|
Component::Slotted(ref selector) | Component::Host(Some(ref selector)) => {
|
||||||
selector.size_of(ops)
|
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::PseudoElement(ref pseudo) => (*pseudo).size_of(ops),
|
||||||
Component::Combinator(..) |
|
Component::Combinator(..) |
|
||||||
Component::ExplicitAnyNamespace |
|
Component::ExplicitAnyNamespace |
|
||||||
|
|
|
@ -332,12 +332,14 @@ where
|
||||||
Component::NonTSPseudoClass(..) => {
|
Component::NonTSPseudoClass(..) => {
|
||||||
specificity.class_like_selectors += 1;
|
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:
|
// https://drafts.csswg.org/selectors/#specificity-rules:
|
||||||
//
|
//
|
||||||
// The specificity of an :is() pseudo-class is replaced by the
|
// The specificity of an :is(), :not(), or :has() pseudo-class
|
||||||
// specificity of the most specific complex selector in its
|
// is replaced by the specificity of the most specific complex
|
||||||
// selector list argument.
|
// selector in its selector list argument.
|
||||||
let mut max = 0;
|
let mut max = 0;
|
||||||
for selector in &**list {
|
for selector in &**list {
|
||||||
max = std::cmp::max(selector.specificity(), max);
|
max = std::cmp::max(selector.specificity(), max);
|
||||||
|
|
|
@ -326,6 +326,32 @@ where
|
||||||
matches!(result, SelectorMatchingResult::Matched)
|
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.
|
/// Whether the :hover and :active quirk applies.
|
||||||
///
|
///
|
||||||
/// https://quirks.spec.whatwg.org/#the-active-and-hover-quirk
|
/// https://quirks.spec.whatwg.org/#the-active-and-hover-quirk
|
||||||
|
@ -833,6 +859,9 @@ where
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}),
|
}),
|
||||||
|
Component::Has(ref list) => context.shared.nest(|context| {
|
||||||
|
has_children_matching(list, element, context)
|
||||||
|
}),
|
||||||
Component::Combinator(_) => unsafe {
|
Component::Combinator(_) => unsafe {
|
||||||
debug_unreachable!("Shouldn't try to selector-match combinators")
|
debug_unreachable!("Shouldn't try to selector-match combinators")
|
||||||
},
|
},
|
||||||
|
|
|
@ -261,6 +261,11 @@ pub trait Parser<'i> {
|
||||||
false
|
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.
|
/// Whether the given function name is an alias for the `:is()` function.
|
||||||
fn is_is_alias(&self, _name: &str) -> bool {
|
fn is_is_alias(&self, _name: &str) -> bool {
|
||||||
false
|
false
|
||||||
|
@ -1116,6 +1121,12 @@ pub enum Component<Impl: SelectorImpl> {
|
||||||
///
|
///
|
||||||
/// Same comment as above re. the argument.
|
/// Same comment as above re. the argument.
|
||||||
Is(Box<[Selector<Impl>]>),
|
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.
|
/// An implementation-dependent pseudo-element selector.
|
||||||
PseudoElement(#[cfg_attr(feature = "shmem", shmem(field_bound))] Impl::PseudoElement),
|
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)?;
|
write_affine(dest, a, b)?;
|
||||||
dest.write_char(')')
|
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 {
|
match *self {
|
||||||
Where(..) => dest.write_str(":where(")?,
|
Where(..) => dest.write_str(":where(")?,
|
||||||
Is(..) => dest.write_str(":is(")?,
|
Is(..) => dest.write_str(":is(")?,
|
||||||
Negation(..) => dest.write_str(":not(")?,
|
Negation(..) => dest.write_str(":not(")?,
|
||||||
|
Has(..) => dest.write_str(":has(")?,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
serialize_selector_list(list.iter(), dest)?;
|
serialize_selector_list(list.iter(), dest)?;
|
||||||
|
@ -2253,7 +2265,7 @@ where
|
||||||
Ok(empty)
|
Ok(empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_is_or_where<'i, 't, P, Impl>(
|
fn parse_is_where_has<'i, 't, P, Impl>(
|
||||||
parser: &P,
|
parser: &P,
|
||||||
input: &mut CssParser<'i, 't>,
|
input: &mut CssParser<'i, 't>,
|
||||||
state: SelectorParsingState,
|
state: SelectorParsingState,
|
||||||
|
@ -2295,8 +2307,9 @@ where
|
||||||
"nth-of-type" => return parse_nth_pseudo_class(parser, input, state, Component::NthOfType),
|
"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-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),
|
"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),
|
"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_or_where(parser, input, state, Component::Where),
|
"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" => {
|
"host" => {
|
||||||
if !state.allows_tree_structural_pseudo_classes() {
|
if !state.allows_tree_structural_pseudo_classes() {
|
||||||
return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
|
return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
|
||||||
|
@ -2310,7 +2323,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
if parser.parse_is_and_where() && parser.is_is_alias(&name) {
|
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() {
|
if !state.allows_custom_functional_pseudo_classes() {
|
||||||
|
@ -2684,6 +2697,10 @@ pub mod tests {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_has(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_part(&self) -> bool {
|
fn parse_part(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,9 @@ pub trait Element: Sized + Clone + Debug {
|
||||||
/// Skips non-element nodes
|
/// Skips non-element nodes
|
||||||
fn next_sibling_element(&self) -> Option<Self>;
|
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 is_html_element_in_html_document(&self) -> bool;
|
||||||
|
|
||||||
fn has_local_name(&self, local_name: &<Self::Impl as SelectorImpl>::BorrowedLocalName) -> bool;
|
fn has_local_name(&self, local_name: &<Self::Impl as SelectorImpl>::BorrowedLocalName) -> bool;
|
||||||
|
|
|
@ -305,6 +305,11 @@ impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn parse_has(&self) -> bool {
|
||||||
|
static_prefs::pref!("layout.css.has-selector.enabled")
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn parse_part(&self) -> bool {
|
fn parse_part(&self) -> bool {
|
||||||
true
|
true
|
||||||
|
|
|
@ -1844,6 +1844,18 @@ impl<'le> ::selectors::Element for GeckoElement<'le> {
|
||||||
None
|
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) {
|
fn set_selector_flags(&self, flags: ElementSelectorFlags) {
|
||||||
debug_assert!(!flags.is_empty());
|
debug_assert!(!flags.is_empty());
|
||||||
self.set_flags(selector_flags_to_node_flags(flags));
|
self.set_flags(selector_flags_to_node_flags(flags));
|
||||||
|
|
|
@ -285,6 +285,11 @@ where
|
||||||
Some(Self::new(sibling, self.snapshot_map))
|
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]
|
#[inline]
|
||||||
fn is_html_element_in_html_document(&self) -> bool {
|
fn is_html_element_in_html_document(&self) -> bool {
|
||||||
self.element.is_html_element_in_html_document()
|
self.element.is_html_element_in_html_document()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue