/* * 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 namespace Web::SelectorEngine { static inline bool matches(CSS::Selector const& selector, int component_list_index, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope, SelectorKind selector_kind, GC::Ptr 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 GC::Ptr traverse_up(GC::Ptr node, GC::Ptr 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://www.rfc-editor.org/rfc/rfc4647.html#section-3.3.2 // NB: Language tags only use ASCII characters, so we can get away with using StringView. static bool language_range_matches_tag(StringView language_range, StringView language_tag) { // 1. Split both the extended language range and the language tag being compared into a list of subtags by // dividing on the hyphen (%x2D) character. auto range_subtags = language_range.split_view('-', SplitBehavior::KeepEmpty); auto tag_subtags = language_tag.split_view('-', SplitBehavior::KeepEmpty); // Two subtags match if either they are the same when compared case-insensitively or the language range's subtag // is the wildcard '*'. auto subtags_match = [](StringView language_range_subtag, StringView language_subtag) { return language_range_subtag == "*"sv || language_range_subtag.equals_ignoring_ascii_case(language_subtag); }; // 2. Begin with the first subtag in each list. If the first subtag in the range does not match the first // subtag in the tag, the overall match fails. Otherwise, move to the next subtag in both the range and the // tag. auto tag_subtag = tag_subtags.begin(); auto range_subtag = range_subtags.begin(); if (!subtags_match(*range_subtag, *tag_subtag)) return false; ++tag_subtag; ++range_subtag; // 3. While there are more subtags left in the language range's list: while (!range_subtag.is_end()) { // A. If the subtag currently being examined in the range is the wildcard ('*'), move to the next subtag in // the range and continue with the loop. if (*range_subtag == "*"sv) { ++range_subtag; continue; } // B. Else, if there are no more subtags in the language tag's list, the match fails. if (tag_subtag.is_end()) return false; // C. Else, if the current subtag in the range's list matches the current subtag in the language tag's // list, move to the next subtag in both lists and continue with the loop. if (subtags_match(*range_subtag, *tag_subtag)) { ++range_subtag; ++tag_subtag; continue; } // D. Else, if the language tag's subtag is a "singleton" (a single letter or digit, which includes the // private-use subtag 'x') the match fails. if (tag_subtag->length() == 1 && is_ascii_alphanumeric((*tag_subtag)[0])) { return false; } // E. Else, move to the next subtag in the language tag's list and continue with the loop. ++tag_subtag; } // 4. When the language range's list has no more subtags, the match succeeds. return true; } // 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(); // The element’s content language matches a language range if its content language, as represented in BCP 47 // syntax, matches the given language range in an extended filtering operation per [RFC4647] Matching of Language // Tags (section 3.3.2). Both the content language and the language range must be canonicalized and converted to // extlang form as per section 4.5 of [RFC5646] prior to the extended filtering operation. The matching is // performed case-insensitively within the ASCII range. // FIXME: Canonicalize both as extlang. for (auto const& language_range : languages) { if (language_range_matches_tag(language_range, element_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, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, GC::Ref anchor) { if (compound_index >= selector.compound_selectors().size()) return matches(selector, element, shadow_host, context, {}, {}, 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, descendant_element, shadow_host, context, {}, {}, 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, compound_index, child_element, shadow_host, context, {}, SelectorKind::Relative, anchor)) return IterationDecision::Continue; if (matches_relative_selector(selector, compound_index + 1, child_element, shadow_host, context, anchor)) { has = true; return IterationDecision::Break; } return IterationDecision::Continue; }); return has; } case CSS::Selector::Combinator::NextSibling: { if (context.collect_per_element_selector_involvement_metadata) { const_cast(*anchor).set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(true); } auto* sibling = element.next_element_sibling(); if (!sibling) return false; if (!matches(selector, compound_index, *sibling, shadow_host, context, {}, SelectorKind::Relative, anchor)) return false; return matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, anchor); } case CSS::Selector::Combinator::SubsequentSibling: { if (context.collect_per_element_selector_involvement_metadata) { const_cast(*anchor).set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(true); } for (auto const* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) { if (!matches(selector, compound_index, *sibling, shadow_host, context, {}, SelectorKind::Relative, anchor)) continue; if (matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, 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, DOM::Element const& anchor, GC::Ptr shadow_host, MatchContext& context) { return matches_relative_selector(selector, 0, anchor, shadow_host, context, anchor); } static 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-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: // https://whatpr.org/html-attr-input-switch/9546/semantics-other.html#selector-indeterminate // input elements whose type attribute is in the Checkbox state, whose switch attribute is not set return input_element.indeterminate() && !element.has_attribute(HTML::AttributeNames::switch_); default: return false; } } // - progress elements with no value content attribute if (is(element)) { return !element.has_attribute(HTML::AttributeNames::value); } return false; } static inline void for_each_matching_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute_selector, GC::Ptr style_sheet_for_rule, DOM::Element const& element, Function const& process_attribute) { auto const& qualified_name = attribute_selector.qualified_name; auto const& attribute_name = qualified_name.name.name; switch (qualified_name.namespace_type) { // "In keeping with the Namespaces in the XML recommendation, default namespaces do not apply to attributes, // therefore attribute selectors without a namespace component apply only to attributes that have no namespace (equivalent to "|attr")" case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default: case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::None: if (auto const* attribute = element.attributes()->get_attribute(attribute_name)) (void)process_attribute(*attribute); return; case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any: { // When comparing the name part of a CSS attribute selector to the names of attributes on HTML elements in HTML // documents, the name part of the CSS attribute selector must first be converted to ASCII lowercase. The same // selector when compared to other attributes must be compared according to its original case. In both cases, the // comparison is case-sensitive. // https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors bool const case_insensitive = element.document().is_html_document() && element.namespace_uri() == Namespace::HTML; for (auto i = 0u; i < element.attributes()->length(); ++i) { auto const* attr = element.attributes()->item(i); bool matches = case_insensitive ? attr->local_name().equals_ignoring_ascii_case(attribute_name) : attr->local_name() == attribute_name; if (matches) { if (process_attribute(*attr) == IterationDecision::Break) break; } } return; } case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named: if (!style_sheet_for_rule) return; auto const& selector_namespace = style_sheet_for_rule->namespace_uri(qualified_name.namespace_); if (!selector_namespace.has_value()) return; if (auto const* attribute = element.attributes()->get_attribute_ns(selector_namespace, attribute_name)) (void)process_attribute(*attribute); return; } VERIFY_NOT_REACHED(); } static bool matches_single_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute_selector, DOM::Attr const& attribute, CaseSensitivity case_sensitivity) { auto const case_insensitive_match = case_sensitivity == CaseSensitivity::CaseInsensitive; switch (attribute_selector.match_type) { case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch: return case_insensitive_match ? attribute.value().equals_ignoring_ascii_case(attribute_selector.value) : attribute.value() == attribute_selector.value; case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord: { if (attribute_selector.value.is_empty()) { // This selector is always false is match value is empty. return false; } auto const& attribute_value = attribute.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 ? value.equals_ignoring_ascii_case(attribute_selector.value) : value == attribute_selector.value) { return true; } } return false; } case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString: return !attribute_selector.value.is_empty() && attribute.value().contains(attribute_selector.value, case_sensitivity); case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment: { // https://www.w3.org/TR/CSS2/selector.html#attribute-selectors // [att|=val] // Represents an element with the att attribute, its value either being exactly "val" or beginning with "val" immediately followed by "-" (U+002D). auto const& element_attr_value = attribute.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_selector.value.is_empty(); } if (attribute_selector.value.is_empty()) { return false; } auto element_attribute_length = element_attr_value.bytes_as_string_view().length(); auto attribute_length = attribute_selector.value.bytes_as_string_view().length(); if (element_attribute_length < attribute_length) return false; if (attribute_length == element_attribute_length) { return case_insensitive_match ? element_attr_value.equals_ignoring_ascii_case(attribute_selector.value) : element_attr_value == attribute_selector.value; } return element_attr_value.starts_with_bytes(attribute_selector.value, case_insensitive_match ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive) && element_attr_value.bytes_as_string_view()[attribute_length] == '-'; } case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString: return !attribute_selector.value.is_empty() && attribute.value().bytes_as_string_view().starts_with(attribute_selector.value, case_sensitivity); case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString: return !attribute_selector.value.is_empty() && attribute.value().bytes_as_string_view().ends_with(attribute_selector.value, case_sensitivity); case CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute: return true; } return false; } static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, [[maybe_unused]] GC::Ptr style_sheet_for_rule, DOM::Element const& element) { auto const& attribute_name = attribute.qualified_name.name.name; 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.qualified_name.namespace_type == CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default && 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); bool found_matching_attribute = false; for_each_matching_attribute(attribute, style_sheet_for_rule, element, [&attribute, case_sensitivity, &found_matching_attribute](DOM::Attr const& attr) { if (matches_single_attribute(attribute, attr, case_sensitivity)) { found_matching_attribute = true; return IterationDecision::Break; } return IterationDecision::Continue; }); return found_matching_attribute; } 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; } /// Returns true if this selector should be blocked from matching against the shadow host from within a shadow tree. /// Only :host pseudo-class is allowed to match the shadow host from within shadow tree, all other selectors (including other pseudo-classes) are blocked. /// Compound selectors (:has(), :is(), :where()), nesting, and pseudo-elements are allowed as they may contain or be contained within :host. static inline bool should_block_shadow_host_matching(CSS::Selector::SimpleSelector const& component, GC::Ptr shadow_host, DOM::Element const& element) { if (!shadow_host || &element != shadow_host.ptr()) return false; // From within shadow tree, only :host pseudo-class can match the host element if (component.type == CSS::Selector::SimpleSelector::Type::PseudoClass) { auto const& pseudo_class = component.pseudo_class(); return pseudo_class.type != CSS::PseudoClass::Host && pseudo_class.type != CSS::PseudoClass::Has && pseudo_class.type != CSS::PseudoClass::Is && pseudo_class.type != CSS::PseudoClass::Where; } // Allow nesting and PseudoElement as it may contain :host class if (component.type == CSS::Selector::SimpleSelector::Type::Nesting || component.type == CSS::Selector::SimpleSelector::Type::PseudoElement) return false; return true; } // 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_or_editing_host(); } // https://drafts.csswg.org/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. // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-open // The :open pseudo-class must match any element falling into one of the following categories: // - details elements that have an open attribute // - dialog elements that have an open attribute if (is(element) || is(element)) return open == element.has_attribute(HTML::AttributeNames::open); // - select elements that are a drop-down box and whose drop-down boxes are open if (auto const* select = as_if(element)) return open == select->is_open(); // - input elements that support a picker and whose pickers are open if (auto const* input = as_if(element)) return open == (input->supports_a_picker() && input->is_open()); return false; } // https://drafts.csswg.org/css-scoping/#host-selector static inline bool matches_host_pseudo_class(GC::Ref element, GC::Ptr shadow_host, MatchContext& context, CSS::SelectorList const& argument_selector_list) { // When evaluated in the context of a shadow tree, it matches the shadow tree’s shadow host if the shadow host, // in its normal context, matches the selector argument. In any other context, it matches nothing. if (!shadow_host || element != shadow_host) return false; // NOTE: There's either 0 or 1 argument selector, since the syntax is :host or :host() if (!argument_selector_list.is_empty()) return matches(argument_selector_list.first(), element, nullptr, context); return true; } static bool matches_optimal_value_pseudo_class(DOM::Element const& element, HTML::HTMLMeterElement::ValueState desired_state) { if (auto* meter = as_if(element)) return meter->value_state() == desired_state; return false; } static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector const& pseudo_class, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope, SelectorKind selector_kind) { context.attempted_pseudo_class_matches.set(pseudo_class.type, true); switch (pseudo_class.type) { case CSS::PseudoClass::__Count: VERIFY_NOT_REACHED(); case CSS::PseudoClass::Link: 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 element.matches_link_pseudo_class(); case CSS::PseudoClass::LocalLink: { return element.matches_local_link_pseudo_class(); } case CSS::PseudoClass::Visited: // FIXME: Maybe match this selector sometimes? return false; case CSS::PseudoClass::Active: return element.is_active(); case CSS::PseudoClass::Hover: return matches_hover_pseudo_class(element); case CSS::PseudoClass::Focus: return element.is_focused(); case CSS::PseudoClass::FocusVisible: // FIXME: We should only apply this when a visible focus is useful. Decide when that is! return element.is_focused(); case CSS::PseudoClass::FocusWithin: { auto* focused_element = element.document().focused_element(); return focused_element && element.is_inclusive_ancestor_of(*focused_element); } case CSS::PseudoClass::FirstChild: if (context.collect_per_element_selector_involvement_metadata) { const_cast(element).set_affected_by_sibling_position_or_count_pseudo_class(true); } return !element.previous_element_sibling(); case CSS::PseudoClass::LastChild: if (context.collect_per_element_selector_involvement_metadata) { const_cast(element).set_affected_by_sibling_position_or_count_pseudo_class(true); } return !element.next_element_sibling(); case CSS::PseudoClass::OnlyChild: if (context.collect_per_element_selector_involvement_metadata) { const_cast(element).set_affected_by_sibling_position_or_count_pseudo_class(true); } return !(element.previous_element_sibling() || element.next_element_sibling()); case CSS::PseudoClass::Empty: { if (!element.has_children()) return true; if (element.first_child_of_type()) return false; // NOTE: CSS Selectors level 4 changed ":empty" to also match whitespace-only text nodes. // However, none of the major browser supports this yet, so let's just hang back until they do. bool has_nonempty_text_child = false; element.for_each_child_of_type([&](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 CSS::PseudoClass::Root: return is(element); case CSS::PseudoClass::Host: return matches_host_pseudo_class(element, shadow_host, context, pseudo_class.argument_selector_list); case CSS::PseudoClass::Scope: return scope ? &element == scope : is(element); case CSS::PseudoClass::FirstOfType: if (context.collect_per_element_selector_involvement_metadata) { const_cast(element).set_affected_by_sibling_position_or_count_pseudo_class(true); } return !previous_sibling_with_same_tag_name(element); case CSS::PseudoClass::LastOfType: if (context.collect_per_element_selector_involvement_metadata) { const_cast(element).set_affected_by_sibling_position_or_count_pseudo_class(true); } return !next_sibling_with_same_tag_name(element); case CSS::PseudoClass::OnlyOfType: if (context.collect_per_element_selector_involvement_metadata) { const_cast(element).set_affected_by_sibling_position_or_count_pseudo_class(true); } return !previous_sibling_with_same_tag_name(element) && !next_sibling_with_same_tag_name(element); case CSS::PseudoClass::Lang: return matches_lang_pseudo_class(element, pseudo_class.languages); case CSS::PseudoClass::Disabled: return element.matches_disabled_pseudo_class(); case CSS::PseudoClass::Enabled: return element.matches_enabled_pseudo_class(); case CSS::PseudoClass::Checked: return element.matches_checked_pseudo_class(); case CSS::PseudoClass::Indeterminate: return matches_indeterminate_pseudo_class(element); case CSS::PseudoClass::HighValue: if (auto* meter = as_if(element)) return meter->value() > meter->high(); return false; case CSS::PseudoClass::LowValue: if (auto* meter = as_if(element)) return meter->value() < meter->low(); return false; case CSS::PseudoClass::OptimalValue: return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::Optimal); case CSS::PseudoClass::SuboptimalValue: return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::Suboptimal); case CSS::PseudoClass::EvenLessGoodValue: return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::EvenLessGood); case CSS::PseudoClass::Defined: return element.is_defined(); case CSS::PseudoClass::Has: // :has() cannot be nested in a :has() if (selector_kind == SelectorKind::Relative) return false; if (context.collect_per_element_selector_involvement_metadata) { if (&element == context.subject) { const_cast(element).set_affected_by_has_pseudo_class_in_subject_position(true); } else { const_cast(element).set_affected_by_has_pseudo_class_in_non_subject_position(true); } } // These selectors should be relative selectors (https://drafts.csswg.org/selectors-4/#relative-selector) for (auto& selector : pseudo_class.argument_selector_list) { if (matches_has_pseudo_class(selector, element, shadow_host, context)) return true; } return false; case CSS::PseudoClass::Is: case CSS::PseudoClass::Where: for (auto& selector : pseudo_class.argument_selector_list) { if (matches(selector, element, shadow_host, context)) return true; } return false; case CSS::PseudoClass::Not: for (auto& selector : pseudo_class.argument_selector_list) { if (matches(selector, element, shadow_host, context)) return false; } return true; case CSS::PseudoClass::NthChild: case CSS::PseudoClass::NthLastChild: case CSS::PseudoClass::NthOfType: case CSS::PseudoClass::NthLastOfType: { auto const step_size = pseudo_class.nth_child_pattern.step_size; auto const offset = pseudo_class.nth_child_pattern.offset; if (step_size == 0 && offset == 0) return false; // "If both a and b are equal to zero, the pseudo-class represents no element in the document tree." auto const* parent = element.parent(); if (!parent) return false; if (context.collect_per_element_selector_involvement_metadata) { const_cast(element).set_affected_by_nth_child_pseudo_class(true); } auto matches_selector_list = [&context, shadow_host](CSS::SelectorList const& list, DOM::Element const& element) { if (list.is_empty()) return true; for (auto const& child_selector : list) { if (matches(child_selector, element, shadow_host, context)) { return true; } } return false; }; int index = 1; switch (pseudo_class.type) { case CSS::PseudoClass::__Count: VERIFY_NOT_REACHED(); case CSS::PseudoClass::NthChild: { if (!matches_selector_list(pseudo_class.argument_selector_list, element)) return false; for (auto* child = parent->first_child_of_type(); child && child != &element; child = child->next_element_sibling()) { if (matches_selector_list(pseudo_class.argument_selector_list, *child)) ++index; } break; } case CSS::PseudoClass::NthLastChild: { if (!matches_selector_list(pseudo_class.argument_selector_list, element)) return false; for (auto* child = parent->last_child_of_type(); child && child != &element; child = child->previous_element_sibling()) { if (matches_selector_list(pseudo_class.argument_selector_list, *child)) ++index; } break; } case CSS::PseudoClass::NthOfType: { for (auto* child = previous_sibling_with_same_tag_name(element); child; child = previous_sibling_with_same_tag_name(*child)) ++index; break; } case CSS::PseudoClass::NthLastOfType: { for (auto* child = next_sibling_with_same_tag_name(element); child; child = next_sibling_with_same_tag_name(*child)) ++index; break; } default: VERIFY_NOT_REACHED(); } // When "step_size == -1", selector represents first "offset" elements in document tree. if (step_size == -1) return !(offset <= 0 || index > offset); // When "step_size == 1", selector represents last "offset" elements in document tree. if (step_size == 1) return !(offset < 0 || index < offset); // When "step_size == 0", selector picks only the "offset" element. if (step_size == 0) return index == offset; // If both are negative, nothing can match. if (step_size < 0 && offset < 0) return false; // Like "a % b", but handles negative integers correctly. auto const canonical_modulo = [](int a, int b) -> int { int c = a % b; if ((c < 0 && b > 0) || (c > 0 && b < 0)) { c += b; } return c; }; // When "step_size < 0", we start at "offset" and count backwards. if (step_size < 0) return index <= offset && canonical_modulo(index - offset, -step_size) == 0; // Otherwise, we start at "offset" and count forwards. return index >= offset && canonical_modulo(index - offset, step_size) == 0; } case CSS::PseudoClass::Playing: { if (!is(element)) return false; auto const& media_element = static_cast(element); return !media_element.paused(); } case CSS::PseudoClass::Paused: { if (!is(element)) return false; auto const& media_element = static_cast(element); return media_element.paused(); } case CSS::PseudoClass::Seeking: { if (!is(element)) return false; auto const& media_element = static_cast(element); return media_element.seeking(); } case CSS::PseudoClass::Muted: { if (!is(element)) return false; auto const& media_element = static_cast(element); return media_element.muted(); } case CSS::PseudoClass::VolumeLocked: { // FIXME: Currently we don't allow the user to specify an override volume, so this is always false. // Once we do, implement this! return false; } case CSS::PseudoClass::Buffering: { if (!is(element)) return false; auto const& media_element = static_cast(element); return media_element.blocked(); } case CSS::PseudoClass::Stalled: { if (!is(element)) return false; auto const& media_element = static_cast(element); return media_element.stalled(); } case CSS::PseudoClass::Target: return element.is_target(); case CSS::PseudoClass::TargetWithin: { auto* target_element = element.document().target_element(); if (!target_element) return false; return element.is_inclusive_ancestor_of(*target_element); } case CSS::PseudoClass::Dir: { // "Values other than ltr and rtl are not invalid, but do not match anything." // - https://www.w3.org/TR/selectors-4/#the-dir-pseudo if (!pseudo_class.ident.has_value()) return false; if (!first_is_one_of(pseudo_class.ident->keyword, CSS::Keyword::Ltr, CSS::Keyword::Rtl)) return false; switch (element.directionality()) { case DOM::Element::Directionality::Ltr: return pseudo_class.ident->keyword == CSS::Keyword::Ltr; case DOM::Element::Directionality::Rtl: return pseudo_class.ident->keyword == CSS::Keyword::Rtl; } VERIFY_NOT_REACHED(); } case CSS::PseudoClass::ReadOnly: return !matches_read_write_pseudo_class(element); case CSS::PseudoClass::ReadWrite: return matches_read_write_pseudo_class(element); case CSS::PseudoClass::PlaceholderShown: { // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-placeholder-shown // 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(element) && element.has_attribute(HTML::AttributeNames::placeholder)) { auto const& input_element = static_cast(element); 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; } case CSS::PseudoClass::Open: return matches_open_state_pseudo_class(element, pseudo_class.type == CSS::PseudoClass::Open); case CSS::PseudoClass::Modal: { // https://drafts.csswg.org/selectors/#modal-state if (is(element)) { auto const& dialog_element = static_cast(element); return dialog_element.is_modal(); } // FIXME: fullscreen elements are also modal. return false; } case CSS::PseudoClass::PopoverOpen: { // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open // The :popover-open pseudo-class is defined to match any HTML element whose popover attribute is not in the no popover state and whose popover visibility state is showing. if (is(element) && element.has_attribute(HTML::AttributeNames::popover)) { auto& html_element = static_cast(element); return html_element.popover_visibility_state() == HTML::HTMLElement::PopoverVisibilityState::Showing; } return false; } case CSS::PseudoClass::Valid: { // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-valid // The :valid pseudo-class must match any element falling into one of the following categories: // - elements that are candidates for constraint validation and that satisfy their constraints if (auto form_associated_element = as_if(element)) if (form_associated_element->is_candidate_for_constraint_validation() && form_associated_element->satisfies_its_constraints()) return true; // - form elements that are not the form owner of any elements that themselves are candidates for constraint validation but do not satisfy their constraints if (auto form_element = as_if(element)) { bool has_invalid_elements = false; element.for_each_in_subtree([&](auto& node) { if (auto form_associated_element = as_if(&node)) { if (form_associated_element->form() == form_element && form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { has_invalid_elements = true; return TraversalDecision::Break; } } return TraversalDecision::Continue; }); if (!has_invalid_elements) return true; } // - fieldset elements that have no descendant elements that themselves are candidates for constraint validation but do not satisfy their constraints if (is(element)) { bool has_invalid_children = false; element.for_each_in_subtree([&](auto& node) { if (auto form_associated_element = as_if(&node)) { if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { has_invalid_children = true; return TraversalDecision::Break; } } return TraversalDecision::Continue; }); if (!has_invalid_children) return true; } return false; } case CSS::PseudoClass::Invalid: { // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-invalid // The :invalid pseudo-class must match any element falling into one of the following categories: // - elements that are candidates for constraint validation but that do not satisfy their constraints if (auto form_associated_element = as_if(element)) if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) return true; // - form elements that are the form owner of one or more elements that themselves are candidates for constraint validation but do not satisfy their constraints if (auto form_element = as_if(element)) { bool has_invalid_elements = false; element.for_each_in_subtree([&](auto& node) { if (auto form_associated_element = as_if(&node)) { if (form_associated_element->form() == form_element && form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { has_invalid_elements = true; return TraversalDecision::Break; } } return TraversalDecision::Continue; }); if (has_invalid_elements) return true; } // - fieldset elements that have of one or more descendant elements that themselves are candidates for constraint validation but do not satisfy their constraints if (is(element)) { bool has_invalid_children = false; element.for_each_in_subtree([&](auto& node) { if (auto form_associated_element = as_if(&node)) { if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { has_invalid_children = true; return TraversalDecision::Break; } } return TraversalDecision::Continue; }); if (has_invalid_children) return true; } return false; } case CSS::PseudoClass::UserValid: { // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-valid // The :user-valid pseudo-class must match input, textarea, and select elements whose user validity is true, bool user_validity = false; if (auto input_element = as_if(element)) { user_validity = input_element->user_validity(); } else if (auto select_element = as_if(element)) { user_validity = select_element->user_validity(); } else if (auto text_area_element = as_if(element)) { user_validity = text_area_element->user_validity(); } if (!user_validity) return false; // are candidates for constraint validation, and that satisfy their constraints. auto& form_associated_element = as(element); if (form_associated_element.is_candidate_for_constraint_validation() && form_associated_element.satisfies_its_constraints()) return true; return false; } case CSS::PseudoClass::UserInvalid: { // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-invalid // The :user-invalid pseudo-class must match input, textarea, and select elements whose user validity is true, bool user_validity = false; if (auto input_element = as_if(element)) { user_validity = input_element->user_validity(); } else if (auto select_element = as_if(element)) { user_validity = select_element->user_validity(); } else if (auto text_area_element = as_if(element)) { user_validity = text_area_element->user_validity(); } if (!user_validity) return false; // are candidates for constraint validation but do not satisfy their constraints. auto& form_associated_element = as(element); if (form_associated_element.is_candidate_for_constraint_validation() && !form_associated_element.satisfies_its_constraints()) return true; return false; } } return false; } static ALWAYS_INLINE bool matches_namespace( CSS::Selector::SimpleSelector::QualifiedName const& qualified_name, DOM::Element const& element, GC::Ptr style_sheet_for_rule) { switch (qualified_name.namespace_type) { case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default: // "if no default namespace has been declared for selectors, this is equivalent to *|E." if (!style_sheet_for_rule || !style_sheet_for_rule->default_namespace_rule()) return true; // "Otherwise it is equivalent to ns|E where ns is the default namespace." return element.namespace_uri() == style_sheet_for_rule->default_namespace_rule()->namespace_uri(); case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::None: // "elements with name E without a namespace" return !element.namespace_uri().has_value(); case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any: // "elements with name E in any namespace, including those without a namespace" return true; case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named: // "elements with name E in namespace ns" // Unrecognized namespace prefixes are invalid, so don't match. // (We can't detect this at parse time, since a namespace rule may be inserted later.) // So, if we don't have a context to look up namespaces from, we fail to match. if (!style_sheet_for_rule) return false; auto selector_namespace = style_sheet_for_rule->namespace_uri(qualified_name.namespace_); return selector_namespace.has_value() && selector_namespace.value() == element.namespace_uri(); } VERIFY_NOT_REACHED(); } static inline bool matches(CSS::Selector::SimpleSelector const& component, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope, SelectorKind selector_kind, [[maybe_unused]] GC::Ptr anchor) { if (should_block_shadow_host_matching(component, shadow_host, element)) return false; switch (component.type) { case CSS::Selector::SimpleSelector::Type::Universal: case CSS::Selector::SimpleSelector::Type::TagName: { auto const& qualified_name = component.qualified_name(); // Reject if the tag name doesn't match if (component.type == CSS::Selector::SimpleSelector::Type::TagName) { // See https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors if (element.document().document_type() == DOM::Document::Type::HTML && element.namespace_uri() == Namespace::HTML) { if (qualified_name.name.lowercase_name != element.local_name()) return false; } else if (!qualified_name.name.name.equals_ignoring_ascii_case(element.local_name())) { return false; } } return matches_namespace(qualified_name, element, context.style_sheet_for_rule); } case CSS::Selector::SimpleSelector::Type::Id: return component.name() == element.id(); case CSS::Selector::SimpleSelector::Type::Class: { // Class selectors are matched case insensitively in quirks mode. // See: https://drafts.csswg.org/selectors-4/#class-html auto case_sensitivity = element.document().in_quirks_mode() ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive; return element.has_class(component.name(), case_sensitivity); } case CSS::Selector::SimpleSelector::Type::Attribute: return matches_attribute(component.attribute(), context.style_sheet_for_rule, element); case CSS::Selector::SimpleSelector::Type::PseudoClass: return matches_pseudo_class(component.pseudo_class(), element, shadow_host, context, scope, selector_kind); case CSS::Selector::SimpleSelector::Type::PseudoElement: // Pseudo-element matching/not-matching is handled in the top level matches(). return true; case CSS::Selector::SimpleSelector::Type::Nesting: // Nesting either behaves like :is(), or like :scope. // :is() is handled already, by us replacing it with :is() directly, so if we // got here, it's :scope. return matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector { .type = CSS::PseudoClass::Scope }, element, shadow_host, context, scope, selector_kind); case CSS::Selector::SimpleSelector::Type::Invalid: // Invalid selectors never match return false; } VERIFY_NOT_REACHED(); } bool matches(CSS::Selector const& selector, int component_list_index, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope, SelectorKind selector_kind, GC::Ptr anchor) { auto& compound_selector = selector.compound_selectors()[component_list_index]; for (auto& simple_selector : compound_selector.simple_selectors) { if (!matches(simple_selector, element, shadow_host, context, scope, selector_kind, anchor)) { return false; } } if (selector_kind == SelectorKind::Relative && component_list_index == 0) { VERIFY(anchor); return &element != anchor; } switch (compound_selector.combinator) { case CSS::Selector::Combinator::None: VERIFY(selector_kind != SelectorKind::Relative); return true; case CSS::Selector::Combinator::Descendant: VERIFY(component_list_index != 0); for (auto ancestor = traverse_up(element, shadow_host); ancestor; ancestor = traverse_up(ancestor, shadow_host)) { if (!is(*ancestor)) continue; if (ancestor == anchor) return false; if (matches(selector, component_list_index - 1, static_cast(*ancestor), shadow_host, context, scope, selector_kind, anchor)) return true; } return false; case CSS::Selector::Combinator::ImmediateChild: { VERIFY(component_list_index != 0); auto parent = traverse_up(element, shadow_host); if (!parent || !parent->is_element()) return false; return matches(selector, component_list_index - 1, static_cast(*parent), shadow_host, context, scope, selector_kind, anchor); } case CSS::Selector::Combinator::NextSibling: if (context.collect_per_element_selector_involvement_metadata) { const_cast(element).set_affected_by_direct_sibling_combinator(true); auto new_sibling_invalidation_distance = max(selector.sibling_invalidation_distance(), element.sibling_invalidation_distance()); const_cast(element).set_sibling_invalidation_distance(new_sibling_invalidation_distance); } VERIFY(component_list_index != 0); if (auto* sibling = element.previous_element_sibling()) return matches(selector, component_list_index - 1, *sibling, shadow_host, context, scope, selector_kind, anchor); return false; case CSS::Selector::Combinator::SubsequentSibling: if (context.collect_per_element_selector_involvement_metadata) { const_cast(element).set_affected_by_indirect_sibling_combinator(true); } VERIFY(component_list_index != 0); for (auto* sibling = element.previous_element_sibling(); sibling; sibling = sibling->previous_element_sibling()) { if (matches(selector, component_list_index - 1, *sibling, shadow_host, context, scope, selector_kind, anchor)) return true; } return false; case CSS::Selector::Combinator::Column: TODO(); } VERIFY_NOT_REACHED(); } bool fast_matches(CSS::Selector const& selector, DOM::Element const& element_to_match, GC::Ptr shadow_host, MatchContext& context); bool matches(CSS::Selector const& selector, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context, Optional pseudo_element, GC::Ptr scope, SelectorKind selector_kind, GC::Ptr anchor) { if (selector_kind == SelectorKind::Normal && selector.can_use_fast_matches()) { return fast_matches(selector, element, shadow_host, context); } VERIFY(!selector.compound_selectors().is_empty()); if (pseudo_element.has_value() && selector.pseudo_element().has_value() && selector.pseudo_element().value().type() != pseudo_element) return false; if (!pseudo_element.has_value() && selector.pseudo_element().has_value()) return false; return matches(selector, selector.compound_selectors().size() - 1, element, shadow_host, context, scope, selector_kind, anchor); } static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& simple_selector, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context) { if (should_block_shadow_host_matching(simple_selector, shadow_host, element)) return false; switch (simple_selector.type) { case CSS::Selector::SimpleSelector::Type::Universal: return matches_namespace(simple_selector.qualified_name(), element, context.style_sheet_for_rule); case CSS::Selector::SimpleSelector::Type::TagName: // https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors // When comparing a CSS element type selector to the names of HTML elements in HTML documents, the CSS element type selector must first be converted to ASCII lowercase. The // same selector when compared to other elements must be compared according to its original case. In both cases, to match the values must be identical to each other (and therefore // the comparison is case sensitive). if (element.namespace_uri() == Namespace::HTML && element.document().document_type() == DOM::Document::Type::HTML) { if (simple_selector.qualified_name().name.lowercase_name != element.local_name()) return false; } else if (simple_selector.qualified_name().name.name != element.local_name()) { // NOTE: Any other elements are either SVG, XHTML or MathML, all of which are case-sensitive. return false; } return matches_namespace(simple_selector.qualified_name(), element, context.style_sheet_for_rule); case CSS::Selector::SimpleSelector::Type::Class: { // Class selectors are matched case insensitively in quirks mode. // See: https://drafts.csswg.org/selectors-4/#class-html auto case_sensitivity = element.document().in_quirks_mode() ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive; return element.has_class(simple_selector.name(), case_sensitivity); } case CSS::Selector::SimpleSelector::Type::Id: return simple_selector.name() == element.id(); case CSS::Selector::SimpleSelector::Type::Attribute: return matches_attribute(simple_selector.attribute(), context.style_sheet_for_rule, element); case CSS::Selector::SimpleSelector::Type::PseudoClass: return matches_pseudo_class(simple_selector.pseudo_class(), element, shadow_host, context, nullptr, SelectorKind::Normal); default: VERIFY_NOT_REACHED(); } } static bool fast_matches_compound_selector(CSS::Selector::CompoundSelector const& compound_selector, DOM::Element const& element, GC::Ptr shadow_host, MatchContext& context) { for (auto const& simple_selector : compound_selector.simple_selectors) { if (!fast_matches_simple_selector(simple_selector, element, shadow_host, context)) return false; } return true; } bool fast_matches(CSS::Selector const& selector, DOM::Element const& element_to_match, GC::Ptr shadow_host, MatchContext& context) { DOM::Element const* current = &element_to_match; ssize_t compound_selector_index = selector.compound_selectors().size() - 1; if (!fast_matches_compound_selector(selector.compound_selectors().last(), *current, shadow_host, context)) return false; // NOTE: If we fail after following a child combinator, we may need to backtrack // to the last matched descendant. We store the state here. struct { GC::Ptr element; ssize_t compound_selector_index = 0; } backtrack_state; for (;;) { // NOTE: There should always be a leftmost compound selector without combinator that kicks us out of this loop. VERIFY(compound_selector_index >= 0); auto const* compound_selector = &selector.compound_selectors()[compound_selector_index]; switch (compound_selector->combinator) { case CSS::Selector::Combinator::None: return true; case CSS::Selector::Combinator::Descendant: backtrack_state = { current->parent_element(), compound_selector_index }; compound_selector = &selector.compound_selectors()[--compound_selector_index]; for (current = current->parent_element(); current; current = current->parent_element()) { if (fast_matches_compound_selector(*compound_selector, *current, shadow_host, context)) break; } if (!current) return false; break; case CSS::Selector::Combinator::ImmediateChild: compound_selector = &selector.compound_selectors()[--compound_selector_index]; current = current->parent_element(); if (!current) return false; if (!fast_matches_compound_selector(*compound_selector, *current, shadow_host, context)) { if (backtrack_state.element) { current = backtrack_state.element; compound_selector_index = backtrack_state.compound_selector_index; continue; } return false; } break; default: VERIFY_NOT_REACHED(); } } } }