/* * Copyright (c) 2018-2022, Andreas Kling * Copyright (c) 2021-2023, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::SelectorEngine { // https://drafts.csswg.org/selectors-4/#the-lang-pseudo static inline bool matches_lang_pseudo_class(DOM::Element const& element, Vector const& languages) { FlyString element_language; for (auto const* e = &element; e; e = e->parent_element()) { auto lang = e->attribute(HTML::AttributeNames::lang); if (lang.has_value()) { element_language = lang.release_value(); break; } } if (element_language.is_empty()) return false; // FIXME: This is ad-hoc. Implement a proper language range matching algorithm as recommended by BCP47. for (auto const& language : languages) { if (language.is_empty()) continue; if (language == "*"sv) return true; if (!element_language.to_string().contains('-') && Infra::is_ascii_case_insensitive_match(element_language, language)) return true; auto parts = element_language.to_string().split_limit('-', 2).release_value_but_fixme_should_propagate_errors(); if (Infra::is_ascii_case_insensitive_match(parts[0], language)) return true; } return false; } // 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(element) && !is(element)) return false; return element.has_attribute(HTML::AttributeNames::href); } static inline bool matches_hover_pseudo_class(DOM::Element const& element) { auto* hovered_node = element.document().hovered_node(); if (!hovered_node) return false; if (&element == hovered_node) return true; return element.is_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(element)) { auto const& input_element = static_cast(element); switch (input_element.type_state()) { case HTML::HTMLInputElement::TypeAttributeState::Checkbox: case HTML::HTMLInputElement::TypeAttributeState::RadioButton: return static_cast(element).checked(); default: return false; } } // - option elements whose selectedness is true if (is(element)) { return static_cast(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) { // The :indeterminate 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 indeterminate IDL attribute is set to true // FIXME: - input elements whose type attribute is in the Radio Button state and whose radio button group contains no input elements whose checkedness state is true. if (is(element)) { auto const& input_element = static_cast(element); switch (input_element.type_state()) { case HTML::HTMLInputElement::TypeAttributeState::Checkbox: return input_element.indeterminate(); default: return false; } } // - progress elements with no value content attribute if (is(element)) { return !element.has_attribute(HTML::AttributeNames::value); } return false; } static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, [[maybe_unused]] Optional style_sheet_for_rule, DOM::Element const& element) { // FIXME: Check the attribute's namespace, once we support that in DOM::Element! auto const& attribute_name = attribute.qualified_name.name.name; if (attribute.match_type == CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute) { // Early way out in case of an attribute existence selector. return element.has_attribute(attribute_name); } auto const case_insensitive_match = (attribute.case_type == CSS::Selector::SimpleSelector::Attribute::CaseType::CaseInsensitiveMatch); auto const case_sensitivity = case_insensitive_match ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive; switch (attribute.match_type) { case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch: return case_insensitive_match ? Infra::is_ascii_case_insensitive_match(element.attribute(attribute_name).value_or({}), attribute.value) : element.attribute(attribute_name) == attribute.value; case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord: { if (attribute.value.is_empty()) { // This selector is always false is match value is empty. return false; } auto attribute_value = element.attribute(attribute_name).value_or({}); auto const view = attribute_value.bytes_as_string_view().split_view(' '); auto const size = view.size(); for (size_t i = 0; i < size; ++i) { auto const value = view.at(i); if (case_insensitive_match ? Infra::is_ascii_case_insensitive_match(value, attribute.value) : value == attribute.value) { return true; } } return false; } case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString: return !attribute.value.is_empty() && element.attribute(attribute_name).value_or({}).contains(attribute.value, case_sensitivity); case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment: { auto const element_attr_value = element.attribute(attribute_name).value_or({}); if (element_attr_value.is_empty()) { // If the attribute value on element is empty, the selector is true // if the match value is also empty and false otherwise. return attribute.value.is_empty(); } if (attribute.value.is_empty()) { return false; } auto segments = element_attr_value.bytes_as_string_view().split_view('-'); return case_insensitive_match ? Infra::is_ascii_case_insensitive_match(segments.first(), attribute.value) : segments.first() == attribute.value; } case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString: return !attribute.value.is_empty() && element.attribute(attribute_name).value_or({}).bytes_as_string_view().starts_with(attribute.value, case_sensitivity); case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString: return !attribute.value.is_empty() && element.attribute(attribute_name).value_or({}).bytes_as_string_view().ends_with(attribute.value, case_sensitivity); default: break; } return false; } static inline DOM::Element const* previous_sibling_with_same_tag_name(DOM::Element const& element) { for (auto const* sibling = element.previous_element_sibling(); sibling; sibling = sibling->previous_element_sibling()) { if (sibling->tag_name() == element.tag_name()) return sibling; } return nullptr; } static inline DOM::Element const* next_sibling_with_same_tag_name(DOM::Element const& element) { for (auto const* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) { if (sibling->tag_name() == element.tag_name()) return sibling; } return nullptr; } // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-read-write static bool matches_read_write_pseudo_class(DOM::Element const& element) { // The :read-write pseudo-class must match any element falling into one of the following categories, // which for the purposes of Selectors are thus considered user-alterable: [SELECTORS] // - input elements to which the readonly attribute applies, and that are mutable // (i.e. that do not have the readonly attribute specified and that are not disabled) if (is(element)) { auto& input_element = static_cast(element); if (input_element.has_attribute(HTML::AttributeNames::readonly)) return false; if (!input_element.enabled()) return false; return true; } // - textarea elements that do not have a readonly attribute, and that are not disabled if (is(element)) { auto& input_element = static_cast(element); if (input_element.has_attribute(HTML::AttributeNames::readonly)) return false; if (!input_element.enabled()) return false; return true; } // - elements that are editing hosts or editable and are neither input elements nor textarea elements return element.is_editable(); } // https://www.w3.org/TR/selectors-4/#open-state static bool matches_open_state_pseudo_class(DOM::Element const& element, bool open) { // The :open pseudo-class represents an element that has both “open” and “closed” states, // and which is currently in the “open” state. // The :closed pseudo-class represents an element that has both “open” and “closed” states, // and which is currently in the closed state. // NOTE: Spec specifically suggests supporting
, , and