/* * Copyright (c) 2018-2024, Andreas Kling * Copyright (c) 2021-2024, 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 #include #include #include #include namespace Web::SelectorEngine { static inline bool matches(CSS::Selector const& selector, Optional style_sheet_for_rule, int component_list_index, DOM::Element const& element, JS::GCPtr shadow_host, JS::GCPtr scope, SelectorKind selector_kind, JS::GCPtr anchor = nullptr); // Upward traversal for descendant (' ') and immediate child combinator ('>') // If we're starting inside a shadow tree, traversal stops at the nearest shadow host. // This is an implementation detail of the :host selector. Otherwise we would just traverse up to the document root. static inline JS::GCPtr traverse_up(JS::GCPtr node, JS::GCPtr shadow_host) { if (!node) return nullptr; if (shadow_host) { // NOTE: We only traverse up to the shadow host, not beyond. if (node == shadow_host) return nullptr; return node->parent_or_shadow_host_element(); } return node->parent(); } // https://drafts.csswg.org/selectors-4/#the-lang-pseudo static inline bool matches_lang_pseudo_class(DOM::Element const& element, Vector const& languages) { auto maybe_element_language = element.lang(); if (!maybe_element_language.has_value()) return false; auto element_language = maybe_element_language.release_value(); // 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.contains('-') && Infra::is_ascii_case_insensitive_match(element_language, language)) return true; auto parts = element_language.split_limit('-', 2).release_value_but_fixme_should_propagate_errors(); if (!parts.is_empty() && Infra::is_ascii_case_insensitive_match(parts[0], language)) return true; } return false; } // https://drafts.csswg.org/selectors-4/#relational static inline bool matches_relative_selector(CSS::Selector const& selector, size_t compound_index, Optional style_sheet_for_rule, DOM::Element const& element, JS::GCPtr shadow_host, JS::NonnullGCPtr anchor) { if (compound_index >= selector.compound_selectors().size()) return matches(selector, style_sheet_for_rule, element, shadow_host, {}, {}, SelectorKind::Relative, anchor); switch (selector.compound_selectors()[compound_index].combinator) { // Shouldn't be possible because we've parsed relative selectors, which always have a combinator, implicitly or explicitly. case CSS::Selector::Combinator::None: VERIFY_NOT_REACHED(); case CSS::Selector::Combinator::Descendant: { bool has = false; element.for_each_in_subtree([&](auto const& descendant) { if (!descendant.is_element()) return TraversalDecision::Continue; auto const& descendant_element = static_cast(descendant); if (matches(selector, style_sheet_for_rule, descendant_element, shadow_host, {}, {}, SelectorKind::Relative, anchor)) { has = true; return TraversalDecision::Break; } return TraversalDecision::Continue; }); return has; } case CSS::Selector::Combinator::ImmediateChild: { bool has = false; element.for_each_child([&](DOM::Node const& child) { if (!child.is_element()) return IterationDecision::Continue; auto const& child_element = static_cast(child); if (!matches(selector, style_sheet_for_rule, compound_index, child_element, shadow_host, {}, SelectorKind::Relative, anchor)) return IterationDecision::Continue; if (matches_relative_selector(selector, compound_index + 1, style_sheet_for_rule, child_element, shadow_host, anchor)) { has = true; return IterationDecision::Break; } return IterationDecision::Continue; }); return has; } case CSS::Selector::Combinator::NextSibling: { auto* sibling = element.next_element_sibling(); if (!sibling) return false; if (!matches(selector, style_sheet_for_rule, compound_index, *sibling, shadow_host, {}, SelectorKind::Relative, anchor)) return false; return matches_relative_selector(selector, compound_index + 1, style_sheet_for_rule, *sibling, shadow_host, anchor); } case CSS::Selector::Combinator::SubsequentSibling: { for (auto const* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) { if (!matches(selector, style_sheet_for_rule, compound_index, *sibling, shadow_host, {}, SelectorKind::Relative, anchor)) continue; if (matches_relative_selector(selector, compound_index + 1, style_sheet_for_rule, *sibling, shadow_host, anchor)) return true; } return false; } case CSS::Selector::Combinator::Column: TODO(); } return false; } // https://drafts.csswg.org/selectors-4/#relational static inline bool matches_has_pseudo_class(CSS::Selector const& selector, Optional style_sheet_for_rule, DOM::Element const& anchor, JS::GCPtr shadow_host) { return matches_relative_selector(selector, 0, style_sheet_for_rule, anchor, shadow_host, 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(element) && !is(element) && !is(element)) return false; return element.has_attribute(HTML::AttributeNames::href); } 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_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(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; auto const* attr = element.namespace_uri() == Namespace::HTML ? element.attributes()->get_attribute_with_lowercase_qualified_name(attribute_name) : element.attributes()->get_attribute(attribute_name); if (attribute.match_type == CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute) { // Early way out in case of an attribute existence selector. return attr != nullptr; } if (!attr) return false; auto case_sensitivity = [&](CSS::Selector::SimpleSelector::Attribute::CaseType case_type) { switch (case_type) { case CSS::Selector::SimpleSelector::Attribute::CaseType::CaseInsensitiveMatch: return CaseSensitivity::CaseInsensitive; case CSS::Selector::SimpleSelector::Attribute::CaseType::CaseSensitiveMatch: return CaseSensitivity::CaseSensitive; case CSS::Selector::SimpleSelector::Attribute::CaseType::DefaultMatch: // See: https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors if (element.document().is_html_document() && element.namespace_uri() == Namespace::HTML && attribute_name.is_one_of( HTML::AttributeNames::accept, HTML::AttributeNames::accept_charset, HTML::AttributeNames::align, HTML::AttributeNames::alink, HTML::AttributeNames::axis, HTML::AttributeNames::bgcolor, HTML::AttributeNames::charset, HTML::AttributeNames::checked, HTML::AttributeNames::clear, HTML::AttributeNames::codetype, HTML::AttributeNames::color, HTML::AttributeNames::compact, HTML::AttributeNames::declare, HTML::AttributeNames::defer, HTML::AttributeNames::dir, HTML::AttributeNames::direction, HTML::AttributeNames::disabled, HTML::AttributeNames::enctype, HTML::AttributeNames::face, HTML::AttributeNames::frame, HTML::AttributeNames::hreflang, HTML::AttributeNames::http_equiv, HTML::AttributeNames::lang, HTML::AttributeNames::language, HTML::AttributeNames::link, HTML::AttributeNames::media, HTML::AttributeNames::method, HTML::AttributeNames::multiple, HTML::AttributeNames::nohref, HTML::AttributeNames::noresize, HTML::AttributeNames::noshade, HTML::AttributeNames::nowrap, HTML::AttributeNames::readonly, HTML::AttributeNames::rel, HTML::AttributeNames::rev, HTML::AttributeNames::rules, HTML::AttributeNames::scope, HTML::AttributeNames::scrolling, HTML::AttributeNames::selected, HTML::AttributeNames::shape, HTML::AttributeNames::target, HTML::AttributeNames::text, HTML::AttributeNames::type, HTML::AttributeNames::valign, HTML::AttributeNames::valuetype, HTML::AttributeNames::vlink)) { return CaseSensitivity::CaseInsensitive; } return CaseSensitivity::CaseSensitive; } VERIFY_NOT_REACHED(); }(attribute.case_type); auto case_insensitive_match = case_sensitivity == CaseSensitivity::CaseInsensitive; switch (attribute.match_type) { case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch: return case_insensitive_match ? Infra::is_ascii_case_insensitive_match(attr->value(), attribute.value) : attr->value() == 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 const& attribute_value = attr->value(); 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() && attr->value().contains(attribute.value, case_sensitivity); case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment: { auto const& element_attr_value = attr->value(); 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() && attr->value().bytes_as_string_view().starts_with(attribute.value, case_sensitivity); case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString: return !attribute.value.is_empty() && attr->value().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