LibWeb: Deduplicate code for pseudo class selector matching

Moves pseudo class matching helpers into Element methods, so they don't
have to be duplicated between SelectorEngine and function that checks if
element is included in invalidation set.
This commit is contained in:
Aliaksandr Kalenik 2025-02-09 17:02:51 +01:00 committed by Andreas Kling
commit adc17c3576
Notes: github-actions[bot] 2025-02-10 09:26:18 +00:00
3 changed files with 100 additions and 79 deletions

View file

@ -16,8 +16,6 @@
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/AttributeNames.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLAreaElement.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
#include <LibWeb/HTML/HTMLDetailsElement.h>
#include <LibWeb/HTML/HTMLDialogElement.h>
#include <LibWeb/HTML/HTMLFieldSetElement.h>
@ -25,8 +23,6 @@
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/HTML/HTMLOptGroupElement.h>
#include <LibWeb/HTML/HTMLOptionElement.h>
#include <LibWeb/HTML/HTMLProgressElement.h>
#include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/HTML/HTMLTextAreaElement.h>
@ -155,15 +151,6 @@ static inline bool matches_has_pseudo_class(CSS::Selector const& selector, DOM::
return matches_relative_selector(selector, 0, anchor, shadow_host, context, anchor);
}
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-link
static inline bool matches_link_pseudo_class(DOM::Element const& element)
{
// All a elements that have an href attribute, and all area elements that have an href attribute, must match one of :link and :visited.
if (!is<HTML::HTMLAnchorElement>(element) && !is<HTML::HTMLAreaElement>(element) && !is<SVG::SVGAElement>(element))
return false;
return element.has_attribute(HTML::AttributeNames::href);
}
static bool matches_hover_pseudo_class(DOM::Element const& element)
{
auto* hovered_node = element.document().hovered_node();
@ -174,30 +161,6 @@ static bool matches_hover_pseudo_class(DOM::Element const& element)
return element.is_shadow_including_ancestor_of(*hovered_node);
}
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-checked
static inline bool matches_checked_pseudo_class(DOM::Element const& element)
{
// The :checked pseudo-class must match any element falling into one of the following categories:
// - input elements whose type attribute is in the Checkbox state and whose checkedness state is true
// - input elements whose type attribute is in the Radio Button state and whose checkedness state is true
if (is<HTML::HTMLInputElement>(element)) {
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element);
switch (input_element.type_state()) {
case HTML::HTMLInputElement::TypeAttributeState::Checkbox:
case HTML::HTMLInputElement::TypeAttributeState::RadioButton:
return static_cast<HTML::HTMLInputElement const&>(element).checked();
default:
return false;
}
}
// - option elements whose selectedness is true
if (is<HTML::HTMLOptionElement>(element)) {
return static_cast<HTML::HTMLOptionElement const&>(element).selected();
}
return false;
}
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-indeterminate
static inline bool matches_indeterminate_pseudo_class(DOM::Element const& element)
{
@ -463,25 +426,9 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla
case CSS::PseudoClass::AnyLink:
// NOTE: AnyLink should match whether the link is visited or not, so if we ever start matching
// :visited, we'll need to handle these differently.
return matches_link_pseudo_class(element);
return element.matches_link_pseudo_class();
case CSS::PseudoClass::LocalLink: {
// The :local-link pseudo-class allows authors to style hyperlinks based on the users current location
// within a site. It represents an element that is the source anchor of a hyperlink whose targets
// absolute URL matches the elements own document URL. If the hyperlinks target includes a fragment
// URL, then the fragment URL of the current URL must also match; if it does not, then the fragment
// URL portion of the current URL is not taken into account in the comparison.
if (!matches_link_pseudo_class(element))
return false;
auto document_url = element.document().url();
auto maybe_href = element.attribute(HTML::AttributeNames::href);
if (!maybe_href.has_value())
return false;
auto target_url = element.document().encoding_parse_url(*maybe_href);
if (!target_url.has_value())
return false;
if (target_url->fragment().has_value())
return document_url.equals(*target_url, URL::ExcludeFragment::No);
return document_url.equals(*target_url, URL::ExcludeFragment::Yes);
return element.matches_local_link_pseudo_class();
}
case CSS::PseudoClass::Visited:
// FIXME: Maybe match this selector sometimes?
@ -544,16 +491,11 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla
case CSS::PseudoClass::Lang:
return matches_lang_pseudo_class(element, pseudo_class.languages);
case CSS::PseudoClass::Disabled:
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-disabled
// The :disabled pseudo-class must match any element that is actually disabled.
return element.is_actually_disabled();
return element.matches_disabled_pseudo_class();
case CSS::PseudoClass::Enabled:
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-enabled
// The :enabled pseudo-class must match any button, input, select, textarea, optgroup, option, fieldset element, or form-associated custom element that is not actually disabled.
return (is<HTML::HTMLButtonElement>(element) || is<HTML::HTMLInputElement>(element) || is<HTML::HTMLSelectElement>(element) || is<HTML::HTMLTextAreaElement>(element) || is<HTML::HTMLOptGroupElement>(element) || is<HTML::HTMLOptionElement>(element) || is<HTML::HTMLFieldSetElement>(element))
&& !element.is_actually_disabled();
return element.matches_enabled_pseudo_class();
case CSS::PseudoClass::Checked:
return matches_checked_pseudo_class(element);
return element.matches_checked_pseudo_class();
case CSS::PseudoClass::Indeterminate:
return matches_indeterminate_pseudo_class(element);
case CSS::PseudoClass::Defined:

View file

@ -1160,6 +1160,88 @@ bool Element::affected_by_hover() const
return false;
}
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-enabled
bool Element::matches_enabled_pseudo_class() const
{
// The :enabled pseudo-class must match any button, input, select, textarea, optgroup, option, fieldset element, or form-associated custom element that is not actually disabled.
return (is<HTML::HTMLButtonElement>(*this) || is<HTML::HTMLInputElement>(*this) || is<HTML::HTMLSelectElement>(*this) || is<HTML::HTMLTextAreaElement>(*this) || is<HTML::HTMLOptGroupElement>(*this) || is<HTML::HTMLOptionElement>(*this) || is<HTML::HTMLFieldSetElement>(*this))
&& !is_actually_disabled();
}
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-disabled
bool Element::matches_disabled_pseudo_class() const
{
// The :disabled pseudo-class must match any element that is actually disabled.
return is_actually_disabled();
}
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-checked
bool Element::matches_checked_pseudo_class() const
{
// The :checked pseudo-class must match any element falling into one of the following categories:
// - input elements whose type attribute is in the Checkbox state and whose checkedness state is true
// - input elements whose type attribute is in the Radio Button state and whose checkedness state is true
if (is<HTML::HTMLInputElement>(*this)) {
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(*this);
switch (input_element.type_state()) {
case HTML::HTMLInputElement::TypeAttributeState::Checkbox:
case HTML::HTMLInputElement::TypeAttributeState::RadioButton:
return static_cast<HTML::HTMLInputElement const&>(*this).checked();
default:
return false;
}
}
// - option elements whose selectedness is true
if (is<HTML::HTMLOptionElement>(*this)) {
return static_cast<HTML::HTMLOptionElement const&>(*this).selected();
}
return false;
}
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-placeholder-shown
bool Element::matches_placeholder_shown_pseudo_class() const
{
// The :placeholder-shown pseudo-class must match any element falling into one of the following categories:
// - input elements that have a placeholder attribute whose value is currently being presented to the user.
if (is<HTML::HTMLInputElement>(*this) && has_attribute(HTML::AttributeNames::placeholder)) {
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(*this);
return input_element.placeholder_element() && input_element.placeholder_value().has_value();
}
// - FIXME: textarea elements that have a placeholder attribute whose value is currently being presented to the user.
return false;
}
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-link
bool Element::matches_link_pseudo_class() const
{
// All a elements that have an href attribute, and all area elements that have an href attribute, must match one of :link and :visited.
if (!is<HTML::HTMLAnchorElement>(*this) && !is<HTML::HTMLAreaElement>(*this) && !is<SVG::SVGAElement>(*this))
return false;
return has_attribute(HTML::AttributeNames::href);
}
bool Element::matches_local_link_pseudo_class() const
{
// The :local-link pseudo-class allows authors to style hyperlinks based on the users current location
// within a site. It represents an element that is the source anchor of a hyperlink whose targets
// absolute URL matches the elements own document URL. If the hyperlinks target includes a fragment
// URL, then the fragment URL of the current URL must also match; if it does not, then the fragment
// URL portion of the current URL is not taken into account in the comparison.
if (!matches_link_pseudo_class())
return false;
auto document_url = document().url();
auto maybe_href = attribute(HTML::AttributeNames::href);
if (!maybe_href.has_value())
return false;
auto target_url = document().encoding_parse_url(*maybe_href);
if (!target_url.has_value())
return false;
if (target_url->fragment().has_value())
return document_url.equals(*target_url, URL::ExcludeFragment::No);
return document_url.equals(*target_url, URL::ExcludeFragment::Yes);
}
bool Element::includes_properties_from_invalidation_set(CSS::InvalidationSet const& set) const
{
auto includes_property = [&](CSS::InvalidationSet::Property const& property) {
@ -1180,35 +1262,25 @@ bool Element::includes_properties_from_invalidation_set(CSS::InvalidationSet con
case CSS::PseudoClass::Has:
return true;
case CSS::PseudoClass::Enabled: {
return (is<HTML::HTMLButtonElement>(*this) || is<HTML::HTMLInputElement>(*this) || is<HTML::HTMLSelectElement>(*this) || is<HTML::HTMLTextAreaElement>(*this) || is<HTML::HTMLOptGroupElement>(*this) || is<HTML::HTMLOptionElement>(*this) || is<HTML::HTMLFieldSetElement>(*this))
&& !is_actually_disabled();
return matches_enabled_pseudo_class();
}
case CSS::PseudoClass::Disabled: {
return is_actually_disabled();
return matches_disabled_pseudo_class();
}
case CSS::PseudoClass::Defined: {
return is_defined();
}
case CSS::PseudoClass::Checked: {
// FIXME: This could be narrowed down to return true only if element is actually checked.
return is<HTML::HTMLInputElement>(*this) || is<HTML::HTMLOptionElement>(*this);
return matches_checked_pseudo_class();
}
case CSS::PseudoClass::PlaceholderShown: {
if (is<HTML::HTMLInputElement>(*this) && has_attribute(HTML::AttributeNames::placeholder)) {
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(*this);
return input_element.placeholder_element() && input_element.placeholder_value().has_value();
}
// - FIXME: textarea elements that have a placeholder attribute whose value is currently being presented to the user.
return false;
return matches_placeholder_shown_pseudo_class();
}
case CSS::PseudoClass::AnyLink:
case CSS::PseudoClass::Link:
return matches_link_pseudo_class();
case CSS::PseudoClass::LocalLink: {
if (!is<HTML::HTMLAnchorElement>(*this) && !is<HTML::HTMLAreaElement>(*this) && !is<SVG::SVGAElement>(*this))
return false;
if (!has_attribute(HTML::AttributeNames::href))
return false;
return true;
return matches_local_link_pseudo_class();
}
default:
VERIFY_NOT_REACHED();

View file

@ -420,6 +420,13 @@ public:
bool has_style_containment() const;
bool has_paint_containment() const;
bool matches_enabled_pseudo_class() const;
bool matches_disabled_pseudo_class() const;
bool matches_checked_pseudo_class() const;
bool matches_placeholder_shown_pseudo_class() const;
bool matches_link_pseudo_class() const;
bool matches_local_link_pseudo_class() const;
bool affected_by_has_pseudo_class_in_subject_position() const { return m_affected_by_has_pseudo_class_in_subject_position; }
void set_affected_by_has_pseudo_class_in_subject_position(bool value) { m_affected_by_has_pseudo_class_in_subject_position = value; }