2025-01-12 18:38:05 +03:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
2026-05-24 12:23:13 +02:00
|
|
|
#include <AK/Assertions.h>
|
2025-01-12 18:38:05 +03:00
|
|
|
#include <AK/GenericShorthands.h>
|
2026-03-08 20:36:20 +01:00
|
|
|
#include <AK/Optional.h>
|
2025-01-12 18:38:05 +03:00
|
|
|
#include <LibWeb/CSS/Selector.h>
|
|
|
|
|
#include <LibWeb/CSS/StyleInvalidationData.h>
|
|
|
|
|
|
|
|
|
|
namespace Web::CSS {
|
|
|
|
|
|
2026-04-17 23:22:26 +02:00
|
|
|
static void append_or_merge_descendant_rule(Vector<DescendantInvalidationRule>& rules, DescendantInvalidationRule const& rule)
|
|
|
|
|
{
|
|
|
|
|
for (auto& existing_rule : rules) {
|
|
|
|
|
if (existing_rule.match_any != rule.match_any)
|
|
|
|
|
continue;
|
|
|
|
|
if (*existing_rule.payload != *rule.payload)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (existing_rule.match_any)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
existing_rule.match_set.include_all_from(rule.match_set);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
rules.append(rule);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void append_or_merge_sibling_rule(Vector<SiblingInvalidationRule>& rules, SiblingInvalidationRule const& rule)
|
|
|
|
|
{
|
|
|
|
|
for (auto& existing_rule : rules) {
|
|
|
|
|
if (existing_rule.reach != rule.reach)
|
|
|
|
|
continue;
|
|
|
|
|
if (existing_rule.match_any != rule.match_any)
|
|
|
|
|
continue;
|
|
|
|
|
if (*existing_rule.payload != *rule.payload)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (existing_rule.match_any)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
existing_rule.match_set.include_all_from(rule.match_set);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
rules.append(rule);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 12:23:13 +02:00
|
|
|
static void append_or_merge_guarded_rule(Vector<GuardedInvalidationRule>& rules, GuardedInvalidationRule const& rule)
|
|
|
|
|
{
|
|
|
|
|
for (auto& existing_rule : rules) {
|
|
|
|
|
if (existing_rule.guard != rule.guard)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
existing_rule.payload->include_all_from(*rule.payload);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
rules.append(rule);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 20:36:20 +01:00
|
|
|
bool InvalidationPlan::is_empty() const
|
|
|
|
|
{
|
2026-05-24 12:23:13 +02:00
|
|
|
return !invalidate_self && !invalidate_whole_subtree && descendant_rules.is_empty() && sibling_rules.is_empty() && guarded_rules.is_empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool InvalidationGuard::operator==(InvalidationGuard const& other) const
|
|
|
|
|
{
|
|
|
|
|
return property_sets == other.property_sets;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GuardedInvalidationRule::operator==(GuardedInvalidationRule const& other) const
|
|
|
|
|
{
|
|
|
|
|
return guard == other.guard
|
|
|
|
|
&& *payload == *other.payload;
|
2026-03-08 20:36:20 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:22:26 +02:00
|
|
|
bool DescendantInvalidationRule::operator==(DescendantInvalidationRule const& other) const
|
|
|
|
|
{
|
|
|
|
|
return match_set == other.match_set
|
|
|
|
|
&& match_any == other.match_any
|
|
|
|
|
&& *payload == *other.payload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool SiblingInvalidationRule::operator==(SiblingInvalidationRule const& other) const
|
|
|
|
|
{
|
|
|
|
|
return reach == other.reach
|
|
|
|
|
&& match_set == other.match_set
|
|
|
|
|
&& match_any == other.match_any
|
|
|
|
|
&& *payload == *other.payload;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:30:07 +02:00
|
|
|
template<typename Rule>
|
|
|
|
|
static bool rule_lists_are_equal_ignoring_order(Vector<Rule> const& a, Vector<Rule> const& b)
|
|
|
|
|
{
|
|
|
|
|
if (a.size() != b.size())
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
Vector<bool> matched_rules;
|
|
|
|
|
matched_rules.resize(b.size());
|
|
|
|
|
|
|
|
|
|
for (auto const& rule : a) {
|
|
|
|
|
bool found_match = false;
|
|
|
|
|
for (size_t i = 0; i < b.size(); ++i) {
|
|
|
|
|
if (matched_rules[i])
|
|
|
|
|
continue;
|
|
|
|
|
if (!(rule == b[i]))
|
|
|
|
|
continue;
|
|
|
|
|
matched_rules[i] = true;
|
|
|
|
|
found_match = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (!found_match)
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:22:26 +02:00
|
|
|
bool InvalidationPlan::operator==(InvalidationPlan const& other) const
|
|
|
|
|
{
|
|
|
|
|
if (invalidate_self != other.invalidate_self)
|
|
|
|
|
return false;
|
|
|
|
|
if (invalidate_whole_subtree != other.invalidate_whole_subtree)
|
|
|
|
|
return false;
|
|
|
|
|
|
2026-04-17 23:30:07 +02:00
|
|
|
return rule_lists_are_equal_ignoring_order(descendant_rules, other.descendant_rules)
|
2026-05-24 12:23:13 +02:00
|
|
|
&& rule_lists_are_equal_ignoring_order(sibling_rules, other.sibling_rules)
|
|
|
|
|
&& rule_lists_are_equal_ignoring_order(guarded_rules, other.guarded_rules);
|
2026-04-17 23:22:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-08 20:36:20 +01:00
|
|
|
void InvalidationPlan::include_all_from(InvalidationPlan const& other)
|
|
|
|
|
{
|
|
|
|
|
invalidate_self |= other.invalidate_self;
|
|
|
|
|
|
|
|
|
|
if (invalidate_whole_subtree)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (other.invalidate_whole_subtree) {
|
|
|
|
|
invalidate_whole_subtree = true;
|
|
|
|
|
descendant_rules.clear();
|
|
|
|
|
sibling_rules.clear();
|
2026-05-24 12:23:13 +02:00
|
|
|
guarded_rules.clear();
|
2026-03-08 20:36:20 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:22:26 +02:00
|
|
|
for (auto const& descendant_rule : other.descendant_rules)
|
|
|
|
|
append_or_merge_descendant_rule(descendant_rules, descendant_rule);
|
|
|
|
|
for (auto const& sibling_rule : other.sibling_rules)
|
|
|
|
|
append_or_merge_sibling_rule(sibling_rules, sibling_rule);
|
2026-05-24 12:23:13 +02:00
|
|
|
for (auto const& guarded_rule : other.guarded_rules)
|
|
|
|
|
append_or_merge_guarded_rule(guarded_rules, guarded_rule);
|
2026-03-08 20:36:20 +01:00
|
|
|
}
|
|
|
|
|
|
2025-01-12 18:38:05 +03:00
|
|
|
// Iterates over the given selector, grouping consecutive simple selectors that have no combinator (Combinator::None).
|
|
|
|
|
// For example, given "div:not(.a) + .b[foo]", the callback is invoked twice:
|
|
|
|
|
// once for "div:not(.a)" and once for ".b[foo]".
|
|
|
|
|
template<typename Callback>
|
|
|
|
|
static void for_each_consecutive_simple_selector_group(Selector const& selector, Callback callback)
|
|
|
|
|
{
|
|
|
|
|
auto const& compound_selectors = selector.compound_selectors();
|
|
|
|
|
int compound_selector_index = compound_selectors.size() - 1;
|
|
|
|
|
Vector<Selector::SimpleSelector const&> simple_selectors;
|
|
|
|
|
Selector::Combinator combinator = Selector::Combinator::None;
|
|
|
|
|
bool is_rightmost = true;
|
|
|
|
|
while (compound_selector_index >= 0) {
|
|
|
|
|
if (!simple_selectors.is_empty()) {
|
|
|
|
|
callback(simple_selectors, combinator, is_rightmost);
|
|
|
|
|
simple_selectors.clear();
|
|
|
|
|
is_rightmost = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto const& compound_selector = compound_selectors[compound_selector_index];
|
2026-03-08 20:36:20 +01:00
|
|
|
for (auto const& simple_selector : compound_selector.simple_selectors)
|
2025-01-12 18:38:05 +03:00
|
|
|
simple_selectors.append(simple_selector);
|
|
|
|
|
combinator = compound_selector.combinator;
|
|
|
|
|
|
|
|
|
|
--compound_selector_index;
|
|
|
|
|
}
|
2026-03-08 20:36:20 +01:00
|
|
|
if (!simple_selectors.is_empty())
|
2025-01-12 18:38:05 +03:00
|
|
|
callback(simple_selectors, combinator, is_rightmost);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 08:57:12 +02:00
|
|
|
static HasArgumentScope classify_has_argument_scope(Selector const& selector)
|
|
|
|
|
{
|
|
|
|
|
if (selector.compound_selectors().is_empty())
|
|
|
|
|
return HasArgumentScope::Complex;
|
|
|
|
|
|
|
|
|
|
auto leftmost_combinator = selector.compound_selectors().first().combinator;
|
|
|
|
|
switch (leftmost_combinator) {
|
|
|
|
|
case Selector::Combinator::Descendant:
|
|
|
|
|
return HasArgumentScope::AllDescendants;
|
|
|
|
|
case Selector::Combinator::ImmediateChild:
|
|
|
|
|
return selector.compound_selectors().size() == 1 ? HasArgumentScope::ChildrenOnly : HasArgumentScope::Complex;
|
|
|
|
|
case Selector::Combinator::NextSibling:
|
|
|
|
|
return selector.compound_selectors().size() == 1 ? HasArgumentScope::NextSiblingOnly : HasArgumentScope::Complex;
|
|
|
|
|
case Selector::Combinator::SubsequentSibling:
|
|
|
|
|
return selector.compound_selectors().size() == 1 ? HasArgumentScope::AllFollowingSiblings : HasArgumentScope::Complex;
|
|
|
|
|
default:
|
|
|
|
|
return HasArgumentScope::Complex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename Key>
|
|
|
|
|
static void append_has_invalidation_metadata(HashMap<Key, Vector<HasInvalidationMetadata>>& map, Key const& key, HasInvalidationMetadata const& metadata)
|
|
|
|
|
{
|
|
|
|
|
auto& bucket = map.ensure(key, [] { return Vector<HasInvalidationMetadata> {}; });
|
|
|
|
|
if (!bucket.contains_slow(metadata))
|
|
|
|
|
bucket.append(metadata);
|
|
|
|
|
}
|
|
|
|
|
|
LibWeb: Avoid unrelated structural :has invalidation
Track the simple selector features that appear inside :has() arguments
on each StyleScope, then consult that metadata before scheduling an
ancestor walk for a structural mutation. If the mutated subtree has no
tag, id, class, attribute, or pseudo-class feature that any cached
:has() argument cares about, skip the walk entirely.
Stay conservative for featureless-sensitive arguments such as :has(*),
:has(:not(...)), :has(:empty), and child-index pseudos: an unfeatured
node can still start or stop matching there. Track that case via a new
has_selectors_sensitive_to_featureless_subtree_changes flag on
StyleInvalidationData and fall back to the old conservative walk.
Stay conservative for pseudo-classes the subtree filter cannot probe
(:focus, :hover, validation pseudos). Move :default out of the set of
trackable feature pseudo-classes for the same reason; it now triggers
the conservative walk where it previously recorded metadata.
Tag and attribute names are stored lowercased, so for non-HTML elements
(SVG, MathML) treat lowercased matches as scheduling hints only; the
actual :has() match still goes through case-sensitive selector matching.
Test counter expectations are rebaselined to reflect the skipped walks
and reduced recomputations. Matching behavior is unchanged.
2026-04-27 22:14:05 +02:00
|
|
|
static bool selector_contains_featureless_subtree_sensitive_selector(Selector const&);
|
|
|
|
|
|
|
|
|
|
static bool pseudo_class_can_be_used_as_has_invalidation_feature(PseudoClass pseudo_class)
|
|
|
|
|
{
|
|
|
|
|
return first_is_one_of(pseudo_class,
|
|
|
|
|
PseudoClass::Enabled,
|
|
|
|
|
PseudoClass::Disabled,
|
|
|
|
|
PseudoClass::Defined,
|
|
|
|
|
PseudoClass::PlaceholderShown,
|
|
|
|
|
PseudoClass::Checked,
|
|
|
|
|
PseudoClass::Required,
|
|
|
|
|
PseudoClass::Optional,
|
|
|
|
|
PseudoClass::Link,
|
|
|
|
|
PseudoClass::AnyLink,
|
|
|
|
|
PseudoClass::LocalLink);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 08:57:12 +02:00
|
|
|
static void collect_properties_used_in_has(Selector::SimpleSelector const& selector, StyleInvalidationData& style_invalidation_data, Optional<HasInvalidationMetadata> metadata)
|
2025-01-12 18:38:05 +03:00
|
|
|
{
|
|
|
|
|
switch (selector.type) {
|
|
|
|
|
case Selector::SimpleSelector::Type::Id: {
|
2026-04-17 08:57:12 +02:00
|
|
|
if (metadata.has_value())
|
|
|
|
|
append_has_invalidation_metadata(style_invalidation_data.ids_used_in_has_selectors, selector.name(), *metadata);
|
2025-01-12 18:38:05 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case Selector::SimpleSelector::Type::Class: {
|
2026-04-17 08:57:12 +02:00
|
|
|
if (metadata.has_value())
|
|
|
|
|
append_has_invalidation_metadata(style_invalidation_data.class_names_used_in_has_selectors, selector.name(), *metadata);
|
2025-01-12 18:38:05 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case Selector::SimpleSelector::Type::Attribute: {
|
2026-04-17 08:57:12 +02:00
|
|
|
if (metadata.has_value())
|
|
|
|
|
append_has_invalidation_metadata(style_invalidation_data.attribute_names_used_in_has_selectors, selector.attribute().qualified_name.name.lowercase_name, *metadata);
|
2025-01-12 18:38:05 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case Selector::SimpleSelector::Type::TagName: {
|
2026-04-17 08:57:12 +02:00
|
|
|
if (metadata.has_value())
|
|
|
|
|
append_has_invalidation_metadata(style_invalidation_data.tag_names_used_in_has_selectors, selector.qualified_name().name.lowercase_name, *metadata);
|
2025-01-12 18:38:05 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case Selector::SimpleSelector::Type::PseudoClass: {
|
|
|
|
|
auto const& pseudo_class = selector.pseudo_class();
|
LibWeb: Avoid unrelated structural :has invalidation
Track the simple selector features that appear inside :has() arguments
on each StyleScope, then consult that metadata before scheduling an
ancestor walk for a structural mutation. If the mutated subtree has no
tag, id, class, attribute, or pseudo-class feature that any cached
:has() argument cares about, skip the walk entirely.
Stay conservative for featureless-sensitive arguments such as :has(*),
:has(:not(...)), :has(:empty), and child-index pseudos: an unfeatured
node can still start or stop matching there. Track that case via a new
has_selectors_sensitive_to_featureless_subtree_changes flag on
StyleInvalidationData and fall back to the old conservative walk.
Stay conservative for pseudo-classes the subtree filter cannot probe
(:focus, :hover, validation pseudos). Move :default out of the set of
trackable feature pseudo-classes for the same reason; it now triggers
the conservative walk where it previously recorded metadata.
Tag and attribute names are stored lowercased, so for non-HTML elements
(SVG, MathML) treat lowercased matches as scheduling hints only; the
actual :has() match still goes through case-sensitive selector matching.
Test counter expectations are rebaselined to reflect the skipped walks
and reduced recomputations. Matching behavior is unchanged.
2026-04-27 22:14:05 +02:00
|
|
|
if (pseudo_class_can_be_used_as_has_invalidation_feature(pseudo_class.type)) {
|
2026-04-17 08:57:12 +02:00
|
|
|
if (metadata.has_value())
|
|
|
|
|
append_has_invalidation_metadata(style_invalidation_data.pseudo_classes_used_in_has_selectors, pseudo_class.type, *metadata);
|
LibWeb: Avoid unrelated structural :has invalidation
Track the simple selector features that appear inside :has() arguments
on each StyleScope, then consult that metadata before scheduling an
ancestor walk for a structural mutation. If the mutated subtree has no
tag, id, class, attribute, or pseudo-class feature that any cached
:has() argument cares about, skip the walk entirely.
Stay conservative for featureless-sensitive arguments such as :has(*),
:has(:not(...)), :has(:empty), and child-index pseudos: an unfeatured
node can still start or stop matching there. Track that case via a new
has_selectors_sensitive_to_featureless_subtree_changes flag on
StyleInvalidationData and fall back to the old conservative walk.
Stay conservative for pseudo-classes the subtree filter cannot probe
(:focus, :hover, validation pseudos). Move :default out of the set of
trackable feature pseudo-classes for the same reason; it now triggers
the conservative walk where it previously recorded metadata.
Tag and attribute names are stored lowercased, so for non-HTML elements
(SVG, MathML) treat lowercased matches as scheduling hints only; the
actual :has() match still goes through case-sensitive selector matching.
Test counter expectations are rebaselined to reflect the skipped walks
and reduced recomputations. Matching behavior is unchanged.
2026-04-27 22:14:05 +02:00
|
|
|
} else if (metadata.has_value() && !first_is_one_of(pseudo_class.type, PseudoClass::Has, PseudoClass::Is, PseudoClass::Where)) {
|
|
|
|
|
// The structural subtree filter can only compare concrete features and the pseudo-classes listed above.
|
|
|
|
|
// For other pseudo-classes, such as :focus, :default, and :valid, a featureless node can still start or
|
|
|
|
|
// stop matching. Keep the old conservative walk instead of trying to probe them generically.
|
|
|
|
|
style_invalidation_data.has_selectors_sensitive_to_featureless_subtree_changes = true;
|
2025-01-19 17:22:44 +01:00
|
|
|
}
|
2025-01-12 18:38:05 +03:00
|
|
|
for (auto const& child_selector : pseudo_class.argument_selector_list) {
|
2026-04-17 08:57:12 +02:00
|
|
|
Optional<HasInvalidationMetadata> child_metadata = metadata;
|
|
|
|
|
if (pseudo_class.type == PseudoClass::Has) {
|
|
|
|
|
child_metadata = HasInvalidationMetadata {
|
|
|
|
|
.relative_selector = child_selector.ptr(),
|
|
|
|
|
.scope = classify_has_argument_scope(*child_selector),
|
|
|
|
|
};
|
LibWeb: Avoid unrelated structural :has invalidation
Track the simple selector features that appear inside :has() arguments
on each StyleScope, then consult that metadata before scheduling an
ancestor walk for a structural mutation. If the mutated subtree has no
tag, id, class, attribute, or pseudo-class feature that any cached
:has() argument cares about, skip the walk entirely.
Stay conservative for featureless-sensitive arguments such as :has(*),
:has(:not(...)), :has(:empty), and child-index pseudos: an unfeatured
node can still start or stop matching there. Track that case via a new
has_selectors_sensitive_to_featureless_subtree_changes flag on
StyleInvalidationData and fall back to the old conservative walk.
Stay conservative for pseudo-classes the subtree filter cannot probe
(:focus, :hover, validation pseudos). Move :default out of the set of
trackable feature pseudo-classes for the same reason; it now triggers
the conservative walk where it previously recorded metadata.
Tag and attribute names are stored lowercased, so for non-HTML elements
(SVG, MathML) treat lowercased matches as scheduling hints only; the
actual :has() match still goes through case-sensitive selector matching.
Test counter expectations are rebaselined to reflect the skipped walks
and reduced recomputations. Matching behavior is unchanged.
2026-04-27 22:14:05 +02:00
|
|
|
// These selectors can match because a featureless node is inserted, removed, or moved.
|
|
|
|
|
// Since there is no concrete tag/class/id/attribute/pseudo-class feature to compare against
|
|
|
|
|
// later, structural invalidation must keep walking conservatively for them.
|
|
|
|
|
if (selector_contains_featureless_subtree_sensitive_selector(*child_selector))
|
|
|
|
|
style_invalidation_data.has_selectors_sensitive_to_featureless_subtree_changes = true;
|
2026-04-17 08:57:12 +02:00
|
|
|
}
|
2025-01-12 18:38:05 +03:00
|
|
|
for (auto const& compound_selector : child_selector->compound_selectors()) {
|
2026-03-08 20:36:20 +01:00
|
|
|
for (auto const& simple_selector : compound_selector.simple_selectors)
|
2026-04-17 08:57:12 +02:00
|
|
|
collect_properties_used_in_has(simple_selector, style_invalidation_data, child_metadata);
|
2025-01-12 18:38:05 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-04-25 16:15:25 +02:00
|
|
|
case Selector::SimpleSelector::Type::PseudoElement: {
|
|
|
|
|
// Pseudo-elements like ::slotted(.x:has(...)) carry a compound selector argument whose contents need the same
|
|
|
|
|
// recursive collection.
|
|
|
|
|
auto const& pseudo_element = selector.pseudo_element();
|
|
|
|
|
if (pseudo_element.type() == PseudoElement::Slotted) {
|
|
|
|
|
for (auto const& compound_selector : pseudo_element.compound_selector().compound_selectors()) {
|
|
|
|
|
for (auto const& simple_selector : compound_selector.simple_selectors)
|
|
|
|
|
collect_properties_used_in_has(simple_selector, style_invalidation_data, metadata);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-01-12 18:38:05 +03:00
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
LibWeb: Avoid unrelated structural :has invalidation
Track the simple selector features that appear inside :has() arguments
on each StyleScope, then consult that metadata before scheduling an
ancestor walk for a structural mutation. If the mutated subtree has no
tag, id, class, attribute, or pseudo-class feature that any cached
:has() argument cares about, skip the walk entirely.
Stay conservative for featureless-sensitive arguments such as :has(*),
:has(:not(...)), :has(:empty), and child-index pseudos: an unfeatured
node can still start or stop matching there. Track that case via a new
has_selectors_sensitive_to_featureless_subtree_changes flag on
StyleInvalidationData and fall back to the old conservative walk.
Stay conservative for pseudo-classes the subtree filter cannot probe
(:focus, :hover, validation pseudos). Move :default out of the set of
trackable feature pseudo-classes for the same reason; it now triggers
the conservative walk where it previously recorded metadata.
Tag and attribute names are stored lowercased, so for non-HTML elements
(SVG, MathML) treat lowercased matches as scheduling hints only; the
actual :has() match still goes through case-sensitive selector matching.
Test counter expectations are rebaselined to reflect the skipped walks
and reduced recomputations. Matching behavior is unchanged.
2026-04-27 22:14:05 +02:00
|
|
|
static bool simple_selector_is_featureless_subtree_sensitive(Selector::SimpleSelector const& selector)
|
|
|
|
|
{
|
|
|
|
|
switch (selector.type) {
|
|
|
|
|
case Selector::SimpleSelector::Type::Universal:
|
|
|
|
|
return true;
|
|
|
|
|
case Selector::SimpleSelector::Type::PseudoClass: {
|
|
|
|
|
auto const& pseudo_class = selector.pseudo_class();
|
|
|
|
|
switch (pseudo_class.type) {
|
|
|
|
|
case PseudoClass::Not:
|
|
|
|
|
case PseudoClass::Empty:
|
|
|
|
|
case PseudoClass::FirstChild:
|
|
|
|
|
case PseudoClass::LastChild:
|
|
|
|
|
case PseudoClass::OnlyChild:
|
|
|
|
|
case PseudoClass::FirstOfType:
|
|
|
|
|
case PseudoClass::LastOfType:
|
|
|
|
|
case PseudoClass::OnlyOfType:
|
|
|
|
|
case PseudoClass::NthChild:
|
|
|
|
|
case PseudoClass::NthLastChild:
|
|
|
|
|
case PseudoClass::NthOfType:
|
|
|
|
|
case PseudoClass::NthLastOfType:
|
|
|
|
|
return true;
|
|
|
|
|
case PseudoClass::Is:
|
|
|
|
|
case PseudoClass::Where:
|
|
|
|
|
for (auto const& child_selector : pseudo_class.argument_selector_list) {
|
|
|
|
|
if (selector_contains_featureless_subtree_sensitive_selector(*child_selector))
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool selector_contains_featureless_subtree_sensitive_selector(Selector const& selector)
|
|
|
|
|
{
|
|
|
|
|
for (auto const& compound_selector : selector.compound_selectors()) {
|
|
|
|
|
for (auto const& simple_selector : compound_selector.simple_selectors) {
|
|
|
|
|
if (simple_selector_is_featureless_subtree_sensitive(simple_selector))
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-24 22:48:45 +01:00
|
|
|
static InvalidationSet build_invalidation_sets_for_selector_impl(StyleInvalidationData& style_invalidation_data, Selector const& selector, InsideNthChildPseudoClass inside_nth_child_pseudo_class);
|
|
|
|
|
|
2025-02-08 19:09:08 +01:00
|
|
|
static void add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(Selector const& selector, StyleInvalidationData& style_invalidation_data);
|
|
|
|
|
|
2026-03-08 20:36:20 +01:00
|
|
|
static bool should_register_invalidation_property(InvalidationSet::Property const& property)
|
|
|
|
|
{
|
|
|
|
|
return !AK::first_is_one_of(property.type, InvalidationSet::Property::Type::InvalidateSelf, InvalidationSet::Property::Type::InvalidateWholeSubtree);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 12:23:13 +02:00
|
|
|
static void collect_guard_properties_for_simple_selector(Selector::SimpleSelector const&, InvalidationSet&);
|
|
|
|
|
|
|
|
|
|
static Optional<InvalidationSet> build_invalidation_guard_property_set_for_selector_subject(Selector const& selector)
|
|
|
|
|
{
|
|
|
|
|
if (selector.compound_selectors().is_empty())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
InvalidationSet property_set;
|
|
|
|
|
for (auto const& simple_selector : selector.compound_selectors().last().simple_selectors)
|
|
|
|
|
collect_guard_properties_for_simple_selector(simple_selector, property_set);
|
|
|
|
|
if (property_set.is_empty())
|
|
|
|
|
return {};
|
|
|
|
|
return property_set;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void collect_guard_properties_for_simple_selector(Selector::SimpleSelector const& selector, InvalidationSet& property_set)
|
|
|
|
|
{
|
|
|
|
|
// Only class and id selectors are safe to use as coarse guard properties. Tag
|
|
|
|
|
// and attribute matching depends on document and namespace case-sensitivity.
|
|
|
|
|
switch (selector.type) {
|
|
|
|
|
case Selector::SimpleSelector::Type::Class:
|
|
|
|
|
property_set.set_needs_invalidate_class(selector.name());
|
|
|
|
|
break;
|
|
|
|
|
case Selector::SimpleSelector::Type::Id:
|
|
|
|
|
property_set.set_needs_invalidate_id(selector.name());
|
|
|
|
|
break;
|
|
|
|
|
case Selector::SimpleSelector::Type::PseudoClass: {
|
|
|
|
|
auto const& pseudo_class = selector.pseudo_class();
|
|
|
|
|
if (pseudo_class.type == PseudoClass::Is || pseudo_class.type == PseudoClass::Where) {
|
|
|
|
|
InvalidationSet selector_list_property_set;
|
|
|
|
|
for (auto const& nested_selector : pseudo_class.argument_selector_list) {
|
|
|
|
|
auto nested_property_set = build_invalidation_guard_property_set_for_selector_subject(*nested_selector);
|
|
|
|
|
if (!nested_property_set.has_value())
|
|
|
|
|
return;
|
|
|
|
|
selector_list_property_set.include_all_from(*nested_property_set);
|
|
|
|
|
}
|
|
|
|
|
property_set.include_all_from(selector_list_property_set);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static InvalidationGuard build_invalidation_guard_for_simple_selectors(Vector<Selector::SimpleSelector const&> const& simple_selectors)
|
|
|
|
|
{
|
|
|
|
|
InvalidationGuard guard;
|
|
|
|
|
for (auto const& simple_selector : simple_selectors) {
|
|
|
|
|
InvalidationSet property_set;
|
|
|
|
|
collect_guard_properties_for_simple_selector(simple_selector, property_set);
|
|
|
|
|
if (!property_set.is_empty())
|
|
|
|
|
guard.property_sets.append(move(property_set));
|
|
|
|
|
}
|
|
|
|
|
return guard;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 20:36:20 +01:00
|
|
|
static InvalidationSet build_invalidation_set_for_simple_selectors(Vector<Selector::SimpleSelector const&> const& simple_selectors, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& style_invalidation_data, InsideNthChildPseudoClass inside_nth_child_pseudo_class)
|
|
|
|
|
{
|
|
|
|
|
InvalidationSet invalidation_set;
|
|
|
|
|
for (auto const& simple_selector : simple_selectors)
|
|
|
|
|
build_invalidation_sets_for_simple_selector(simple_selector, invalidation_set, exclude_properties_nested_in_not_pseudo_class, style_invalidation_data, inside_nth_child_pseudo_class);
|
|
|
|
|
return invalidation_set;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool simple_selector_group_matches_any(Vector<Selector::SimpleSelector const&> const& simple_selectors)
|
|
|
|
|
{
|
|
|
|
|
return simple_selectors.size() == 1 && simple_selectors.first().type == Selector::SimpleSelector::Type::Universal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static NonnullRefPtr<InvalidationPlan> make_invalidate_self_invalidation()
|
|
|
|
|
{
|
|
|
|
|
auto invalidation = InvalidationPlan::create();
|
|
|
|
|
invalidation->invalidate_self = true;
|
|
|
|
|
return invalidation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static NonnullRefPtr<InvalidationPlan> make_invalidate_whole_subtree_invalidation()
|
|
|
|
|
{
|
|
|
|
|
auto invalidation = InvalidationPlan::create();
|
|
|
|
|
invalidation->invalidate_whole_subtree = true;
|
|
|
|
|
return invalidation;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 12:23:13 +02:00
|
|
|
static NonnullRefPtr<InvalidationPlan> copy_invalidation_plan(InvalidationPlan const& plan)
|
|
|
|
|
{
|
|
|
|
|
auto copy = InvalidationPlan::create();
|
|
|
|
|
copy->include_all_from(plan);
|
|
|
|
|
return copy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void add_invalidation_plan_for_properties(StyleInvalidationData& style_invalidation_data, InvalidationSet const& invalidation_properties, InvalidationPlan const& plan, InvalidationGuard const& guard = {})
|
2026-03-08 20:36:20 +01:00
|
|
|
{
|
|
|
|
|
invalidation_properties.for_each_property([&](auto const& invalidation_property) {
|
|
|
|
|
if (!should_register_invalidation_property(invalidation_property))
|
|
|
|
|
return IterationDecision::Continue;
|
|
|
|
|
|
|
|
|
|
auto& stored_invalidation = style_invalidation_data.invalidation_plans.ensure(invalidation_property, [] {
|
|
|
|
|
return InvalidationPlan::create();
|
|
|
|
|
});
|
2026-05-24 12:23:13 +02:00
|
|
|
if (invalidation_property.type != InvalidationSet::Property::Type::PseudoClass || guard.is_empty()) {
|
|
|
|
|
stored_invalidation->include_all_from(plan);
|
|
|
|
|
} else {
|
|
|
|
|
GuardedInvalidationRule guarded_rule {
|
|
|
|
|
.guard = guard,
|
|
|
|
|
.payload = copy_invalidation_plan(plan),
|
|
|
|
|
};
|
|
|
|
|
append_or_merge_guarded_rule(stored_invalidation->guarded_rules, guarded_rule);
|
|
|
|
|
}
|
2026-03-08 20:36:20 +01:00
|
|
|
return IterationDecision::Continue;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct SelectorRighthand {
|
|
|
|
|
InvalidationSet subject_match_set;
|
|
|
|
|
bool subject_matches_any { false };
|
|
|
|
|
NonnullRefPtr<InvalidationPlan> payload;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static NonnullRefPtr<InvalidationPlan> build_invalidation_for_combinator(Selector::Combinator combinator, SelectorRighthand const& righthand)
|
|
|
|
|
{
|
LibWeb/CSS: Insert a combinator before all pseudo-element selectors
Previously, and according to the spec, `a::part(foo)::before` would be a
single CompoundSelector, even though it matches against 3 different
targets. This meant some awkward swapping of targets in the middle of
matching, and in particular it made `::part()` and `::slotted()` quite
hacky, requiring them to track extra data on the MatchContext to then
use later. This was scattered around and difficult to follow.
Partly inspired by Gecko, this commit instead introduces an invisible
PseudoElement combinator. After parsing a selector, we find any
CompoundSelectors that contain a pseudo-element and split them up, so
that each CompoundSelector only has a single target in the end. Where
the pseudo-element was at the start of a CompoundSelector, we insert an
invisible universal selector before it to represent its originating
element.
So now, a CompoundSelector deals with one target, and switching targets
is done at the combinator.
The one inconsistency is that we match the target of ::slotted()
and ::part() in pseudo_element_transition_target(), instead of before
then when processing the SimpleSelector. This is to avoid repeating the
same computations twice.
No outward-facing behaviour changes, though the invalidation metrics
have changed.
2026-05-01 16:16:47 +01:00
|
|
|
if (combinator == Selector::Combinator::PseudoElement)
|
|
|
|
|
return righthand.payload;
|
|
|
|
|
|
2026-03-08 20:36:20 +01:00
|
|
|
if (righthand.payload->invalidate_whole_subtree || (!righthand.subject_matches_any && righthand.subject_match_set.is_empty()))
|
|
|
|
|
return make_invalidate_whole_subtree_invalidation();
|
|
|
|
|
|
|
|
|
|
auto invalidation = InvalidationPlan::create();
|
|
|
|
|
switch (combinator) {
|
|
|
|
|
case Selector::Combinator::ImmediateChild:
|
|
|
|
|
case Selector::Combinator::Descendant:
|
2026-04-17 23:22:26 +02:00
|
|
|
append_or_merge_descendant_rule(invalidation->descendant_rules, { righthand.subject_match_set, righthand.subject_matches_any, righthand.payload });
|
2026-03-08 20:36:20 +01:00
|
|
|
break;
|
|
|
|
|
case Selector::Combinator::NextSibling:
|
2026-04-17 23:22:26 +02:00
|
|
|
append_or_merge_sibling_rule(invalidation->sibling_rules, { SiblingInvalidationReach::Adjacent, righthand.subject_match_set, righthand.subject_matches_any, righthand.payload });
|
2026-03-08 20:36:20 +01:00
|
|
|
break;
|
|
|
|
|
case Selector::Combinator::SubsequentSibling:
|
2026-04-17 23:22:26 +02:00
|
|
|
append_or_merge_sibling_rule(invalidation->sibling_rules, { SiblingInvalidationReach::Subsequent, righthand.subject_match_set, righthand.subject_matches_any, righthand.payload });
|
2026-03-08 20:36:20 +01:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
invalidation->invalidate_whole_subtree = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return invalidation;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 01:05:53 +01:00
|
|
|
void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector const& selector, InvalidationSet& invalidation_set, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& style_invalidation_data, InsideNthChildPseudoClass inside_nth_child_selector)
|
2025-01-12 18:38:05 +03:00
|
|
|
{
|
|
|
|
|
switch (selector.type) {
|
|
|
|
|
case Selector::SimpleSelector::Type::Class:
|
|
|
|
|
invalidation_set.set_needs_invalidate_class(selector.name());
|
|
|
|
|
break;
|
|
|
|
|
case Selector::SimpleSelector::Type::Id:
|
|
|
|
|
invalidation_set.set_needs_invalidate_id(selector.name());
|
|
|
|
|
break;
|
|
|
|
|
case Selector::SimpleSelector::Type::TagName:
|
|
|
|
|
invalidation_set.set_needs_invalidate_tag_name(selector.qualified_name().name.lowercase_name);
|
|
|
|
|
break;
|
|
|
|
|
case Selector::SimpleSelector::Type::Attribute:
|
|
|
|
|
invalidation_set.set_needs_invalidate_attribute(selector.attribute().qualified_name.name.lowercase_name);
|
|
|
|
|
break;
|
|
|
|
|
case Selector::SimpleSelector::Type::PseudoClass: {
|
|
|
|
|
auto const& pseudo_class = selector.pseudo_class();
|
2025-01-19 17:22:44 +01:00
|
|
|
switch (pseudo_class.type) {
|
|
|
|
|
case PseudoClass::Enabled:
|
2025-02-06 16:19:27 +01:00
|
|
|
case PseudoClass::Defined:
|
2025-01-19 17:22:44 +01:00
|
|
|
case PseudoClass::Disabled:
|
2026-04-27 10:06:35 +02:00
|
|
|
case PseudoClass::Empty:
|
2025-01-19 17:22:44 +01:00
|
|
|
case PseudoClass::PlaceholderShown:
|
|
|
|
|
case PseudoClass::Checked:
|
2025-02-08 19:09:08 +01:00
|
|
|
case PseudoClass::Has: {
|
2026-03-08 20:36:20 +01:00
|
|
|
for (auto const& nested_selector : pseudo_class.argument_selector_list)
|
2025-02-08 19:09:08 +01:00
|
|
|
add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(*nested_selector, style_invalidation_data);
|
|
|
|
|
[[fallthrough]];
|
|
|
|
|
}
|
2025-02-08 14:23:17 +01:00
|
|
|
case PseudoClass::Link:
|
|
|
|
|
case PseudoClass::AnyLink:
|
|
|
|
|
case PseudoClass::LocalLink:
|
2025-04-30 10:23:46 +10:00
|
|
|
case PseudoClass::Required:
|
|
|
|
|
case PseudoClass::Optional:
|
LibWeb: Make :host, :root, and interaction-state pseudos targetable
Stylesheet add/remove previously fell back to a whole-subtree
invalidation whenever a sheet's rightmost compound carried one of
these pseudo-classes. They each match a small, knowable set of
elements at any moment, so the invalidation walk can target them
directly:
- :host matches one element per shadow root and :root matches the
html element. Mark them targetable.
- :hover, :focus, :focus-visible, :focus-within, :active, and
:target all match at most a handful of elements at a time. Mark
them targetable and add matchers in InvalidationSetMatcher that
consult Document::hovered_node, focused_area, target_element,
and the element's own focused/active state.
2026-05-07 17:36:28 +02:00
|
|
|
// OPTIMIZATION: Interaction-state pseudo-classes match at most a handful of elements at any
|
|
|
|
|
// given time (the hovered element, the focused element, and ancestors thereof
|
|
|
|
|
// for :focus-within). Treating them as targetable lets a stylesheet add or
|
|
|
|
|
// remove that has, e.g., a `:hover` rule walk to those few elements instead
|
|
|
|
|
// of falling back to a whole-subtree invalidation.
|
|
|
|
|
case PseudoClass::Hover:
|
|
|
|
|
case PseudoClass::Focus:
|
|
|
|
|
case PseudoClass::FocusVisible:
|
|
|
|
|
case PseudoClass::FocusWithin:
|
|
|
|
|
case PseudoClass::Active:
|
|
|
|
|
case PseudoClass::Target:
|
|
|
|
|
// OPTIMIZATION: :host matches at most one element per shadow root and :root matches the html
|
|
|
|
|
// element only, so treating them as targetable lets a stylesheet add or remove
|
|
|
|
|
// with `:host` / `:root` rules invalidate just those elements instead of
|
|
|
|
|
// falling back to a whole-subtree invalidation.
|
|
|
|
|
case PseudoClass::Host:
|
|
|
|
|
case PseudoClass::Root:
|
2025-01-19 17:22:44 +01:00
|
|
|
invalidation_set.set_needs_invalidate_pseudo_class(pseudo_class.type);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-01-12 18:38:05 +03:00
|
|
|
if (pseudo_class.type == PseudoClass::Has)
|
|
|
|
|
break;
|
|
|
|
|
if (exclude_properties_nested_in_not_pseudo_class == ExcludePropertiesNestedInNotPseudoClass::Yes && pseudo_class.type == PseudoClass::Not)
|
|
|
|
|
break;
|
2025-01-24 22:48:45 +01:00
|
|
|
InsideNthChildPseudoClass inside_nth_child_pseudo_class_for_nested = inside_nth_child_selector;
|
2026-03-08 20:36:20 +01:00
|
|
|
if (AK::first_is_one_of(pseudo_class.type, PseudoClass::NthChild, PseudoClass::NthLastChild, PseudoClass::NthOfType, PseudoClass::NthLastOfType))
|
2025-01-24 22:48:45 +01:00
|
|
|
inside_nth_child_pseudo_class_for_nested = InsideNthChildPseudoClass::Yes;
|
2025-01-12 18:38:05 +03:00
|
|
|
for (auto const& nested_selector : pseudo_class.argument_selector_list) {
|
2025-01-24 22:48:45 +01:00
|
|
|
auto rightmost_invalidation_set_for_selector = build_invalidation_sets_for_selector_impl(style_invalidation_data, *nested_selector, inside_nth_child_pseudo_class_for_nested);
|
2025-01-12 18:38:05 +03:00
|
|
|
invalidation_set.include_all_from(rightmost_invalidation_set_for_selector);
|
2026-03-10 13:54:43 +01:00
|
|
|
|
|
|
|
|
// Propagate :has() from inner selectors where it appears in non-rightmost compounds.
|
|
|
|
|
// The rightmost set only carries properties from the rightmost compound, so :has() in
|
|
|
|
|
// non-rightmost positions (e.g., :is(:has(.x) .y)) is not propagated. We need it in the
|
|
|
|
|
// outer invalidation set so outer compounds register plans for pseudo_class:Has that
|
|
|
|
|
// account for the full selector context.
|
|
|
|
|
// Additionally, when :has() is inside a complex :is()/:where() argument (multiple
|
|
|
|
|
// compounds), the outer invalidation plan can't correctly capture the nested combinator
|
|
|
|
|
// structure — e.g., sibling combinators at the outer level would be applied at the wrong
|
|
|
|
|
// DOM level. Fall back to whole-subtree invalidation for :has() in these cases.
|
|
|
|
|
if (nested_selector->contains_pseudo_class(PseudoClass::Has)) {
|
|
|
|
|
invalidation_set.set_needs_invalidate_pseudo_class(PseudoClass::Has);
|
|
|
|
|
if (nested_selector->compound_selectors().size() > 1) {
|
|
|
|
|
InvalidationSet has_only;
|
|
|
|
|
has_only.set_needs_invalidate_pseudo_class(PseudoClass::Has);
|
|
|
|
|
add_invalidation_plan_for_properties(style_invalidation_data, has_only, *make_invalidate_whole_subtree_invalidation());
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-12 18:38:05 +03:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-04-25 16:21:07 +02:00
|
|
|
case Selector::SimpleSelector::Type::PseudoElement: {
|
|
|
|
|
// Pseudo-elements like ::slotted(.x) and ::part(...) carry a compound selector argument whose simple
|
|
|
|
|
// selectors decide which property changes should trigger invalidation against this rule.
|
|
|
|
|
auto const& pseudo_element = selector.pseudo_element();
|
|
|
|
|
if (pseudo_element.type() == PseudoElement::Slotted) {
|
|
|
|
|
for (auto const& compound_selector : pseudo_element.compound_selector().compound_selectors()) {
|
|
|
|
|
for (auto const& nested_simple : compound_selector.simple_selectors)
|
|
|
|
|
build_invalidation_sets_for_simple_selector(nested_simple, invalidation_set, exclude_properties_nested_in_not_pseudo_class, style_invalidation_data, inside_nth_child_selector);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-01-12 18:38:05 +03:00
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-08 19:09:08 +01:00
|
|
|
static void add_invalidation_sets_to_cover_scope_leakage_of_relative_selector_in_has_pseudo_class(Selector const& selector, StyleInvalidationData& style_invalidation_data)
|
|
|
|
|
{
|
|
|
|
|
// Normally, :has() invalidation scope is limited to ancestors and ancestor siblings, however it could require
|
|
|
|
|
// descendants invalidation when :is() with complex selector is used inside :has() relative selector.
|
|
|
|
|
// For example ".a:has(:is(.b .c))" requires invalidation whenever "b" class is added or removed.
|
|
|
|
|
// To cover this case, we add descendant invalidation set that requires whole subtree invalidation for each
|
|
|
|
|
// property used in non-subject part of complex selector.
|
|
|
|
|
|
2026-03-08 20:36:20 +01:00
|
|
|
auto invalidate_whole_subtree_for_invalidation_properties_in_non_subject_part_of_complex_selector = [&](Selector const& selector_to_invalidate) {
|
|
|
|
|
for_each_consecutive_simple_selector_group(selector_to_invalidate, [&](Vector<Selector::SimpleSelector const&> const& simple_selectors, Selector::Combinator, bool rightmost) {
|
|
|
|
|
if (rightmost)
|
2025-02-08 19:09:08 +01:00
|
|
|
return;
|
|
|
|
|
|
2026-03-08 20:36:20 +01:00
|
|
|
auto invalidation_set = build_invalidation_set_for_simple_selectors(simple_selectors, ExcludePropertiesNestedInNotPseudoClass::No, style_invalidation_data, InsideNthChildPseudoClass::No);
|
|
|
|
|
add_invalidation_plan_for_properties(style_invalidation_data, invalidation_set, *make_invalidate_whole_subtree_invalidation());
|
2025-02-08 19:09:08 +01:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for_each_consecutive_simple_selector_group(selector, [&](Vector<Selector::SimpleSelector const&> const& simple_selectors, Selector::Combinator, bool) {
|
|
|
|
|
for (auto const& simple_selector : simple_selectors) {
|
|
|
|
|
if (simple_selector.type != Selector::SimpleSelector::Type::PseudoClass)
|
|
|
|
|
continue;
|
|
|
|
|
auto const& pseudo_class = simple_selector.pseudo_class();
|
|
|
|
|
if (pseudo_class.type == PseudoClass::Is || pseudo_class.type == PseudoClass::Where || pseudo_class.type == PseudoClass::Not) {
|
2026-03-08 20:36:20 +01:00
|
|
|
for (auto const& nested_selector : pseudo_class.argument_selector_list)
|
|
|
|
|
invalidate_whole_subtree_for_invalidation_properties_in_non_subject_part_of_complex_selector(*nested_selector);
|
2025-02-08 19:09:08 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-24 22:48:45 +01:00
|
|
|
static InvalidationSet build_invalidation_sets_for_selector_impl(StyleInvalidationData& style_invalidation_data, Selector const& selector, InsideNthChildPseudoClass inside_nth_child_pseudo_class)
|
2025-01-12 18:38:05 +03:00
|
|
|
{
|
|
|
|
|
auto const& compound_selectors = selector.compound_selectors();
|
|
|
|
|
int compound_selector_index = compound_selectors.size() - 1;
|
|
|
|
|
VERIFY(compound_selector_index >= 0);
|
|
|
|
|
|
|
|
|
|
InvalidationSet invalidation_set_for_rightmost_selector;
|
|
|
|
|
Selector::Combinator previous_compound_combinator = Selector::Combinator::None;
|
2026-03-08 20:36:20 +01:00
|
|
|
Optional<SelectorRighthand> selector_righthand;
|
2025-01-12 18:38:05 +03:00
|
|
|
for_each_consecutive_simple_selector_group(selector, [&](Vector<Selector::SimpleSelector const&> const& simple_selectors, Selector::Combinator combinator, bool is_rightmost) {
|
|
|
|
|
// Collect properties used in :has() so we can decide if only specific properties
|
|
|
|
|
// trigger descendant invalidation or if the entire document must be invalidated.
|
|
|
|
|
for (auto const& simple_selector : simple_selectors) {
|
2026-04-17 08:57:12 +02:00
|
|
|
collect_properties_used_in_has(simple_selector, style_invalidation_data, {});
|
2025-01-12 18:38:05 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-08 20:36:20 +01:00
|
|
|
auto invalidation_properties = build_invalidation_set_for_simple_selectors(simple_selectors, ExcludePropertiesNestedInNotPseudoClass::No, style_invalidation_data, inside_nth_child_pseudo_class);
|
|
|
|
|
auto subject_match_set = build_invalidation_set_for_simple_selectors(simple_selectors, ExcludePropertiesNestedInNotPseudoClass::Yes, style_invalidation_data, inside_nth_child_pseudo_class);
|
2026-05-24 12:23:13 +02:00
|
|
|
auto subject_guard = build_invalidation_guard_for_simple_selectors(simple_selectors);
|
2026-03-08 20:36:20 +01:00
|
|
|
bool subject_matches_any = subject_match_set.is_empty() && simple_selector_group_matches_any(simple_selectors);
|
|
|
|
|
|
2025-01-12 18:38:05 +03:00
|
|
|
if (is_rightmost) {
|
|
|
|
|
// The rightmost selector is handled twice:
|
|
|
|
|
// 1) Include properties nested in :not()
|
|
|
|
|
// 2) Exclude properties nested in :not()
|
|
|
|
|
//
|
|
|
|
|
// This ensures we handle cases like:
|
|
|
|
|
// :not(.foo) => produce invalidation set .foo { $ } ($ = invalidate self)
|
|
|
|
|
// .bar :not(.foo) => produce invalidation sets .foo { $ } and .bar { * } (* = invalidate subtree)
|
|
|
|
|
// which means invalidation_set_for_rightmost_selector should be empty
|
2026-03-08 20:36:20 +01:00
|
|
|
auto root_plan = make_invalidate_self_invalidation();
|
|
|
|
|
if (inside_nth_child_pseudo_class == InsideNthChildPseudoClass::Yes) {
|
|
|
|
|
// When invalidation property is nested in nth-child selector like p:nth-child(even of #t1, #t2, #t3)
|
|
|
|
|
// we need to make sure all affected siblings are invalidated.
|
|
|
|
|
root_plan->invalidate_whole_subtree = true;
|
2025-01-12 18:38:05 +03:00
|
|
|
}
|
2026-03-08 20:36:20 +01:00
|
|
|
add_invalidation_plan_for_properties(style_invalidation_data, invalidation_properties, *root_plan);
|
2025-01-12 18:38:05 +03:00
|
|
|
|
2026-03-08 20:36:20 +01:00
|
|
|
invalidation_set_for_rightmost_selector = subject_match_set;
|
|
|
|
|
selector_righthand = SelectorRighthand {
|
|
|
|
|
.subject_match_set = move(subject_match_set),
|
|
|
|
|
.subject_matches_any = subject_matches_any,
|
|
|
|
|
.payload = root_plan,
|
|
|
|
|
};
|
2025-01-12 18:38:05 +03:00
|
|
|
} else {
|
|
|
|
|
VERIFY(previous_compound_combinator != Selector::Combinator::None);
|
2026-03-08 20:36:20 +01:00
|
|
|
VERIFY(selector_righthand.has_value());
|
|
|
|
|
|
|
|
|
|
auto plan = build_invalidation_for_combinator(previous_compound_combinator, *selector_righthand);
|
2026-05-24 12:23:13 +02:00
|
|
|
add_invalidation_plan_for_properties(style_invalidation_data, invalidation_properties, *plan, subject_guard);
|
2026-03-08 20:36:20 +01:00
|
|
|
|
|
|
|
|
selector_righthand = SelectorRighthand {
|
|
|
|
|
.subject_match_set = move(subject_match_set),
|
|
|
|
|
.subject_matches_any = subject_matches_any,
|
|
|
|
|
.payload = move(plan),
|
|
|
|
|
};
|
2025-01-12 18:38:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
previous_compound_combinator = combinator;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return invalidation_set_for_rightmost_selector;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-24 22:48:45 +01:00
|
|
|
void StyleInvalidationData::build_invalidation_sets_for_selector(Selector const& selector)
|
|
|
|
|
{
|
|
|
|
|
(void)build_invalidation_sets_for_selector_impl(*this, selector, InsideNthChildPseudoClass::No);
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-12 18:38:05 +03:00
|
|
|
}
|