mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-27 19:51:03 +00:00
Compound selectors with a non-rightmost pseudo-class used to register their descendant and sibling invalidation plans directly under the pseudo-class property. A selector like `.item:hover * .target` ran the descendant plan for every hovered element, even when that element could not match `.item`. Store those non-rightmost plans behind a guard made from stable class and id subject features in the same compound selector. Deliberately leave pseudo-classes out of guards, since one mutation can change multiple state pseudo-classes at once. Also leave tag and attribute selectors out, since their matching depends on document and namespace case-sensitivity that the guard property does not carry. The new style-invalidation test covers the GitHub-shaped `:hover * :not(...)` case, co-invalidated `:link` and `:any-link`, partial or complex `:is()`/`:where()` alternatives, and case-sensitive SVG tag and attribute selectors.
130 lines
5.5 KiB
C++
130 lines
5.5 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 {
|
|
|
|
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.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;
|
|
}
|
|
|
|
}
|