ladybird/Libraries/LibWeb/CSS/Invalidation/InvalidationSetMatcher.cpp
Andreas Kling 62a5ee7ff9 LibWeb: Guard pseudo-class invalidation by selector features
Include tag and attribute selectors in guarded pseudo-class invalidation
plans. This keeps selectors like a:hover .target from applying work to
unrelated descendants when :hover changes on other elements.

Store both original and lowercase attribute names for invalidation keys.
HTML attribute mutations still hit lowercase selector buckets, while SVG
and MathML selectors such as [viewBox] keep their case-sensitive guards.
Use the same names for :has() metadata and mutation feature filtering so
attribute changes do not get filtered out after metadata lookup.

Match tag invalidation properties against lowercased local names, like
invalidation data and rule cache buckets do. The regression tests cover
SVG hover fanout and case-sensitive :has() attribute mutation.
2026-06-08 17:16:18 +02:00

208 lines
8.8 KiB
C++

/*
* Copyright (c) 2026-present, the Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Assertions.h>
#include <AK/IterationDecision.h>
#include <LibWeb/CSS/Invalidation/InvalidationSetMatcher.h>
#include <LibWeb/CSS/InvalidationSet.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/HTML/HTMLTextAreaElement.h>
namespace Web::CSS::Invalidation {
static bool element_may_have_attribute_matching_selector(DOM::Element const& element, Selector::SimpleSelector::Attribute const& attribute)
{
auto const& attribute_name = attribute.qualified_name.name.name;
if (element.has_attribute(attribute_name))
return true;
auto const& lowercase_attribute_name = attribute.qualified_name.name.lowercase_name;
return lowercase_attribute_name != attribute_name && element.has_attribute(lowercase_attribute_name);
}
static bool compound_may_match_element_impl(DOM::Element const& element, Selector::CompoundSelector const& compound_selector, Optional<PseudoClass> ignored_pseudo_class, bool in_selector_list_argument)
{
for (auto const& simple_selector : compound_selector.simple_selectors) {
switch (simple_selector.type) {
case Selector::SimpleSelector::Type::Universal:
break;
case Selector::SimpleSelector::Type::Nesting:
case Selector::SimpleSelector::Type::Invalid:
return true;
case Selector::SimpleSelector::Type::PseudoElement:
if (in_selector_list_argument)
return false;
break;
case Selector::SimpleSelector::Type::TagName:
if (element.lowercased_local_name() != simple_selector.qualified_name().name.lowercase_name)
return false;
break;
case Selector::SimpleSelector::Type::Id: {
auto id = element.id();
if (!id.has_value() || *id != simple_selector.name())
return false;
break;
}
case Selector::SimpleSelector::Type::Class:
if (!element.class_names().contains_slow(simple_selector.name()))
return false;
break;
case Selector::SimpleSelector::Type::Attribute:
if (!element_may_have_attribute_matching_selector(element, simple_selector.attribute()))
return false;
break;
case Selector::SimpleSelector::Type::PseudoClass: {
auto const& pseudo_class = simple_selector.pseudo_class();
if (ignored_pseudo_class.has_value() && pseudo_class.type == *ignored_pseudo_class)
break;
switch (pseudo_class.type) {
case PseudoClass::Is:
case PseudoClass::Where: {
bool argument_may_match = false;
for (auto const& argument_selector : pseudo_class.argument_selector_list) {
if (compound_may_match_element_impl(element, argument_selector->compound_selectors().last(), ignored_pseudo_class, true)) {
argument_may_match = true;
break;
}
}
if (!argument_may_match)
return false;
break;
}
case PseudoClass::Root:
if (&element != element.document().document_element())
return false;
break;
default:
break;
}
break;
}
}
}
return true;
}
bool compound_may_match_element(DOM::Element const& element, Selector::CompoundSelector const& compound_selector, Optional<PseudoClass> ignored_pseudo_class)
{
return compound_may_match_element_impl(element, compound_selector, ignored_pseudo_class, false);
}
bool element_matches_any_invalidation_set_property(DOM::Element const& element, InvalidationSet const& set)
{
auto includes_property = [&](InvalidationSet::Property const& property) {
switch (property.type) {
case InvalidationSet::Property::Type::Class: {
auto case_sensitivity = CaseSensitivity::CaseSensitive;
if (element.document().in_quirks_mode())
case_sensitivity = CaseSensitivity::CaseInsensitive;
return element.has_class(property.name(), case_sensitivity);
}
case InvalidationSet::Property::Type::Id:
return element.id() == property.name();
case InvalidationSet::Property::Type::TagName:
return element.lowercased_local_name() == property.name();
case InvalidationSet::Property::Type::Attribute:
return element.has_attribute(property.name()) || element.has_removed_attribute_for_style_invalidation(property.name());
case InvalidationSet::Property::Type::PseudoClass: {
switch (property.value.get<PseudoClass>()) {
case PseudoClass::Has:
return element.affected_by_has_pseudo_class_in_subject_position()
|| element.affected_by_has_pseudo_class_in_non_subject_position();
case PseudoClass::Enabled:
return element.matches_enabled_pseudo_class();
case PseudoClass::Disabled:
return element.matches_disabled_pseudo_class();
case PseudoClass::Defined:
return element.is_defined();
case PseudoClass::Checked:
return element.matches_checked_pseudo_class();
case PseudoClass::PlaceholderShown:
return element.matches_placeholder_shown_pseudo_class();
case PseudoClass::Empty: {
if (!element.has_children())
return true;
if (element.first_child_of_type<DOM::Element>())
return false;
bool has_nonempty_text_child = false;
element.for_each_child_of_type<DOM::Text>([&](auto const& text_child) {
if (!text_child.data().is_empty()) {
has_nonempty_text_child = true;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
return !has_nonempty_text_child;
}
case PseudoClass::AnyLink:
case PseudoClass::Link:
return element.matches_link_pseudo_class();
case PseudoClass::LocalLink:
return element.matches_local_link_pseudo_class();
case PseudoClass::Root:
return is<HTML::HTMLHtmlElement>(element);
case PseudoClass::Host:
return element.is_shadow_host();
case PseudoClass::Required:
case PseudoClass::Optional:
return is<HTML::HTMLInputElement>(element)
|| is<HTML::HTMLSelectElement>(element)
|| is<HTML::HTMLTextAreaElement>(element);
case PseudoClass::Hover: {
auto* hovered = element.document().hovered_node();
if (!hovered)
return false;
if (hovered == &element)
return true;
return element.is_shadow_including_ancestor_of(*hovered);
}
case PseudoClass::Focus:
return element.is_focused();
case PseudoClass::FocusVisible:
return element.is_focused() && element.should_indicate_focus();
case PseudoClass::FocusWithin:
return element.matches_focus_within_pseudo_class();
case PseudoClass::Active:
return element.is_being_activated();
case PseudoClass::Target:
return element.document().target_element() == &element;
case PseudoClass::FirstChild:
return !element.previous_element_sibling();
case PseudoClass::LastChild:
return !element.next_element_sibling();
case PseudoClass::OnlyChild:
return !element.previous_element_sibling() && !element.next_element_sibling();
default:
VERIFY_NOT_REACHED();
}
}
case InvalidationSet::Property::Type::InvalidateSelf:
return false;
case InvalidationSet::Property::Type::InvalidateWholeSubtree:
return true;
default:
VERIFY_NOT_REACHED();
}
};
bool includes_any = false;
set.for_each_property([&](auto const& property) {
if (includes_property(property)) {
includes_any = true;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
return includes_any;
}
}