blob: 21c2fb41eae7672cd9a8de2e4673d668952c8418 [file] [edit]
/*
* Copyright (C) 1999 Lars Knoll (knoll@kde.org)
* (C) 2004-2005 Allan Sandfeld Jensen (kde@carewolf.com)
* Copyright (C) 2006, 2007 Nicholas Shanks (webkit@nickshanks.com)
* Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Apple Inc.
* All rights reserved.
* Copyright (C) 2007 Alexey Proskuryakov <ap@webkit.org>
* Copyright (C) 2007, 2008 Eric Seidel <eric@webkit.org>
* Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved.
* (http://www.torchmobile.com/)
* Copyright (c) 2011, Code Aurora Forum. All rights reserved.
* Copyright (C) Research In Motion Limited 2011. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "third_party/blink/renderer/core/css/selector_checker.h"
#include <algorithm>
#include "base/auto_reset.h"
#include "base/compiler_specific.h"
#include "third_party/blink/public/mojom/input/focus_type.mojom-blink.h"
#include "third_party/blink/renderer/core/css/active_navigation_condition.h"
#include "third_party/blink/renderer/core/css/check_pseudo_has_argument_context.h"
#include "third_party/blink/renderer/core/css/check_pseudo_has_cache_scope.h"
#include "third_party/blink/renderer/core/css/css_selector_list.h"
#include "third_party/blink/renderer/core/css/navigation_query.h"
#include "third_party/blink/renderer/core/css/part_names.h"
#include "third_party/blink/renderer/core/css/post_style_update_scope.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/css/style_rule.h"
#include "third_party/blink/renderer/core/css/style_scope_data.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
#include "third_party/blink/renderer/core/dom/layout_tree_builder_traversal.h"
#include "third_party/blink/renderer/core/dom/node-inl.h"
#include "third_party/blink/renderer/core/dom/nth_index_cache.h"
#include "third_party/blink/renderer/core/dom/popover_data.h"
#include "third_party/blink/renderer/core/dom/pseudo_element.h"
#include "third_party/blink/renderer/core/dom/scroll_button_pseudo_element.h"
#include "third_party/blink/renderer/core/dom/scroll_marker_group_data.h"
#include "third_party/blink/renderer/core/dom/scroll_marker_group_pseudo_element.h"
#include "third_party/blink/renderer/core/dom/scroll_marker_pseudo_element.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/dom/slot_assignment_engine.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/picture_in_picture_controller.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/fullscreen/fullscreen.h"
#include "third_party/blink/renderer/core/html/custom/element_internals.h"
#include "third_party/blink/renderer/core/html/forms/html_button_element.h"
#include "third_party/blink/renderer/core/html/forms/html_data_list_element.h"
#include "third_party/blink/renderer/core/html/forms/html_field_set_element.h"
#include "third_party/blink/renderer/core/html/forms/html_form_control_element.h"
#include "third_party/blink/renderer/core/html/forms/html_form_element.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/forms/html_option_element.h"
#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
#include "third_party/blink/renderer/core/html/html_anchor_element.h"
#include "third_party/blink/renderer/core/html/html_capability_element_base.h"
#include "third_party/blink/renderer/core/html/html_details_element.h"
#include "third_party/blink/renderer/core/html/html_dialog_element.h"
#include "third_party/blink/renderer/core/html/html_document.h"
#include "third_party/blink/renderer/core/html/html_frame_element_base.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html/html_install_element.h"
#include "third_party/blink/renderer/core/html/html_menu_bar_element.h"
#include "third_party/blink/renderer/core/html/html_menu_item_element.h"
#include "third_party/blink/renderer/core/html/html_menu_list_element.h"
#include "third_party/blink/renderer/core/html/html_slot_element.h"
#include "third_party/blink/renderer/core/html/media/html_audio_element.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/html/media/html_video_element.h"
#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
#include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h"
#include "third_party/blink/renderer/core/html/track/vtt/vtt_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/layout/custom_scrollbar.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/page/focus_controller.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/page/scrolling/fragment_anchor.h"
#include "third_party/blink/renderer/core/page/spatial_navigation.h"
#include "third_party/blink/renderer/core/page/spatial_navigation_controller.h"
#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/core/scroll/scrollable_area.h"
#include "third_party/blink/renderer/core/scroll/scrollbar_theme.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/core/style/computed_style_constants.h"
#include "third_party/blink/renderer/core/svg/svg_element.h"
#include "third_party/blink/renderer/core/view_transition/view_transition.h"
#include "third_party/blink/renderer/core/view_transition/view_transition_pseudo_element_base.h"
#include "third_party/blink/renderer/core/view_transition/view_transition_utils.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
#include "third_party/blink/renderer/platform/wtf/text/string_view.h"
namespace blink {
static bool IsFrameFocused(const Element& element) {
return element.GetDocument().GetFrame() && element.GetDocument()
.GetFrame()
->Selection()
.FrameIsFocusedAndActive();
}
static bool MatchesSpatialNavigationFocusPseudoClass(const Element& element) {
auto* option_element = DynamicTo<HTMLOptionElement>(element);
return option_element && option_element->SpatialNavigationFocused() &&
IsFrameFocused(element);
}
static bool MatchesHasDatalistPseudoClass(const Element& element) {
auto* html_input_element = DynamicTo<HTMLInputElement>(element);
return html_input_element && html_input_element->DataList();
}
static bool MatchesHasOpenMenuitemPseudoClass(const Element& element) {
DCHECK(RuntimeEnabledFeatures::MenuElementsEnabled());
if (auto* menu_owner_element = DynamicTo<HTMLMenuOwnerElement>(element)) {
for (HTMLMenuItemElement& menu_item : menu_owner_element->ItemList()) {
if (menu_item.IsSubmenuOpen()) {
return true;
}
}
}
return false;
}
static bool MatchesListBoxPseudoClass(const Element& element) {
auto* html_select_element = DynamicTo<HTMLSelectElement>(element);
return html_select_element && !html_select_element->UsesMenuList();
}
static bool MatchesMultiSelectFocusPseudoClass(const Element& element) {
auto* option_element = DynamicTo<HTMLOptionElement>(element);
return option_element && option_element->IsMultiSelectFocused() &&
IsFrameFocused(element);
}
static bool MatchesTagName(const Element& element,
const QualifiedName& tag_q_name) {
DCHECK_NE(tag_q_name, AnyQName());
const AtomicString& local_name = tag_q_name.LocalName();
DCHECK_NE(local_name, CSSSelector::UniversalSelectorAtom());
if (local_name != element.localName()) {
if (element.IsHTMLElement() || !IsA<HTMLDocument>(element.GetDocument())) {
return false;
}
// Non-html elements in html documents are normalized to their camel-cased
// version during parsing if applicable. Yet, type selectors are lower-cased
// for selectors in html documents. Compare the upper case converted names
// instead to allow matching SVG elements like foreignObject.
if (element.TagQName().LocalNameUpper() != tag_q_name.LocalNameUpper()) {
return false;
}
}
const AtomicString& namespace_uri = tag_q_name.NamespaceURI();
return namespace_uri == g_star_atom ||
namespace_uri == element.namespaceURI();
}
static bool MatchesUniversalTagName(const Element& element,
const QualifiedName& tag_q_name) {
if (tag_q_name == AnyQName()) {
return true;
}
const AtomicString& namespace_uri = tag_q_name.NamespaceURI();
return namespace_uri == g_star_atom ||
namespace_uri == element.namespaceURI();
}
// Wildcards are valid subtags in extended language ranges (RFC 4647),
// but not in language tags (RFC 5646).
enum class WildcardSubtags { kAllow, kDisallow };
// Validates a BCP-47 extended language range (RFC 4647) or tag (RFC 5646):
// extended-language-range = (1*8ALPHA / "*") *("-" (1*8alphanum / "*"))
// language-tag = 1*8ALPHA *("-" 1*8alphanum)
static bool IsValidBCP47Value(const String& value,
WildcardSubtags wildcard_policy) {
if (value.empty()) {
return false;
}
const wtf_size_t len = value.length();
wtf_size_t pos = 0;
bool is_first_subtag = true;
while (pos < len) {
// Find the end of the current subtag (next hyphen or end of string).
const wtf_size_t subtag_start = pos;
while (pos < len && value[pos] != '-') {
++pos;
}
const wtf_size_t subtag_len = pos - subtag_start;
// Empty subtag indicates leading, consecutive, or trailing hyphen.
if (subtag_len == 0) {
return false;
}
if (subtag_len == 1 && value[subtag_start] == '*') {
if (wildcard_policy == WildcardSubtags::kDisallow) {
// Wildcard subtags are not allowed inside language tags.
return false;
}
} else {
// Each subtag is limited to 8 characters.
if (subtag_len > 8) {
return false;
}
// First subtag must be alphabetic, subsequent ones can be alphanumeric.
for (wtf_size_t j = subtag_start; j < pos; ++j) {
const bool valid = is_first_subtag ? IsAsciiAlpha(value[j])
: IsAsciiAlphanumeric(value[j]);
if (!valid) {
return false;
}
}
}
is_first_subtag = false;
// Skip the hyphen separator if present.
if (pos < len) {
++pos;
// Trailing hyphen: no more subtags after the hyphen.
if (pos == len) {
return false;
}
}
}
return true;
}
// Matches the element's content language against one or more language ranges,
// both represented in BCP 47 syntax, by following the extended filtering
// algorithm defined in [RFC4647] Matching of Language Tags (section 3.3.2).
// A language range matches a particular language tag if each respective list
// of subtags matches. Comparisons are case-insensitive within the ASCII range.
// See: https://www.rfc-editor.org/rfc/rfc4647#section-3.3.2
static bool MatchesLangPseudoClass(
const AtomicString& language,
const Vector<AtomicString>& language_ranges) {
// Iterator class to traverse subtags within a language tag or range.
class LanguageTagIterator {
STACK_ALLOCATED();
public:
explicit LanguageTagIterator(const AtomicString& language_range)
: language_range_(language_range),
language_range_length_(language_range.length()),
subtag_end_(
std::min(language_range.find('-', 0), language_range.length())) {}
void operator++() {
if (subtag_end_ >= language_range_length_) {
subtag_start_ = language_range_length_;
subtag_end_ = language_range_length_;
return;
}
subtag_start_ = subtag_end_ + 1;
subtag_end_ = std::min(language_range_.find('-', subtag_start_),
language_range_length_);
}
bool AtEnd() const {
return subtag_start_ >= subtag_end_ ||
subtag_start_ >= language_range_length_;
}
StringView CurrentSubtag() const {
return {language_range_, subtag_start_, subtag_end_ - subtag_start_};
}
bool Matches(const LanguageTagIterator& other) const {
return EqualIgnoringAsciiCase(CurrentSubtag(), other.CurrentSubtag());
}
bool MatchesWildcard() const {
StringView subtag = CurrentSubtag();
// SAFETY: empty string short-circuited in &&-expression.
return subtag.length() == 1 && UNSAFE_BUFFERS(subtag[0]) == '*';
}
bool IsSingleton() const {
return (subtag_end_ - subtag_start_) == 1 && subtag_start_ > 0;
}
private:
const AtomicString& language_range_;
wtf_size_t language_range_length_;
wtf_size_t subtag_start_ = 0;
wtf_size_t subtag_end_;
};
for (const AtomicString& range : language_ranges) {
if (language.empty()) {
// Per CSS Selectors 4, :lang("") matches elements with lang="".
if (!language.IsNull() && range.empty()) {
return true;
}
continue;
}
// Malformed language ranges (RFC 4647) never match.
if (!IsValidBCP47Value(range.GetString(), WildcardSubtags::kAllow)) {
continue;
}
LanguageTagIterator range_subtag(range);
LanguageTagIterator language_subtag(language);
if (!range_subtag.Matches(language_subtag) &&
!range_subtag.MatchesWildcard()) {
continue;
}
// Compare the subtags of language and range, taking wildcards into account.
// The match succeeds when all the language range subtags can be matched to
// the language subtags, and fails otherwise.
++range_subtag;
++language_subtag;
while (!range_subtag.AtEnd() && !language_subtag.AtEnd()) {
if (range_subtag.MatchesWildcard()) {
// A wildcard must match at least one subtag, so consume it
++language_subtag;
++range_subtag;
} else if (range_subtag.Matches(language_subtag)) {
++range_subtag;
++language_subtag;
} else if (language_subtag.IsSingleton()) {
// Singleton blocks further matching for this range, try next range.
break;
} else {
++language_subtag;
}
}
if (range_subtag.AtEnd()) {
return true;
}
}
return false;
}
// The associated host, if we are matching in the context of a shadow tree.
//
// https://drafts.csswg.org/css-shadow-1/#in-the-context-of-a-shadow-tree
static Element* ShadowHost(
const SelectorChecker::SelectorCheckingContext& context) {
if (auto* shadow_root = DynamicTo<ShadowRoot>(context.tree_scope)) {
return &shadow_root->host();
}
return nullptr;
}
// Returns true if we're matching in the context of a shadow tree [1],
// and currently pointing at the host associated with that shadow tree.
//
// [1] https://drafts.csswg.org/css-shadow-1/#in-the-context-of-a-shadow-tree
bool IsAtShadowHost(const SelectorChecker::SelectorCheckingContext& context) {
return ShadowHost(context) == context.element;
}
// When matched against the context of a shadow tree [1], the ParentElement
// and PreviousSiblingElement functions return nullptr if they would go outside
// of that tree. (Keeping in mind that the host is effectively the root of that
// tree for selector matching purposes.)
//
// Note that even when we are *not* matching in the context of a shadow tree
// (context.tree_scope=nullptr), context.element may still be an element
// in a shadow tree (specifically, a UA shadow tree). For those cases we must
// not escape the tree, since we have UA rules that rely on this behavior.
// TODO(crbug.com/396459461): Find a better solution for styling UA shadows.
//
// [1] https://drafts.csswg.org/css-shadow-1/#in-the-context-of-a-shadow-tree
static Element* ParentElement(
const SelectorChecker::SelectorCheckingContext& context) {
if (!context.tree_scope) {
return context.element->parentElement();
}
if (IsAtShadowHost(context)) {
return nullptr;
}
return context.element->ParentOrShadowHostElement();
}
static Element* PreviousSiblingElement(
const SelectorChecker::SelectorCheckingContext& context) {
if (IsAtShadowHost(context)) {
return nullptr;
}
return ElementTraversal::PreviousSibling(*context.element);
}
// If context has scope, return slot that matches the scope, otherwise return
// the assigned slot for scope-less matching of ::slotted pseudo-element.
static const HTMLSlotElement* FindSlotElementInScope(
const SelectorChecker::SelectorCheckingContext& context) {
if (!context.tree_scope) {
return context.element->AssignedSlot();
}
for (const HTMLSlotElement* slot = context.element->AssignedSlot(); slot;
slot = slot->AssignedSlot()) {
if (slot->GetTreeScope() == context.tree_scope) {
return slot;
}
}
return nullptr;
}
static bool ShouldMatchHoverOrActive(
const SelectorChecker::SelectorCheckingContext& context) {
// If we're in quirks mode, then :hover and :active should never match anchors
// with no href and *:hover and *:active should not match anything. This is
// specified in https://quirks.spec.whatwg.org/#the-:active-and-:hover-quirk
if (!context.element->GetDocument().InQuirksMode()) {
return true;
}
if (context.is_sub_selector) {
return true;
}
if (context.element->IsLink()) {
return true;
}
const CSSSelector* selector = context.selector;
while (selector->Relation() == CSSSelector::kSubSelector &&
selector->NextSimpleSelector()) {
selector = selector->NextSimpleSelector();
if (selector->Match() != CSSSelector::kPseudoClass) {
return true;
}
if (selector->GetPseudoType() != CSSSelector::kPseudoHover &&
selector->GetPseudoType() != CSSSelector::kPseudoActive) {
return true;
}
}
return false;
}
static bool Impacts(const SelectorChecker::SelectorCheckingContext& context,
SelectorChecker::Impact impact) {
return static_cast<int>(context.impact) & static_cast<int>(impact);
}
static bool ImpactsSubject(
const SelectorChecker::SelectorCheckingContext& context) {
return Impacts(context, SelectorChecker::Impact::kSubject);
}
static bool ImpactsNonSubject(
const SelectorChecker::SelectorCheckingContext& context) {
return Impacts(context, SelectorChecker::Impact::kNonSubject);
}
static bool IsFirstChild(const Element& element) {
return !ElementTraversal::PreviousSibling(element);
}
static bool IsLastChild(const Element& element) {
return !ElementTraversal::NextSibling(element);
}
static bool IsFirstOfType(const Element& element, const QualifiedName& type) {
return !ElementTraversal::PreviousSibling(element, HasTagName(type));
}
static bool IsLastOfType(const Element& element, const QualifiedName& type) {
return !ElementTraversal::NextSibling(element, HasTagName(type));
}
static void DisallowMatchVisited(
SelectorChecker::SelectorCheckingContext& context) {
context.had_match_visited |= context.match_visited;
context.match_visited = false;
}
Element& SelectorChecker::SelectorCheckingContext::GetElementForMatching(
wtf_size_t index) const {
// If we don't match for pseudo-element, just return element.
if (!pseudo_element || index == kNotFound) {
return *element;
}
// If we have exhausted the pseudo-elements, return the last pseudo-element,
// to collect pseudo styles presence or pseudo-class states.
// This check is to prevent situations where selector for nested
// pseudo-elements is deeper than the one requested initially, it would be
// marked as failing in other places, so just checking here.
// E.g. when we match for #div::column, but selector is
// #div::column::scroll-marker::marker, we would fail when going from
// ::scroll-marker to ::marker.
CHECK(index <= pseudo_element_ancestors.size());
index = std::min(index, wtf_size_t(pseudo_element_ancestors.size()) - 1);
return *pseudo_element_ancestors[index];
}
bool SelectorChecker::Match(const SelectorCheckingContext& context,
MatchResult& result) const {
DCHECK(context.selector);
DCHECK(!context.had_match_visited);
#if DCHECK_IS_ON()
DCHECK(!inside_match_) << "Do not re-enter Match: use MatchSelector instead";
base::AutoReset<bool> reset_inside_match(&inside_match_, true);
#endif // DCHECK_IS_ON()
if (context.vtt_originating_element) [[unlikely]] {
// A kUAShadow combinator is required for VTT matching.
if (context.selector->IsLastInComplexSelector()) {
return false;
}
}
return MatchSelector(context, result) == kSelectorMatches;
}
namespace {
bool MatchScrollButton(const Element& element,
const SelectorChecker::SelectorCheckingContext& context,
SelectorChecker::MatchResult& result) {
// For regular element just set a generic scroll button pseudo style flag,
// since we don't know writing mode yet, hence, can not determine the specific
// pseudo style flag.
if (!element.IsPseudoElement()) {
result.dynamic_pseudo = kPseudoIdScrollButton;
return true;
}
if (!element.IsScrollButtonPseudoElement()) {
return false;
}
const ComputedStyle* style = element.ParentComputedStyle();
CHECK(style);
PseudoId pseudo_id =
ScrollButtonPseudoElement::PseudoIdFromScrollButtonArgument(
context.selector->Argument(), *style);
// Check that pseudo ids match when checking for pseudo-element,
// but always match if checking for regular element to set the style
// flag.
return pseudo_id == kPseudoIdScrollButton ||
element.GetPseudoId() == pseudo_id;
}
bool NeedsScopeActivation(
const SelectorChecker::SelectorCheckingContext& context) {
// If we reach the end of the selector without handling context.style_scope,
// it means that we didn't find any selectors with the IsScopeContaining
// flag set, but we still need to ensure that we're in scope.
// This can happen for stylesheets imported using "@import scope(...)".
return context.style_scope && (context.selector->IsScopeContaining() ||
context.selector->IsLastInComplexSelector());
}
ViewTransition* GetTransitionForScope(const Element& element) {
if (element.IsPseudoElement()) {
return nullptr;
}
return ViewTransitionUtils::GetTransition(element);
}
} // namespace
SelectorChecker::FeaturelessMatch
SelectorChecker::MatchesShadowHostInComplexSelector(
const SelectorCheckingContext& context,
MatchResult& result) const {
SelectorCheckingContext sub_context(context);
FeaturelessMatch match = kFeaturelessMatches;
while (sub_context.selector) {
if (sub_context.selector->Relation() != CSSSelector::kSubSelector) {
// We have a combinator left of a :host. Such selectors should evaluate to
// false, even when negated. For instance: :not(#foo > :host) { ... }
return kFeaturelessUnknown;
}
SubResult sub_result(result);
switch (MatchShadowHost(sub_context, sub_result)) {
case kFeaturelessMatches:
break;
case kFeaturelessFails:
// We need to keep matching within the compound for non-matching simple
// selectors since `:not(:not(:host))` should match,
// but `:not(:not(:host)#foo)` shouldn't, and we need to reach #foo to
// know that we need to return kFeaturelessUnknown.
match = kFeaturelessFails;
break;
case kFeaturelessUnknown:
return kFeaturelessUnknown;
}
sub_context.selector = sub_context.selector->NextSimpleSelector();
}
return match;
}
SelectorChecker::FeaturelessMatch SelectorChecker::MatchesShadowHostInList(
const SelectorCheckingContext& context,
const CSSSelector* selector_list,
MatchResult& result) const {
SelectorCheckingContext sub_context(context);
sub_context.is_sub_selector = true;
sub_context.in_nested_complex_selector = true;
sub_context.pseudo_id = kPseudoIdNone;
sub_context.pseudo_element = nullptr;
FeaturelessMatch match = kFeaturelessUnknown;
for (sub_context.selector = selector_list; sub_context.selector;
sub_context.selector = CSSSelectorList::Next(*sub_context.selector)) {
switch (MatchesShadowHostInComplexSelector(sub_context, result)) {
case kFeaturelessMatches:
return kFeaturelessMatches;
case kFeaturelessFails:
match = kFeaturelessFails;
break;
case kFeaturelessUnknown:
break;
}
}
return match;
}
SelectorChecker::FeaturelessMatch SelectorChecker::MatchShadowHost(
const SelectorCheckingContext& context,
MatchResult& result) const {
const CSSSelector& selector = *context.selector;
switch (selector.GetPseudoType()) {
case CSSSelector::kPseudoIs:
case CSSSelector::kPseudoWhere:
case CSSSelector::kPseudoAny:
return MatchesShadowHostInList(context, selector.SelectorListOrParent(),
result);
case CSSSelector::kPseudoNot: {
FeaturelessMatch match = MatchesShadowHostInList(
context, selector.SelectorListOrParent(), result);
switch (match) {
case kFeaturelessMatches:
return kFeaturelessFails;
case kFeaturelessFails:
return kFeaturelessMatches;
case kFeaturelessUnknown:
return kFeaturelessUnknown;
}
}
case CSSSelector::kPseudoParent: {
const CSSSelector* parent = selector.SelectorListOrParent();
if (parent == nullptr) {
// & at top level matches like :scope.
return CheckPseudoScope(context, result) ? kFeaturelessMatches
: kFeaturelessFails;
} else {
return MatchesShadowHostInList(context, parent, result);
}
}
case CSSSelector::kPseudoHostContext:
UseCounter::Count(
context.element->GetDocument(),
mode_ == kQueryingRules
? WebFeature::kCSSSelectorHostContextInSnapshotProfile
: WebFeature::kCSSSelectorHostContextInLiveProfile);
[[fallthrough]];
case CSSSelector::kPseudoHost:
return CheckPseudoHost(context, result) ? kFeaturelessMatches
: kFeaturelessFails;
case CSSSelector::kPseudoScope:
return CheckPseudoScope(context, result) ? kFeaturelessMatches
: kFeaturelessFails;
case CSSSelector::kPseudoHas:
return CheckPseudoHas(context, result) ? kFeaturelessMatches
: kFeaturelessFails;
case CSSSelector::kPseudoAnimatedImage:
case CSSSelector::kPseudoActive:
case CSSSelector::kPseudoActiveOption:
case CSSSelector::kPseudoActiveViewTransition:
case CSSSelector::kPseudoActiveViewTransitionType:
case CSSSelector::kPseudoAfter:
case CSSSelector::kPseudoAnyLink:
case CSSSelector::kPseudoAutofill:
case CSSSelector::kPseudoAutofillPreviewed:
case CSSSelector::kPseudoAutofillSelected:
case CSSSelector::kPseudoBackdrop:
case CSSSelector::kPseudoBefore:
case CSSSelector::kPseudoBuffering:
case CSSSelector::kPseudoCheckMark:
case CSSSelector::kPseudoChecked:
case CSSSelector::kPseudoCornerPresent:
case CSSSelector::kPseudoCurrent:
case CSSSelector::kPseudoDecrement:
case CSSSelector::kPseudoDefault:
case CSSSelector::kPseudoDetailsContent:
case CSSSelector::kPseudoDialogInTopLayer:
case CSSSelector::kPseudoDisabled:
case CSSSelector::kPseudoDoubleButton:
case CSSSelector::kPseudoDrag:
case CSSSelector::kPseudoEmpty:
case CSSSelector::kPseudoEnabled:
case CSSSelector::kPseudoEnd:
case CSSSelector::kPseudoExpandIcon:
case CSSSelector::kPseudoFileSelectorButton:
case CSSSelector::kPseudoFiltered:
case CSSSelector::kPseudoFirstChild:
case CSSSelector::kPseudoFirstLetter:
case CSSSelector::kPseudoFirstLine:
case CSSSelector::kPseudoFirstOfType:
case CSSSelector::kPseudoFirstPage:
case CSSSelector::kPseudoFocus:
case CSSSelector::kPseudoFocusVisible:
case CSSSelector::kPseudoFocusWithin:
case CSSSelector::kPseudoFullPageMedia:
case CSSSelector::kPseudoHasSlotted:
case CSSSelector::kPseudoHorizontal:
case CSSSelector::kPseudoHover:
case CSSSelector::kPseudoIncrement:
case CSSSelector::kPseudoIndeterminate:
case CSSSelector::kPseudoInterestButton:
case CSSSelector::kPseudoInterestSource:
case CSSSelector::kPseudoInterestTarget:
case CSSSelector::kPseudoInvalid:
case CSSSelector::kPseudoLang:
case CSSSelector::kPseudoLastChild:
case CSSSelector::kPseudoLastOfType:
case CSSSelector::kPseudoLeftPage:
case CSSSelector::kPseudoLink:
case CSSSelector::kPseudoMarker:
case CSSSelector::kPseudoModal:
case CSSSelector::kPseudoMuted:
case CSSSelector::kPseudoNoButton:
case CSSSelector::kPseudoNthChild:
case CSSSelector::kPseudoNthLastChild:
case CSSSelector::kPseudoNthLastOfType:
case CSSSelector::kPseudoNthOfType:
case CSSSelector::kPseudoOnlyChild:
case CSSSelector::kPseudoOnlyOfType:
case CSSSelector::kPseudoOptional:
case CSSSelector::kPseudoOverscrollTarget:
case CSSSelector::kPseudoOverscrollOpen:
case CSSSelector::kPseudoPart:
case CSSSelector::kPseudoPermissionGranted:
case CSSSelector::kPseudoPermissionIcon:
case CSSSelector::kPseudoPlaceholder:
case CSSSelector::kPseudoPlaceholderShown:
case CSSSelector::kPseudoReadOnly:
case CSSSelector::kPseudoReadWrite:
case CSSSelector::kPseudoRequired:
case CSSSelector::kPseudoResizer:
case CSSSelector::kPseudoRightPage:
case CSSSelector::kPseudoRoot:
case CSSSelector::kPseudoLinkTo:
case CSSSelector::kPseudoActiveNavigation:
case CSSSelector::kPseudoScrollbar:
case CSSSelector::kPseudoScrollbarButton:
case CSSSelector::kPseudoScrollbarCorner:
case CSSSelector::kPseudoScrollbarThumb:
case CSSSelector::kPseudoScrollbarTrack:
case CSSSelector::kPseudoScrollbarTrackPiece:
case CSSSelector::kPseudoSearchText:
case CSSSelector::kPseudoPickerIcon:
case CSSSelector::kPseudoPicker:
case CSSSelector::kPseudoSelection:
case CSSSelector::kPseudoSingleButton:
case CSSSelector::kPseudoStart:
case CSSSelector::kPseudoState:
case CSSSelector::kPseudoTarget:
case CSSSelector::kPseudoUnknown:
case CSSSelector::kPseudoUnparsed:
case CSSSelector::kPseudoUserInvalid:
case CSSSelector::kPseudoUserValid:
case CSSSelector::kPseudoValid:
case CSSSelector::kPseudoVertical:
case CSSSelector::kPseudoVisited:
case CSSSelector::kPseudoWebKitAutofill:
case CSSSelector::kPseudoWebkitAnyLink:
case CSSSelector::kPseudoWindowInactive:
case CSSSelector::kPseudoFullScreen:
case CSSSelector::kPseudoFullScreenAncestor:
case CSSSelector::kPseudoFullscreen:
case CSSSelector::kPseudoInRange:
case CSSSelector::kPseudoOutOfRange:
case CSSSelector::kPseudoPaused:
case CSSSelector::kPseudoPictureInPicture:
case CSSSelector::kPseudoPlaying:
case CSSSelector::kPseudoXrOverlay:
case CSSSelector::kPseudoWebKitCustomElement:
case CSSSelector::kPseudoBlinkInternalElement:
case CSSSelector::kPseudoColumn:
case CSSSelector::kPseudoCue:
case CSSSelector::kPseudoDefined:
case CSSSelector::kPseudoDir:
case CSSSelector::kPseudoFutureCue:
case CSSSelector::kPseudoGrammarError:
case CSSSelector::kPseudoHasDatalist:
case CSSSelector::kPseudoHasOpenMenuitem:
case CSSSelector::kPseudoHighlight:
case CSSSelector::kPseudoHostHasNonAutoAppearance:
case CSSSelector::kPseudoIsHtml:
case CSSSelector::kPseudoListBox:
case CSSSelector::kPseudoMenulistPopoverWithMenubarAnchor:
case CSSSelector::kPseudoMenulistPopoverWithMenulistAnchor:
case CSSSelector::kPseudoMultiSelectFocus:
case CSSSelector::kPseudoOpen:
case CSSSelector::kPseudoPastCue:
case CSSSelector::kPseudoPopoverInTopLayer:
case CSSSelector::kPseudoPopoverOpen:
case CSSSelector::kPseudoRelativeAnchor:
case CSSSelector::kPseudoSeeking:
case CSSSelector::kPseudoSlotted:
case CSSSelector::kPseudoSpatialNavigationFocus:
case CSSSelector::kPseudoSpellingError:
case CSSSelector::kPseudoStalled:
case CSSSelector::kPseudoTargetText:
case CSSSelector::kPseudoVideoPersistent:
case CSSSelector::kPseudoVideoPersistentAncestor:
case CSSSelector::kPseudoTargetCurrent:
case CSSSelector::kPseudoTargetBefore:
case CSSSelector::kPseudoTargetAfter:
case CSSSelector::kPseudoTextField:
case CSSSelector::kPseudoToolFormActive:
case CSSSelector::kPseudoToolSubmitActive:
case CSSSelector::kPseudoUnboundedElementInactive:
case CSSSelector::kPseudoViewTransition:
case CSSSelector::kPseudoViewTransitionGroup:
case CSSSelector::kPseudoViewTransitionGroupChildren:
case CSSSelector::kPseudoViewTransitionImagePair:
case CSSSelector::kPseudoViewTransitionNew:
case CSSSelector::kPseudoViewTransitionOld:
case CSSSelector::kPseudoVolumeLocked:
case CSSSelector::kPseudoScrollMarker:
case CSSSelector::kPseudoScrollMarkerGroup:
case CSSSelector::kPseudoScrollButton:
case CSSSelector::kPseudoOverscrollAreaParent:
case CSSSelector::kPseudoSelectHasSlottedButton:
// These pseudos are not allowed to match featureless elements. When
// adding new pseudos here, they would typically be allowed if they are
// logical pseudos which take selector arguments.
return kFeaturelessUnknown;
}
}
// Recursive check of selectors and combinators
// It can return 4 different values:
// * SelectorMatches - the selector matches the element e
// * SelectorFailsLocally - the selector fails for the element e
// * SelectorFailsAllSiblings - the selector fails for e and any sibling of e
// * SelectorFailsCompletely - the selector fails for e and any sibling or
// ancestor of e
SelectorChecker::MatchStatus SelectorChecker::MatchSelector(
const SelectorCheckingContext& context,
MatchResult& result) const {
if (NeedsScopeActivation(context)) {
// This function invokes`MatchSelector` again, but with context.scope
// set to the appropriate scoping root.
return MatchForScopeActivation(context, result);
}
SubResult sub_result(result);
bool is_covered_by_bucketing =
context.selector->IsCoveredByBucketing() &&
!context
.is_sub_selector && // Don't trust bucketing in sub-selectors; we
// may be in a child selector (a nested rule).
!context.tree_scope; // May be featureless; see CheckOne().
#if DCHECK_IS_ON()
SubResult dummy_result(result);
if (is_covered_by_bucketing) {
DCHECK(CheckOne(context, dummy_result))
<< context.selector->SimpleSelectorTextForDebug()
<< " unexpectedly didn't match element " << context.element;
DCHECK_EQ(0, dummy_result.flags);
}
#endif
if (!is_covered_by_bucketing && !CheckOne(context, sub_result)) {
return kSelectorFailsLocally;
}
// Doing it manually here instead of destructor as result is later used in
// MatchForSubSelector below.
sub_result.PropagatePseudoAncestorIndex();
if (sub_result.dynamic_pseudo != kPseudoIdNone || context.pseudo_element) {
result.dynamic_pseudo = sub_result.dynamic_pseudo;
result.custom_highlight_name = std::move(sub_result.custom_highlight_name);
}
// If we're done matching the subject, we can determine the state of
// matching against pseudo-elements (if any). (The outer if will also
// trigger for ancestors etc., but neither pseudo_id nor pseudo_element
// will exist for them.)
//
// TODO(sesse): Move pseudo-selectors first, so that we can check this right
// away.
if (context.selector->IsLastInComplexSelector() ||
(context.selector->Relation() != CSSSelector::kSubSelector &&
context.selector->Relation() != CSSSelector::kPseudoChild)) {
// TODO(sesse): Is there a reason why this cannot simply be in
// CheckPseudoElement()?
if (!RuntimeEnabledFeatures::CSSLogicalCombinationPseudoEnabled() &&
context.pseudo_id != kPseudoIdNone &&
context.pseudo_id != result.dynamic_pseudo) {
return kSelectorFailsCompletely;
}
// If matching was for a pseudo-element with a vector of ancestors,
// check that we really reached the end of it. E.g., when matching
// the selector div::column::scroll-marker against a ::column
// pseudo-element, the vector would be just {::column}, and the
// index would be 1 (meaning that the matcher found the ::column,
// but also went further and found the pseudo-element selector
// ::scroll-marker; this is fine, as we'd get dynamic_pseudo).
//
// Likewise, for the selector div::column, the index would be 0
// (meaning that the entire selector matched, and nothing more),
// which is also a match.
//
// But for the opposite, namely the selector div::column against
// the pseudo-element ::column::scroll-marker (with the vector
// {::column, ::scroll-marker}), we'd get index 0, which isn't
// a match.
if (context.pseudo_element &&
(result.pseudo_ancestor_index == kNotFound ||
result.pseudo_ancestor_index <
context.pseudo_element_ancestors.size() - 1)) {
return kSelectorFailsCompletely;
}
}
if (context.selector->IsLastInComplexSelector()) {
return kSelectorMatches;
}
switch (context.selector->Relation()) {
case CSSSelector::kSubSelector:
return MatchForSubSelector(context, result);
default: {
base::AutoReset<PseudoId> dynamic_pseudo_scope(&result.dynamic_pseudo,
kPseudoIdNone);
return MatchForRelation(context, result);
}
}
}
static inline SelectorChecker::SelectorCheckingContext
PrepareNextContextForRelation(
const SelectorChecker::SelectorCheckingContext& context) {
SelectorChecker::SelectorCheckingContext next_context(context);
DCHECK(context.selector->NextSimpleSelector());
next_context.selector = context.selector->NextSimpleSelector();
return next_context;
}
SelectorChecker::MatchStatus SelectorChecker::MatchForSubSelector(
const SelectorCheckingContext& context,
MatchResult& result) const {
SelectorCheckingContext next_context = PrepareNextContextForRelation(context);
// Index can be the size of the vector, which would mean we are
// still at the last element. It's needed to mark that e.g. column pseudo-
// element has ::scroll-marker style in #div::column::scroll-marker selector,
// when matching for column. But we can't go past the size of the vector: E.g.
// #div::column::scroll-marker:focus matching for column pseudo-element should
// fail here, but it won't fail when matching the same selector for scroll
// marker pseudo-element that is generated by column pseudo-element.
if (next_context.pseudo_element &&
result.pseudo_ancestor_index != kNotFound &&
result.pseudo_ancestor_index >
next_context.pseudo_element_ancestors.size()) {
return MatchStatus::kSelectorFailsLocally;
}
PseudoId dynamic_pseudo = result.dynamic_pseudo;
next_context.has_scrollbar_pseudo =
dynamic_pseudo != kPseudoIdNone &&
(scrollbar_ || dynamic_pseudo == kPseudoIdScrollbarCorner ||
dynamic_pseudo == kPseudoIdResizer);
// If we saw a pseudo-element while not computing pseudo-element styles, do
// not try to match any simple selectors after the pseudo-element as those
// selectors need to match the actual pseudo-element.
//
// Examples:
//
// span::selection:window-inactive {}
// #id::before:initial {}
// .class::before:hover {}
//
// In all of those cases we need to skip matching the pseudo-classes after the
// pseudo-element on the originating element.
if (context.in_rightmost_compound && dynamic_pseudo != kPseudoIdNone &&
!context.pseudo_element && context.pseudo_id == kPseudoIdNone) {
// We are in the rightmost compound and have matched a pseudo-element
// (dynamic_pseudo is not kPseudoIdNone), which means we are looking at
// pseudo-classes after the pseudo-element. We are also matching the
// originating element (context.pseudo_id is kPseudoIdnone), which means we
// are matching for tracking the existence of such pseudo-elements which
// results in SetHasPseudoElementStyle() on the originating element's
// ComputedStyle.
if (!next_context.has_scrollbar_pseudo &&
dynamic_pseudo == kPseudoIdScrollbar) {
// Fail ::-webkit-scrollbar:hover because HasPseudoElementStyle for
// scrollbars will remove the native scrollbar. Having only
// ::-webkit-scrollbar rules that have pseudo-class modifiers will end up
// with not adding a custom scrollbar which means we end up with no
// scrollbar.
return kSelectorFailsCompletely;
}
// When matching for e.g. <div> and div::column::scroll-marker, set that
// <div> has ::column style.
if (next_context.selector->Match() == CSSSelector::kPseudoElement) {
return kSelectorMatches;
}
// This means we will end up with false positives for pseudo-elements like
// ::before with only pseudo-class modifiers where we end up trying to
// create the pseudo-element but end up not doing it because we have no
// matching rules without modifiers. That is also already the case if you
// have ::before elements without content properties.
return kSelectorMatches;
}
next_context.previously_matched_pseudo_element = dynamic_pseudo;
next_context.is_sub_selector = true;
return MatchSelector(next_context, result);
}
SelectorChecker::MatchStatus SelectorChecker::MatchForScopeActivation(
const SelectorCheckingContext& context,
MatchResult& result) const {
CHECK(context.style_scope);
SelectorCheckingContext next_context = context;
next_context.is_sub_selector = true;
const StyleScopeActivations& activations =
EnsureActivations(context, *context.style_scope);
if (ImpactsSubject(context)) {
// For e.g. @scope (:hover) { :scope { ...} },
// the StyleScopeActivations may have stored MatchFlags that we
// need to propagate. However, this is only needed if :scope
// appears in the subject position, since MatchFlags are only
// used for subject invalidation. Non-subject flags are set on
// Elements directly (e.g. SetChildrenOrSiblingsAffectedByHover)
result.flags |= activations.match_flags;
}
if (activations.vector.empty()) {
return kSelectorFailsCompletely;
}
// Activations are stored in decreasing order of proxmity (parent
// activations are added first in CalculateActivations, then any activation
// for this element). We want to the most proximate match, hence traverse
// activations in reverse order.
for (const StyleScopeActivation& activation :
base::Reversed(activations.vector)) {
next_context.match_visited = context.match_visited;
next_context.impact = context.impact;
next_context.style_scope = nullptr;
next_context.scope = activation.root;
CHECK(!NeedsScopeActivation(next_context)); // Keeps recursing otherwise.
if (MatchSelector(next_context, result) == kSelectorMatches) {
result.proximity = activation.proximity;
return kSelectorMatches;
}
}
return kSelectorFailsLocally;
}
SelectorChecker::MatchStatus SelectorChecker::MatchForRelation(
const SelectorCheckingContext& context,
MatchResult& result) const {
SelectorCheckingContext next_context = PrepareNextContextForRelation(context);
CSSSelector::RelationType relation = context.selector->Relation();
// Disable :visited matching when we see the first link or try to match
// anything else than an ancestor.
if (context.element->IsLink() || (relation != CSSSelector::kDescendant &&
relation != CSSSelector::kChild)) {
DisallowMatchVisited(next_context);
}
next_context.in_rightmost_compound = false;
next_context.impact = Impact::kNonSubject;
next_context.is_sub_selector = false;
next_context.previous_element = context.element;
next_context.pseudo_id = kPseudoIdNone;
next_context.pseudo_element = nullptr;
switch (relation) {
case CSSSelector::kRelativeDescendant:
DCHECK(result.has_argument_leftmost_compound_matches);
result.has_argument_leftmost_compound_matches->push_back(context.element);
[[fallthrough]];
case CSSSelector::kDescendant:
for (next_context.element = ParentElement(next_context);
next_context.element;
next_context.element = ParentElement(next_context)) {
MatchStatus match = MatchSelector(next_context, result);
if (match == kSelectorMatches || match == kSelectorFailsCompletely) {
return match;
}
if (next_context.element->IsLink()) {
DisallowMatchVisited(next_context);
}
}
return kSelectorFailsCompletely;
case CSSSelector::kRelativeChild:
DCHECK(result.has_argument_leftmost_compound_matches);
result.has_argument_leftmost_compound_matches->push_back(context.element);
[[fallthrough]];
case CSSSelector::kChild: {
next_context.element = ParentElement(next_context);
if (!next_context.element) {
return kSelectorFailsCompletely;
}
MatchStatus match = MatchSelector(next_context, result);
if (match == kSelectorFailsLocally) {
// If we have a selector like .a > .b ~ .c, and .b's parent
// isn't .a, then no other sibling ancestor of .c is going to
// match either (they all have the same parent). If we are
// matching .a > .b in some other context (i.e., not related
// to a sibling combinator), then kSelectorFailsAllSiblings
// and kSelectorFailsLocally are the same and this rewrite
// is harmless.
//
// For kDescendant (e.g., .a .b ~ .c), we have similar logic,
// but there, we are allowed to return kSelectorFailsCompletely,
// which is even stronger. (We cannot do so here, because we
// could be in something like .a > .b .c, where we'd have to
// keep searching for .b up in the tree.)
return kSelectorFailsAllSiblings;
} else {
return match;
}
}
case CSSSelector::kRelativeDirectAdjacent:
DCHECK(result.has_argument_leftmost_compound_matches);
result.has_argument_leftmost_compound_matches->push_back(context.element);
[[fallthrough]];
case CSSSelector::kDirectAdjacent:
if (mode_ == kResolvingStyle) {
if (ContainerNode* parent =
context.element->ParentElementOrShadowRoot()) {
parent->SetChildrenAffectedByDirectAdjacentRules();
}
}
next_context.element = PreviousSiblingElement(context);
if (!next_context.element) {
return kSelectorFailsAllSiblings;
}
return MatchSelector(next_context, result);
case CSSSelector::kRelativeIndirectAdjacent:
DCHECK(result.has_argument_leftmost_compound_matches);
result.has_argument_leftmost_compound_matches->push_back(context.element);
[[fallthrough]];
case CSSSelector::kIndirectAdjacent:
if (mode_ == kResolvingStyle) {
if (ContainerNode* parent =
context.element->ParentElementOrShadowRoot()) {
parent->SetChildrenAffectedByIndirectAdjacentRules();
}
}
next_context.element = PreviousSiblingElement(context);
for (; next_context.element;
next_context.element = PreviousSiblingElement(next_context)) {
MatchStatus match = MatchSelector(next_context, result);
if (match == kSelectorMatches || match == kSelectorFailsAllSiblings ||
match == kSelectorFailsCompletely) {
return match;
}
}
return kSelectorFailsAllSiblings;
case CSSSelector::kPseudoChild: {
// In order to represent a pseudo-element, a context may contain
// pseudo_element!=nullptr, or pseudo_id!=kPseudoIdNone, or both.
if (context.pseudo_id != kPseudoIdNone) {
// This context represents a single "final" would-be pseudo-element
// at the end of the (pseudo-)element chain. Because we only support
// one of these, the parent context is simply the same context with
// `pseudo_id` reset.
next_context.pseudo_id = kPseudoIdNone;
next_context.pseudo_element = context.pseudo_element;
} else {
DCHECK(context.pseudo_element);
// Move to originating element, which may be another pseudo-element.
next_context.pseudo_id = kPseudoIdNone;
Element* originating_element =
To<PseudoElement>(*context.pseudo_element).parentElement();
next_context.pseudo_element =
DynamicTo<PseudoElement>(originating_element);
// If `context.pseudo_element`'s parent was *not* a PseudoElement
// (i.e. we reached the end of the pseudo-element-chain),
// then `next_context.pseudo_element` will be nullptr here.
// That's fine, because `context.element` already contains
// the originating element, and becomes the next element we match
// against.
DCHECK(next_context.pseudo_element ||
next_context.element == originating_element);
}
return MatchSelector(next_context, result);
}
case CSSSelector::kUAShadow: {
// Note: context.tree_scope should be non-null unless we're checking user
// or UA origin rules, or VTT rules. (We could CHECK() this if it
// weren't for the user rules part.)
// If we're in the same tree-scope as the scoping element, then following
// a kUAShadow combinator would escape that and thus the scope.
if (DynamicTo<ShadowRoot>(context.tree_scope) ==
context.element->GetTreeScope()) {
return kSelectorFailsCompletely;
}
Element* shadow_host = context.element->OwnerShadowHost();
if (!shadow_host) {
return kSelectorFailsCompletely;
}
// Match against featureless-like Element described by spec:
// https://w3c.github.io/webvtt/#obtaining-css-boxes
if (context.vtt_originating_element) {
shadow_host = context.vtt_originating_element;
}
next_context.element = shadow_host;
// If this is the *last* time that we cross shadow scopes, then make
// sure that we've crossed *enough* shadow scopes. This prevents
// ::pseudo1 from matching in a scope where it shouldn't match but where
// ::part(p)::pseudo1 or where ::pseudo2::pseudo1 should match.
if (context.tree_scope &&
context.tree_scope != next_context.element->GetTreeScope() &&
!next_context.selector->CrossesTreeScopes()) {
return kSelectorFailsCompletely;
}
return MatchSelector(next_context, result);
}
case CSSSelector::kShadowSlot: {
if (ToHTMLSlotElementIfSupportsAssignmentOrNull(*context.element)) {
return kSelectorFailsCompletely;
}
const HTMLSlotElement* slot = FindSlotElementInScope(context);
if (!slot) {
return kSelectorFailsCompletely;
}
next_context.element = const_cast<HTMLSlotElement*>(slot);
return MatchSelector(next_context, result);
}
case CSSSelector::kShadowPart:
// We ascend through ancestor shadow host elements until we reach the host
// in the TreeScope associated with the style rule. We then match against
// that host.
while (true) {
next_context.element = next_context.element->OwnerShadowHost();
if (!next_context.element) {
return kSelectorFailsCompletely;
}
// Generally a ::part() rule needs to be in the host’s tree scope, but
// if (and only if) we are preceded by :host or :host(), then the rule
// could also be in the same scope as the subject.
//
// We recognize :is(:host) and :where(:host) because the former could
// arise from nesting, but we don't understand the more complex cases
// :is(:host, #foo)::part(x), as we'd need to go down the :is() twice;
// once in the tree scope of the rule itself, and once more in the
// parent scope of the rule but somehow ignoring everything that isn't
// :host.
const TreeScope& host_tree_scope =
next_context.selector->IsDeeplyHostPseudoClass() &&
context.element->GetTreeScope() == context.tree_scope
? *context.tree_scope->ParentTreeScope()
: *context.tree_scope;
if (next_context.element->GetTreeScope() == host_tree_scope) {
return MatchSelector(next_context, result);
}
}
case CSSSelector::kSubSelector:
break;
}
NOTREACHED();
}
static bool AttributeValueMatches(const Attribute& attribute_item,
CSSSelector::MatchType match,
const AtomicString& selector_value,
bool case_insensitive) {
const AtomicString& value = attribute_item.Value();
switch (match) {
case CSSSelector::kAttributeExact:
// Comparing AtomicStrings for equality is very cheap,
// so even for a case-insensitive match, we test that first.
return selector_value == value ||
(case_insensitive &&
EqualIgnoringAsciiCase(selector_value, value));
case CSSSelector::kAttributeSet:
return true;
case CSSSelector::kAttributeList: {
// Ignore empty selectors or selectors containing HTML spaces
if (selector_value.empty() ||
selector_value.Find(&IsHTMLSpace<UChar>) != kNotFound) {
return false;
}
unsigned start_search_at = 0;
while (true) {
wtf_size_t found_pos =
case_insensitive
? value.FindIgnoringAsciiCase(selector_value, start_search_at)
: value.find(selector_value, start_search_at);
if (found_pos == kNotFound) {
return false;
}
if (!found_pos || IsHTMLSpace<UChar>(value[found_pos - 1])) {
unsigned end_str = found_pos + selector_value.length();
if (end_str == value.length() || IsHTMLSpace<UChar>(value[end_str])) {
break; // We found a match.
}
}
// No match. Keep looking.
start_search_at = found_pos + 1;
}
return true;
}
case CSSSelector::kAttributeContain:
if (selector_value.empty()) {
return false;
}
return case_insensitive ? value.ContainsIgnoringAsciiCase(selector_value)
: value.contains(selector_value);
case CSSSelector::kAttributeBegin:
if (selector_value.empty()) {
return false;
}
return case_insensitive
? value.StartsWithIgnoringAsciiCase(selector_value)
: value.starts_with(selector_value);
case CSSSelector::kAttributeEnd:
if (selector_value.empty()) {
return false;
}
return case_insensitive ? value.EndsWithIgnoringAsciiCase(selector_value)
: value.ends_with(selector_value);
case CSSSelector::kAttributeHyphen:
if (value.length() < selector_value.length()) {
return false;
}
if (case_insensitive ? !value.StartsWithIgnoringAsciiCase(selector_value)
: !value.starts_with(selector_value)) {
return false;
}
// It they start the same, check for exact match or following '-':
if (value.length() != selector_value.length() &&
value[selector_value.length()] != '-') {
return false;
}
return true;
default:
NOTREACHED();
}
}
static bool AnyAttributeMatches(Element& element,
CSSSelector::MatchType match,
const CSSSelector& selector) {
const QualifiedName& selector_attr = selector.Attribute();
// Should not be possible from the CSS grammar.
DCHECK_NE(selector_attr.LocalName(), CSSSelector::UniversalSelectorAtom());
// Synchronize the attribute in case it is lazy-computed.
// Currently all lazy properties have a null namespace, so only pass
// localName().
element.SynchronizeAttribute(selector_attr.LocalName());
#if !DCHECK_IS_ON()
// In non-debug builds, we test the Bloom filter here and exit early
// if the attribute could not exist on the element. For non-debug builds,
// we go through the entire normal operation but verify that the Bloom
// filter would not erroneously reject a match.
if (!element.CouldHaveAttribute(selector_attr)) {
return false;
}
#endif
// NOTE: For kAttributeSet, this is a bogus pointer but never used.
const AtomicString& selector_value = selector.Value();
// Legacy dictates that values of some attributes should be compared in
// a case-insensitive manner regardless of whether the case insensitive
// flag is set or not (but an explicit case sensitive flag will override
// that, by causing LegacyCaseInsensitiveMatch() never to be set).
const bool case_insensitive =
selector.AttributeMatch() ==
CSSSelector::AttributeMatchType::kCaseInsensitive ||
(selector.LegacyCaseInsensitiveMatch() &&
IsA<HTMLDocument>(element.GetDocument()));
AttributeCollection attributes = element.AttributesWithoutUpdate();
for (const auto& attribute_item : attributes) {
if (!attribute_item.Matches(selector_attr)) {
if (element.IsHTMLElement() ||
!IsA<HTMLDocument>(element.GetDocument())) {
continue;
}
// Non-html attributes in html documents are normalized to their camel-
// cased version during parsing if applicable. Yet, attribute selectors
// are lower-cased for selectors in html documents. Compare the selector
// and the attribute local name insensitively to e.g. allow matching SVG
// attributes like viewBox.
//
// NOTE: If changing this behavior, be sure to also update the bucketing
// in ElementRuleCollector::CollectMatchingRules() accordingly.
if (!attribute_item.MatchesCaseInsensitive(selector_attr)) {
continue;
}
}
#if DCHECK_IS_ON()
// NOTE: Even if the value doesn't match, we want to check that the
// attribute name was properly found.
DCHECK(element.CouldHaveAttribute(selector_attr))
<< element << " should have contained attribute " << selector_attr
<< ", Bloom bits on element are "
<< element.AttributeOrClassBloomFilter();
#endif
if (AttributeValueMatches(attribute_item, match, selector_value,
case_insensitive)) {
return true;
}
if (selector_attr.NamespaceURI() != g_star_atom) {
return false;
}
}
return false;
}
namespace {
Element& GetCandidateElement(
const SelectorChecker::SelectorCheckingContext& context,
SelectorChecker::MatchResult& result) {
if (RuntimeEnabledFeatures::CSSLogicalCombinationPseudoEnabled()) {
DCHECK_EQ(kPseudoIdNone, context.pseudo_id);
DCHECK(context.element);
return context.pseudo_element ? *context.pseudo_element : *context.element;
}
return context.GetElementForMatching(result.pseudo_ancestor_index);
}
} // namespace
ALWAYS_INLINE bool SelectorChecker::CheckOne(
const SelectorCheckingContext& context,
MatchResult& result) const {
DCHECK(context.element);
Element& element = *context.element;
DCHECK(context.selector);
const CSSSelector& selector = *context.selector;
// When considered within its own shadow trees, the shadow host is
// featureless. Only the :host, :host(), and :host-context() pseudo-classes
// are allowed to match it. [1]
//
// However, the :scope pseudo-class may also match the host if the host is the
// scoping root. [2]
//
// Also, we need to descend into selectors that contain lists instead of
// just returning false, such that :is(:host, .doesnotmatch) (see [3]),
// or similar via nesting, is handled correctly. (This also deals with
// :not().) Having a separate code path for matching featureless elements
// (MatchShadowHost) ensures the featureless matching is done correctly.
//
// [1] https://drafts.csswg.org/css-shadow/#host-element-in-tree
// [2] https://github.com/w3c/csswg-drafts/issues/9025
// [3] https://drafts.csswg.org/selectors-4/#data-model
if (ShadowHost(context) == element &&
selector.Match() != CSSSelector::kPseudoElement) {
if (!selector.IsHostPseudoClass() && !selector.SelectorListOrParent() &&
selector.GetPseudoType() != CSSSelector::kPseudoScope) {
return false;
}
return MatchShadowHost(context, result) == kFeaturelessMatches;
}
if (RuntimeEnabledFeatures::CSSLogicalCombinationPseudoEnabled()) {
if (context.pseudo_id != kPseudoIdNone) {
// This is really a match against a would-be pseudo-element that doesn't
// actually exist as a PseudoElement object.
return CheckVirtualPseudo(context, result);
} else if (context.pseudo_element) {
if (selector.Match() != CSSSelector::kPseudoElement &&
selector.Match() != CSSSelector::kPseudoClass) {
return false;
}
}
}
switch (selector.Match()) {
case CSSSelector::kTag:
return MatchesTagName(element, selector.TagQName());
case CSSSelector::kUniversalTag:
return MatchesUniversalTagName(element, selector.TagQName());
case CSSSelector::kClass:
if (!element.CouldHaveClass(selector.Value())) {
#if DCHECK_IS_ON()
DCHECK(!element.HasClass() ||
!element.ClassNames().Contains(selector.Value()))
<< element << " should have matched class " << selector.Value()
<< ", Bloom bits on element are "
<< element.AttributeOrClassBloomFilter();
#endif
return false;
}
return element.HasClass() &&
element.ClassNames().Contains(selector.Value());
case CSSSelector::kId:
return element.HasID() &&
element.IdForStyleResolution() == selector.Value();
// Attribute selectors
case CSSSelector::kAttributeExact:
case CSSSelector::kAttributeSet:
case CSSSelector::kAttributeHyphen:
case CSSSelector::kAttributeList:
case CSSSelector::kAttributeContain:
case CSSSelector::kAttributeBegin:
case CSSSelector::kAttributeEnd:
return AnyAttributeMatches(element, selector.Match(), selector);
case CSSSelector::kPseudoClass:
return CheckPseudoClass(context, result);
case CSSSelector::kPseudoElement:
return CheckPseudoElement(context, result);
default:
NOTREACHED();
}
}
bool SelectorChecker::CheckPseudoNot(const SelectorCheckingContext& context,
MatchResult& result) const {
return !MatchesAnyInList(context, context.selector->SelectorList()->First(),
result);
}
bool SelectorChecker::MatchesAnyInList(const SelectorCheckingContext& context,
const CSSSelector* selector_list,
MatchResult& result) const {
SelectorCheckingContext sub_context(context);
sub_context.is_sub_selector = true;
sub_context.in_nested_complex_selector = true;
// With CSSLogicalCombinationPseudo enabled, pseudo-element selectors
// within logical combinations are valid, e.g. :is(::before).
// We therefore need keep the pseudo_id around, otherwise CheckVirtualPseudo
// won't know that we're matching for a virtual pseudo within nested lists.
if (!RuntimeEnabledFeatures::CSSLogicalCombinationPseudoEnabled()) {
sub_context.pseudo_id = kPseudoIdNone;
sub_context.pseudo_element = nullptr;
}
for (sub_context.selector = selector_list; sub_context.selector;
sub_context.selector = CSSSelectorList::Next(*sub_context.selector)) {
SubResult sub_result(result);
if (MatchSelector(sub_context, sub_result) == kSelectorMatches) {
return true;
}
}
return false;
}
namespace {
Element* TraverseToParentOrShadowHost(Element* element) {
return element->ParentOrShadowHostElement();
}
Element* TraverseToPreviousSibling(Element* element) {
return ElementTraversal::PreviousSibling(*element);
}
inline bool CacheMatchedElementsAndReturnMatchedResultForIndirectRelation(
Element* has_anchor_element,
HeapVector<Member<Element>>& has_argument_leftmost_compound_matches,
CheckPseudoHasCacheScope::Context& cache_scope_context,
Element* (*next)(Element*)) {
if (cache_scope_context.CacheAllowed()) {
bool selector_matched = false;
for (auto leftmost : has_argument_leftmost_compound_matches) {
for (Element* has_matched_element = next(leftmost); has_matched_element;
has_matched_element = next(has_matched_element)) {
if (has_matched_element == has_anchor_element) {
selector_matched = true;
}
uint8_t old_result =
cache_scope_context.SetMatchedAndGetOldResult(has_matched_element);
if (old_result == kCheckPseudoHasResultNotCached) {
continue;
}
if (old_result & kCheckPseudoHasResultMatched) {
break;
}
}
}
return selector_matched;
}
for (auto leftmost : has_argument_leftmost_compound_matches) {
for (Element* has_matched_element = next(leftmost); has_matched_element;
has_matched_element = next(has_matched_element)) {
if (has_matched_element == has_anchor_element) {
return true;
}
}
}
return false;
}
inline bool CacheMatchedElementsAndReturnMatchedResultForDirectRelation(
Element* has_anchor_element,
HeapVector<Member<Element>>& has_argument_leftmost_compound_matches,
CheckPseudoHasCacheScope::Context& cache_scope_context,
Element* (*next)(Element*)) {
if (cache_scope_context.CacheAllowed()) {
bool selector_matched = false;
for (auto leftmost : has_argument_leftmost_compound_matches) {
if (Element* has_matched_element = next(leftmost)) {
cache_scope_context.SetMatchedAndGetOldResult(has_matched_element);
if (has_matched_element == has_anchor_element) {
selector_matched = true;
}
}
}
return selector_matched;
}
for (auto leftmost : has_argument_leftmost_compound_matches) {
if (Element* has_matched_element = next(leftmost)) {
if (has_matched_element == has_anchor_element) {
return true;
}
}
}
return false;
}
inline bool CacheMatchedElementsAndReturnMatchedResult(
CSSSelector::RelationType leftmost_relation,
Element* has_anchor_element,
HeapVector<Member<Element>>& has_argument_leftmost_compound_matches,
CheckPseudoHasCacheScope::Context& cache_scope_context) {
switch (leftmost_relation) {
case CSSSelector::kRelativeDescendant:
return CacheMatchedElementsAndReturnMatchedResultForIndirectRelation(
has_anchor_element, has_argument_leftmost_compound_matches,
cache_scope_context, TraverseToParentOrShadowHost);
case CSSSelector::kRelativeChild:
return CacheMatchedElementsAndReturnMatchedResultForDirectRelation(
has_anchor_element, has_argument_leftmost_compound_matches,
cache_scope_context, TraverseToParentOrShadowHost);
case CSSSelector::kRelativeDirectAdjacent:
return CacheMatchedElementsAndReturnMatchedResultForDirectRelation(
has_anchor_element, has_argument_leftmost_compound_matches,
cache_scope_context, TraverseToPreviousSibling);
case CSSSelector::kRelativeIndirectAdjacent:
return CacheMatchedElementsAndReturnMatchedResultForIndirectRelation(
has_anchor_element, has_argument_leftmost_compound_matches,
cache_scope_context, TraverseToPreviousSibling);
default:
NOTREACHED();
}
}
inline bool ContextForSubjectHasInMatchesArgument(
const SelectorChecker::SelectorCheckingContext& has_checking_context) {
return has_checking_context.element == has_checking_context.scope &&
has_checking_context.in_rightmost_compound;
}
uint8_t SetHasAnchorElementAsCheckedAndGetOldResult(
const SelectorChecker::SelectorCheckingContext& has_checking_context,
CheckPseudoHasCacheScope::Context& cache_scope_context) {
DCHECK_EQ(has_checking_context.selector->GetPseudoType(),
CSSSelector::kPseudoHas);
Element* has_anchor_element = has_checking_context.element;
uint8_t previous_result = cache_scope_context.GetResult(has_anchor_element);
if (previous_result & kCheckPseudoHasResultChecked) {
return previous_result;
}
// If the selector checking context is for the subject :has() in the argument
// of the JavaScript API 'matches()', skip to check whether the :has() anchor
// element was already checked or not.
if (!ContextForSubjectHasInMatchesArgument(has_checking_context) &&
cache_scope_context.AlreadyChecked(has_anchor_element)) {
// If the element already have cache item, set the element as checked.
// Otherwise, skip to set to prevent increasing unnecessary cache item.
if (previous_result != kCheckPseudoHasResultNotCached) {
cache_scope_context.SetChecked(has_anchor_element);
}
// If the :has() anchor element was already checked previously, return the
// previous result with the kCheckPseudoHasResultChecked flag set.
return previous_result | kCheckPseudoHasResultChecked;
}
cache_scope_context.SetChecked(has_anchor_element);
return previous_result;
}
void SetAffectedByHasFlagsForElementAtDepth(
CheckPseudoHasArgumentContext& argument_context,
Element* element,
int depth) {
if (depth > 0) {
element->SetAncestorsOrAncestorSiblingsAffectedByHas();
} else {
element->SetSiblingsAffectedByHasFlags(
argument_context.GetSiblingsAffectedByHasFlags());
}
}
void SetAffectedByHasFlagsForHasAnchorElement(
CheckPseudoHasArgumentContext& argument_context,
Element* has_anchor_element) {
switch (argument_context.LeftmostRelation()) {
case CSSSelector::kRelativeChild:
case CSSSelector::kRelativeDescendant:
has_anchor_element->SetAncestorsOrAncestorSiblingsAffectedByHas();
break;
case CSSSelector::kRelativeDirectAdjacent:
case CSSSelector::kRelativeIndirectAdjacent:
has_anchor_element->SetSiblingsAffectedByHasFlags(
argument_context.GetSiblingsAffectedByHasFlags());
break;
default:
NOTREACHED();
}
}
void SetAffectedByHasFlagsForHasAnchorSiblings(
CheckPseudoHasArgumentContext& argument_context,
Element* has_anchor_element) {
if (argument_context.AdjacentDistanceLimit() == 0) {
return;
}
int distance = 1;
for (Element* sibling = ElementTraversal::NextSibling(*has_anchor_element);
sibling && distance <= argument_context.AdjacentDistanceLimit();
sibling = ElementTraversal::NextSibling(*sibling), distance++) {
sibling->SetSiblingsAffectedByHasFlags(
argument_context.GetSiblingsAffectedByHasFlags());
}
}
void SetAffectedByHasForArgumentMatchedElement(
CheckPseudoHasArgumentContext& argument_context,
Element* has_anchor_element,
Element* argument_matched_element,
int argument_matched_depth) {
// Iterator class to traverse siblings, ancestors and ancestor siblings of the
// CheckPseudoHasArgumentTraversalIterator's current element until reach to
// the :has() anchor element to set the SiblingsAffectedByHasFlags or
// AncestorsOrAncestorSiblingsAffectedByHas flag.
class AffectedByHasIterator {
STACK_ALLOCATED();
public:
AffectedByHasIterator(CheckPseudoHasArgumentContext& argument_context,
Element* has_anchor_element,
Element* argument_matched_element,
int argument_matched_depth)
: argument_context_(argument_context),
has_anchor_element_(has_anchor_element),
argument_matched_depth_(argument_matched_depth),
current_depth_(argument_matched_depth),
current_element_(argument_matched_element) {
DCHECK_GE(current_depth_, 0);
// affected-by flags of the matched element were already set.
// So, this iterator traverses from the next of the matched element.
++*this;
}
Element* CurrentElement() const { return current_element_; }
bool AtEnd() const {
DCHECK_GE(current_depth_, 0);
return current_element_ == has_anchor_element_;
}
int CurrentDepth() const { return current_depth_; }
void operator++() {
DCHECK(current_element_);
if (current_depth_ == 0) {
current_element_ = ElementTraversal::PreviousSibling(*current_element_);
DCHECK(current_element_);
return;
}
Element* previous = nullptr;
if (NeedsTraverseSiblings() &&
(previous = ElementTraversal::PreviousSibling(*current_element_))) {
current_element_ = previous;
DCHECK(current_element_);
return;
}
DCHECK_GT(current_depth_, 0);
current_depth_--;
current_element_ = current_element_->ParentOrShadowHostElement();
DCHECK(current_element_);
}
private:
inline bool NeedsTraverseSiblings() {
// When the current element is at the same depth of the argument selector
// matched element, we can determine whether the sibling traversal is
// needed or not by checking whether the rightmost combinator is an
// adjacent combinator. When the current element is not at the same depth
// of the argument selector matched element, we can determine whether the
// sibling traversal is needed or not by checking whether an adjacent
// combinator is between child or descendant combinator.
DCHECK_LE(current_depth_, argument_matched_depth_);
return argument_matched_depth_ == current_depth_
? argument_context_.SiblingCombinatorAtRightmost()
: argument_context_
.SiblingCombinatorBetweenChildOrDescendantCombinator();
}
const CheckPseudoHasArgumentContext& argument_context_;
Element* has_anchor_element_;
const int argument_matched_depth_;
int current_depth_;
Element* current_element_;
} affected_by_has_iterator(argument_context, has_anchor_element,
argument_matched_element, argument_matched_depth);
// Set AncestorsOrAncestorSiblingsAffectedByHas flag on the elements at
// upward (previous siblings, ancestors, ancestors' previous siblings) of the
// argument matched element.
for (; !affected_by_has_iterator.AtEnd(); ++affected_by_has_iterator) {
SetAffectedByHasFlagsForElementAtDepth(
argument_context, affected_by_has_iterator.CurrentElement(),
affected_by_has_iterator.CurrentDepth());
}
}
bool SkipCheckingHasArgument(
CheckPseudoHasArgumentContext& context,
CheckPseudoHasArgumentTraversalIterator& iterator) {
// Siblings of the :has() anchor element cannot be a subject of :has()
// argument if the argument selector has child or descendant combinator.
if (context.DepthLimit() > 0 && iterator.CurrentDepth() == 0) {
return true;
}
// The current element of the iterator cannot be a subject of :has() argument
// if the :has() argument selector only matches on the elements at a fixed
// depth and the current element of the iterator is not at the certain depth.
// (e.g. For the style rule '.a:has(> .b > .c) {}', a child of '.a' or a great
// grand child of '.a' cannot be a subject of the argument '> .b > .c'. Only
// the grand child of '.a' can be a subject of the argument)
if (context.DepthFixed() &&
(iterator.CurrentDepth() != context.DepthLimit())) {
return true;
}
return false;
}
void AddElementIdentifierHashesInTraversalScopeAndSetAffectedByHasFlags(
CheckPseudoHasFastRejectFilter& fast_reject_filter,
Element& has_anchor_element,
CheckPseudoHasArgumentContext& argument_context,
bool update_affected_by_has_flags) {
for (CheckPseudoHasArgumentTraversalIterator iterator(has_anchor_element,
argument_context);
!iterator.AtEnd(); ++iterator) {
fast_reject_filter.AddElementIdentifierHashes(*iterator.CurrentElement());
if (update_affected_by_has_flags) {
SetAffectedByHasFlagsForElementAtDepth(
argument_context, iterator.CurrentElement(), iterator.CurrentDepth());
}
}
}
void SetAllElementsInTraversalScopeAsChecked(
Element* has_anchor_element,
CheckPseudoHasArgumentContext& argument_context,
CheckPseudoHasCacheScope::Context& cache_scope_context) {
// Find last element and last depth of the argument traversal iterator.
Element* last_element = has_anchor_element;
int last_depth = 0;
if (argument_context.AdjacentDistanceLimit() > 0) {
last_element = ElementTraversal::NextSibling(*last_element);
}
if (last_element) {
if (argument_context.DepthLimit() > 0) {
last_element = ElementTraversal::FirstChild(*last_element);
last_depth = 1;
}
}
if (!last_element) {
return;
}
cache_scope_context.SetAllTraversedElementsAsChecked(last_element,
last_depth);
}
enum EarlyBreakOnHasArgumentChecking {
kBreakEarlyAndReturnAsMatched,
kBreakEarlyAndMoveToNextArgument,
kNoEarlyBreak,
};
EarlyBreakOnHasArgumentChecking CheckEarlyBreakForHasArgument(
const SelectorChecker::SelectorCheckingContext& context,
Element* has_anchor_element,
CheckPseudoHasArgumentContext& argument_context,
CheckPseudoHasCacheScope::Context& cache_scope_context,
bool& update_affected_by_has_flags) {
if (!cache_scope_context.CacheAllowed()) {
return kNoEarlyBreak;
}
// Get the cached :has() checking result of the element to skip :has()
// argument checking.
// - If the element was already marked as matched, break :has() argument
// checking early and return as matched.
// - If the element was already checked but not matched, break :has()
// argument checking early and move to the next argument selector.
// - Otherwise, check :has() argument.
uint8_t previous_result =
SetHasAnchorElementAsCheckedAndGetOldResult(context, cache_scope_context);
if (previous_result & kCheckPseudoHasResultChecked) {
if (update_affected_by_has_flags) {
SetAffectedByHasFlagsForHasAnchorSiblings(argument_context,
has_anchor_element);
}
return previous_result & kCheckPseudoHasResultMatched
? kBreakEarlyAndReturnAsMatched
: kBreakEarlyAndMoveToNextArgument;
}
// Check fast reject filter to reject :has() argument checking early.
bool is_new_entry;
CheckPseudoHasFastRejectFilter& fast_reject_filter =
cache_scope_context.EnsureFastRejectFilter(has_anchor_element,
is_new_entry);
// Filter is not actually created on the first check to avoid unnecessary
// filter creation overhead. If the :has() anchor element has the
// AffectedByMultipleHas flag set, use fast reject filter even if on the first
// check since there can be more checks on the anchor element.
if (is_new_entry && !has_anchor_element->AffectedByMultipleHas()) {
return kNoEarlyBreak;
}
// The bloom filter in the fast reject filter is allocated and initialized on
// the second check. We can check fast rejection with the filter after the
// allocation and initialization.
if (!fast_reject_filter.BloomFilterAllocated()) {
if (update_affected_by_has_flags) {
// Mark the :has() anchor element as affected by multiple :has() pseudo-
// classes so that we can always use fast reject filter for the anchor
// element.
has_anchor_element->SetAffectedByMultipleHas();
}
fast_reject_filter.AllocateBloomFilter();
AddElementIdentifierHashesInTraversalScopeAndSetAffectedByHasFlags(
fast_reject_filter, *has_anchor_element, argument_context,
update_affected_by_has_flags);
}
// affected-by-has flags were already set while adding element identifier
// hashes (AddElementIdentifierHashesInTraversalScopeAndSetAffectedByHasFlags)
update_affected_by_has_flags = false;
if (fast_reject_filter.FastReject(
argument_context.GetPseudoHasArgumentHashes())) {
SetAllElementsInTraversalScopeAsChecked(
has_anchor_element, argument_context, cache_scope_context);
return kBreakEarlyAndMoveToNextArgument;
}
return kNoEarlyBreak;
}
bool MatchesExternalSVGUseTarget(Element& element) {
if (const auto* svg_element = DynamicTo<SVGElement>(element)) {
if (const SVGElement* corresponding = svg_element->CorrespondingElement()) {
svg_element = corresponding;
}
return svg_element->IsResourceTarget();
}
return false;
}
} // namespace
// Check whether a :has() pseudo matches.
//
// The primary challenge in implementing :has() is performance; if we only
// wanted a correct implementation, we could test every element (I'll call
// these the “candidates”) to the right and below the element in question
// (the “:has() anchor”[1]) and that would be it. However, it would be
// far too slow to be usable in practice, so we need to add two mitigating
// strategies.
//
// The first and simplest one is scoping. The type of the combinators used
// inside :has() will determine the _traversal scope_ of the selector; for
// instance, :has(> .a) can only match elements directly below the anchor, while
// :has(+ .a .b) will match elements below something that is in the subtree
// below the anchor's first sibling. You can think of it as a rough shape
// of the subtree we need to search; we classify each :has() selector into
// one of eleven such shapes (the enum CheckPseudoHasArgumentTraversalScope).
// The traversal scope plus more concrete numerical bounds on how far out
// (in width and depth) we need to search is called the _traversal type_.
// This restricts the amount of candidates we need to search.
//
// The second one is caching. When we start a style recalc, we instantiate
// two caches which are local to that style recalc (so that we do not ever need
// to deal with invalidation), the _result cache_ (CheckPseudoHasResultCache)
// and the _fast-reject cache_ (CheckPseudoHasFastRejectFilter). Each will
// attempt to answer the question “does the given candidate match the given
// selector against our anchor”, so they will be queried repeatedly and can
// be reused across elements. The result cache can answer yes/no/unknown,
// while the fast-reject cache can only answer no/unknown. We will deal with
// the fast-reject cache first, since it is simpler to describe.
//
// The fast-reject cache is a Bloom filter similar in spirit to the normal
// SelectorFilter, but it is not incrementally built and corresponds to
// a single given element. When we decide to build it (typically when we've
// had multiple queries against the same element), we look at every relevant
// candidate element (e.g., the entire subtree under the anchor) and add
// their tag/class/attribute names to the Bloom filter. This allows us to
// quickly answer “could we have any element matching .a”, but only in the
// negative. It is expensive to traverse all candidates just for this,
// so to get any real use of the fast-reject filter, we need to reuse it
// for many different :has() selectors (trivial, as long as they have
// the same traversal type), and ideally also for many different elements.
// The latter is only allowed for certain but rather common traversal
// scopes, such as subtrees; if we have a fast-reject filter for a given
// anchor, we can reuse it when styling its children (remember, the caches
// are persistent for the entire style recalc), although of course with
// increased risk of false positives.
//
// The result cache is simpler in itself, but interacts with more components
// of the selector checker. At its core, it stores “would anchor element E
// match selector S?” (where S is the serialized form of the inside of
// :has(), in order to facilitate more sharing across similar selectors),
// storing both positive and negative results. This cache wouldn't immediately
// seem so useful (why would we ever try to check the same anchor repeatedly
// against the same selector?), but there are two things to keep in mind:
//
// - First, :has() doesn't need to be in the subject. If we have a selector
// like “:has(.a) .b”, then each ancestor could indeed be checked a lot of
// times, and the cache would have a good hit rate without any trickery.
//
// - Second, when inserting positive results into the cache, we get some help
// from the selector checker. When getting a positive match for the inside
// of :has(), It identifies the element(s) that matched _the leftmost
// compound_ of the (sub)selector and return those as a side effect to the
// match result. Depending on the traversal scope, we can then propagate
// the positive match for free to other relevant elements.
//
// E.g., in the simplest possible case, we could have a rule like “:has(.a)”,
// and once we find an .a, we know that not only our current anchor matches
// this rule, but every parent element of the matched .a would also match
// and can be inserted in the cache. Similarly, for a rule like
// “:has(.b ~ .c)”, .b would be our leftmost compound, and upon seeing
// which element matched .b, we could insert every sibling before it
// into the cache. Not all traversal scopes support such propagation,
// but many do.
//
// In order to get the most out of the latter optimization, the traversal
// over candidates happen in _reverse_ DOM tree traversal order; that is,
// the element furthest away from what we would normally expect is processed
// first. (See CheckPseudoHasArgumentTraversalIterator for the implementation.
// It also makes sure we check only candidates relevant for the traversal
// type.) For instance, if we are in “all neighbors” traversal scope,
// this is the rightmost sibling of our anchor. This is not what an author
// would expect, but it maximizes the amounts of extra cache entries
// we can add.
//
// There are, of course, many more details to these caches;
// for instance, see check_pseudo_has_cache_scope.h for more information.
// In particular, the result cache also automatically gets populated with
// _negative_ results as we traverse the tree and don't find what we are
// looking for.
//
//
// [1] This gives rise to the variable name “has_anchor_element”, which sounds
// like it is a boolean for whether we have an anchor element or not.
// But we always do; “has_” comes from “:has()”, and it always stores
// the element we are testing from the selector checker's point of view.
bool SelectorChecker::CheckPseudoHas(const SelectorCheckingContext& context,
MatchResult& result) const {
Element& element = *context.element;
if (mode_ == kResolvingStyle) {
// Set 'AffectedBySubjectHas' or 'AffectedByNonSubjectHas' flag to
// indicate that the element is affected by a subject or non-subject
// :has() state change. It means that, when we have a mutation on
// an element, and the element is in the :has() argument checking scope
// of a :has() anchor element, we may need to invalidate the subject
// element of the style rule containing the :has() pseudo-class because
// the mutation can affect the state of the :has().
if (ImpactsSubject(context)) {
element.SetAffectedBySubjectHas();
}
if (ImpactsNonSubject(context)) {
element.SetAffectedByNonSubjectHas();
}
if (context.selector->ContainsPseudoInsideHasPseudoClass()) {
element.SetAffectedByPseudoInHas();
}
if (context.selector
->ContainsComplexLogicalCombinationsInsideHasPseudoClass()) {
element.SetAffectedByLogicalCombinationsInHas();
}
}
if (element.GetDocument().InPseudoHasChecking()) {
// :has() within :has() would normally be rejected parse-time, but we can
// end up in this situation nevertheless, due to nesting. We just return
// a not-matched for now; it is possible that we should fail the entire rule
// (consider what happens if it is e.g. within :not()), but we would have to
// have some way to propagate that up the stack, and consider interactions
// with the forgiveness of :is().
return false;
}
CheckPseudoHasCacheScope check_pseudo_has_cache_scope(
&element.GetDocument(), /*within_selector_checking=*/true);
Element* has_anchor_element = context.element;
Document& document = has_anchor_element->GetDocument();
DCHECK(document.GetCheckPseudoHasCacheScope());
SelectorCheckingContext sub_context(has_anchor_element);
sub_context.tree_scope = context.tree_scope;
sub_context.scope = context.scope;
// sub_context.match_visited is false (by default) to disable
// :visited matching when it is in the :has argument
sub_context.is_inside_has_pseudo_class = true;
sub_context.pseudo_has_in_rightmost_compound = context.in_rightmost_compound;
bool update_affected_by_has_flags = mode_ == kResolvingStyle;
bool match_in_shadow_tree = context.selector->HasArgumentMatchInShadowTree();
if (match_in_shadow_tree && !has_anchor_element->GetShadowRoot()) {
// Able to reach here when :host is after :has(). (e.g. ':has(div):host')
return false;
}
DCHECK(context.selector->SelectorList());
for (const CSSSelector* selector = context.selector->SelectorList()->First();
selector; selector = CSSSelectorList::Next(*selector)) {
CheckPseudoHasArgumentContext argument_context(selector, context.scope,
match_in_shadow_tree);
// In case of matching a :has() argument on a shadow root subtree, skip
// matching if the argument contains the sibling relationship to the :has()
// anchor element because the shadow root cannot have sibling element.
if (argument_context.TraversalScope() == kInvalidShadowRootTraversalScope) {
continue;
}
CSSSelector::RelationType leftmost_relation =
argument_context.LeftmostRelation();
CheckPseudoHasCacheScope::Context cache_scope_context(&document,
argument_context);
// In case that the :has() pseudo-class checks a relationship to a sibling
// element at fixed distance (e.g. '.a:has(+ .b)') or a sibling subtree at
// fixed distance (e.g. '.a:has(+ .b .c)'), set the parent of the :has()
// anchor element as ChildrenAffectedByDirectAdjacentRules to indicate
// that removing a child from the parent may affect a :has() testing result
// on a child of the parent.
// (e.g. When we have a style rule '.a:has(+ .b) {}' we always need :has()
// invalidation if the preceding element of '.b' is removed)
// Please refer the :has() invalidation for element removal:
// - StyleEngine::ScheduleInvalidationsForHasPseudoAffectedByRemoval()
if (argument_context.AdjacentDistanceLimit() > 0 &&
argument_context.AdjacentDistanceFixed()) {
if (ContainerNode* parent =
has_anchor_element->ParentElementOrShadowRoot()) {
parent->SetChildrenAffectedByDirectAdjacentRules();
}
}
if (update_affected_by_has_flags) {
SetAffectedByHasFlagsForHasAnchorElement(argument_context,
has_anchor_element);
}
EarlyBreakOnHasArgumentChecking early_break = CheckEarlyBreakForHasArgument(
context, has_anchor_element, argument_context, cache_scope_context,
update_affected_by_has_flags);
if (early_break == kBreakEarlyAndReturnAsMatched) {
return true;
} else if (early_break == kBreakEarlyAndMoveToNextArgument) {
continue;
}
sub_context.selector = selector;
sub_context.relative_anchor_element = has_anchor_element;
bool selector_matched = false;
Element* last_argument_checked_element = nullptr;
int last_argument_checked_depth = -1;
for (CheckPseudoHasArgumentTraversalIterator iterator(*has_anchor_element,
argument_context);
!iterator.AtEnd(); ++iterator) {
if (update_affected_by_has_flags) {
SetAffectedByHasFlagsForElementAtDepth(argument_context,
iterator.CurrentElement(),
iterator.CurrentDepth());
}
if (SkipCheckingHasArgument(argument_context, iterator)) {
continue;
}
sub_context.element = iterator.CurrentElement();
HeapVector<Member<Element>> has_argument_leftmost_compound_matches;
SubResult sub_result(result);
sub_result.has_argument_leftmost_compound_matches =
&has_argument_leftmost_compound_matches;
MatchSelector(sub_context, sub_result);
last_argument_checked_element = iterator.CurrentElement();
last_argument_checked_depth = iterator.CurrentDepth();
selector_matched = CacheMatchedElementsAndReturnMatchedResult(
leftmost_relation, has_anchor_element,
has_argument_leftmost_compound_matches, cache_scope_context);
if (selector_matched) {
break;
}
}
if (cache_scope_context.CacheAllowed() && last_argument_checked_element) {
cache_scope_context.SetAllTraversedElementsAsChecked(
last_argument_checked_element, last_argument_checked_depth);
}
if (!selector_matched) {
continue;
}
if (update_affected_by_has_flags) {
SetAffectedByHasForArgumentMatchedElement(
argument_context, has_anchor_element, last_argument_checked_element,
last_argument_checked_depth);
}
return true;
}
return false;
}
bool SelectorChecker::CheckPseudoLinkTo(const SelectorCheckingContext& context,
MatchResult& result) const {
DCHECK(context.selector);
DCHECK(context.selector->GetRouteLocation());
Element& element = GetCandidateElement(context, result);
return context.selector->GetRouteLocation()->CheckSelectorMatch(element);
}
bool SelectorChecker::CheckPseudoActiveNavigation(
const SelectorCheckingContext& context,
MatchResult& result) const {
DCHECK(context.selector);
DCHECK(context.selector->GetActiveNavigationCondition());
Element& element = GetCandidateElement(context, result);
return context.selector->GetActiveNavigationCondition()->CheckSelectorMatch(
element);
}
bool SelectorChecker::CheckPseudoClass(const SelectorCheckingContext& context,
MatchResult& result) const {
Element& element = GetCandidateElement(context, result);
const CSSSelector& selector = *context.selector;
bool force_pseudo_state = false;
if (context.has_scrollbar_pseudo) {
// CSS scrollbars match a specific subset of pseudo-classes, and they have
// specialized rules for each
// (since there are no elements involved).
return CheckScrollbarPseudoClass(context, result);
}
switch (selector.GetPseudoType()) {
case CSSSelector::kPseudoNot:
return CheckPseudoNot(context, result);
case CSSSelector::kPseudoEmpty: {
bool is_empty = true;
bool has_whitespace = false;
for (Node* n = element.firstChild(); n; n = n->nextSibling()) {
if (n->IsElementNode()) {
is_empty = false;
break;
}
if (auto* text_node = DynamicTo<Text>(n)) {
if (!text_node->data().empty()) {
if (text_node->ContainsOnlyWhitespaceOrEmpty()) {
has_whitespace = true;
} else {
is_empty = false;
break;
}
}
}
}
if (is_empty && has_whitespace) {
UseCounter::Count(context.element->GetDocument(),
WebFeature::kCSSSelectorEmptyWhitespaceOnlyFail);
is_empty = false;
}
if (mode_ == kResolvingStyle) {
element.SetStyleAffectedByEmpty();
}
return is_empty;
}
case CSSSelector::kPseudoAnimatedImage: {
CHECK(RuntimeEnabledFeatures::CSSImageAnimationEnabled());
if (auto* image_element = DynamicTo<HTMLImageElement>(element)) {
if (auto* image = image_element->CachedImage()) {
return image->IsAnimatedImage();
}
}
return false;
}
case CSSSelector::kPseudoFirstChild:
if (mode_ == kResolvingStyle) {
if (ContainerNode* parent = element.ParentElementOrDocumentFragment()) {
parent->SetChildrenAffectedByFirstChildRules();
}
element.SetAffectedByFirstChildRules();
}
return IsFirstChild(element);
case CSSSelector::kPseudoFirstOfType:
if (mode_ == kResolvingStyle) {
if (ContainerNode* parent = element.ParentElementOrDocumentFragment()) {
parent->SetChildrenAffectedByForwardPositionalRules();
}
}
return IsFirstOfType(element, element.TagQName());
case CSSSelector::kPseudoLastChild: {
ContainerNode* parent = element.ParentElementOrDocumentFragment();
if (mode_ == kResolvingStyle) {
if (parent) {
parent->SetChildrenAffectedByLastChildRules();
}
element.SetAffectedByLastChildRules();
}
if (mode_ != kQueryingRules && parent &&
!parent->IsFinishedParsingChildren()) {
return false;
}
return IsLastChild(element);
}
case CSSSelector::kPseudoLastOfType: {
ContainerNode* parent = element.ParentElementOrDocumentFragment();
if (mode_ == kResolvingStyle) {
if (parent) {
parent->SetChildrenAffectedByBackwardPositionalRules();
}
}
if (mode_ != kQueryingRules && parent &&
!parent->IsFinishedParsingChildren()) {
return false;
}
return IsLastOfType(element, element.TagQName());
}
case CSSSelector::kPseudoOnlyChild: {
PseudoId pseudo_id_to_check =
element.IsPseudoElement() ? element.GetPseudoId() : context.pseudo_id;
if (IsTransitionPseudoElement(pseudo_id_to_check)) {
ViewTransition* transition =
ViewTransitionUtils::GetTransition(element);
if (!transition) {
return false;
}
DCHECK((transition->Scope() == &element && context.pseudo_id) ||
element.IsPseudoElement());
DCHECK(context.pseudo_argument || element.IsPseudoElement());
const AtomicString& pseudo_argument =
element.IsPseudoElement()
? To<PseudoElement>(element).GetPseudoArgument()
: *context.pseudo_argument;
return transition->MatchForOnlyChild(pseudo_id_to_check,
pseudo_argument);
}
ContainerNode* parent = element.ParentElementOrDocumentFragment();
if (mode_ == kResolvingStyle) {
if (parent) {
parent->SetChildrenAffectedByFirstChildRules();
parent->SetChildrenAffectedByLastChildRules();
}
element.SetAffectedByFirstChildRules();
element.SetAffectedByLastChildRules();
}
if (mode_ != kQueryingRules && parent &&
!parent->IsFinishedParsingChildren()) {
return false;
}
return IsFirstChild(element) && IsLastChild(element);
}
case CSSSelector::kPseudoOnlyOfType: {
// FIXME: This selector is very slow.
ContainerNode* parent = element.ParentElementOrDocumentFragment();
if (mode_ == kResolvingStyle && parent) {
parent->SetChildrenAffectedByForwardPositionalRules();
parent->SetChildrenAffectedByBackwardPositionalRules();
}
if (mode_ != kQueryingRules && parent &&
!parent->IsFinishedParsingChildren()) {
return false;
}
return IsFirstOfType(element, element.TagQName()) &&
IsLastOfType(element, element.TagQName());
}
case CSSSelector::kPseudoPlaceholderShown: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoPlaceholderShown,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
if (auto* text_control = ToTextControlOrNull(element)) {
return text_control->IsPlaceholderVisible();
}
break;
}
case CSSSelector::kPseudoNthChild:
if (mode_ == kResolvingStyle) {
if (ContainerNode* parent = element.ParentElementOrDocumentFragment()) {
parent->SetChildrenAffectedByForwardPositionalRules();
}
}
if (selector.SelectorList()) {
// Check if the element itself matches the “of” selector.
// Note that this will also propagate the correct MatchResult flags,
// so NthIndexCache does not have to do that.
if (!MatchesAnyInList(context, selector.SelectorList()->First(),
result)) {
return false;
}
}
return selector.MatchNth(NthIndexCache::NthChildIndex(
element, selector.SelectorList(), this, &context));
case CSSSelector::kPseudoNthOfType:
if (mode_ == kResolvingStyle) {
if (ContainerNode* parent = element.ParentElementOrDocumentFragment()) {
parent->SetChildrenAffectedByForwardPositionalRules();
}
}
return selector.MatchNth(NthIndexCache::NthOfTypeIndex(element));
case CSSSelector::kPseudoNthLastChild: {
ContainerNode* parent = element.ParentElementOrDocumentFragment();
if (mode_ == kResolvingStyle && parent) {
parent->SetChildrenAffectedByBackwardPositionalRules();
}
if (mode_ != kQueryingRules && parent &&
!parent->IsFinishedParsingChildren()) {
return false;
}
if (selector.SelectorList()) {
// Check if the element itself matches the “of” selector.
if (!MatchesAnyInList(context, selector.SelectorList()->First(),
result)) {
return false;
}
}
return selector.MatchNth(NthIndexCache::NthLastChildIndex(
element, selector.SelectorList(), this, &context));
}
case CSSSelector::kPseudoNthLastOfType: {
ContainerNode* parent = element.ParentElementOrDocumentFragment();
if (mode_ == kResolvingStyle && parent) {
parent->SetChildrenAffectedByBackwardPositionalRules();
}
if (mode_ != kQueryingRules && parent &&
!parent->IsFinishedParsingChildren()) {
return false;
}
return selector.MatchNth(NthIndexCache::NthLastOfTypeIndex(element));
}
case CSSSelector::kPseudoSelectHasSlottedButton:
if (auto* select = DynamicTo<HTMLSelectElement>(element)) {
return select->SlottedButton();
}
return false;
case CSSSelector::kPseudoTarget:
probe::ForcePseudoState(&element, CSSSelector::kPseudoTarget,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
return element == element.GetDocument().CssTarget() ||
MatchesExternalSVGUseTarget(element);
case CSSSelector::kPseudoIs:
case CSSSelector::kPseudoWhere:
case CSSSelector::kPseudoAny:
return MatchesAnyInList(context, selector.SelectorListOrParent(), result);
case CSSSelector::kPseudoParent: {
const CSSSelector* parent = selector.SelectorListOrParent();
if (parent == nullptr) {
// & at top level matches like :scope.
return CheckPseudoScope(context, result);
} else {
return MatchesAnyInList(context, parent, result);
}
}
case CSSSelector::kPseudoAutofill:
case CSSSelector::kPseudoWebKitAutofill:
case CSSSelector::kPseudoAutofillPreviewed:
case CSSSelector::kPseudoAutofillSelected:
return CheckPseudoAutofill(selector.GetPseudoType(), element);
case CSSSelector::kPseudoToolFormActive:
if (auto* form_element = DynamicTo<HTMLFormElement>(element)) {
return form_element->MatchesToolFormActivePseudoClass();
}
return false;
case CSSSelector::kPseudoToolSubmitActive:
if (auto* form_control_element =
DynamicTo<HTMLFormControlElement>(element)) {
return form_control_element->MatchesToolSubmitActivePseudoClass();
}
return false;
case CSSSelector::kPseudoAnyLink:
case CSSSelector::kPseudoWebkitAnyLink:
return element.IsLink();
case CSSSelector::kPseudoLink:
return element.IsLink() && !context.match_visited;
case CSSSelector::kPseudoVisited:
return element.IsLink() && context.match_visited;
case CSSSelector::kPseudoDrag:
if (mode_ == kResolvingStyle) {
if (ImpactsNonSubject(context)) {
element.SetChildrenOrSiblingsAffectedByDrag();
}
}
if (ImpactsSubject(context)) {
result.SetFlag(MatchFlag::kAffectedByDrag);
}
return element.IsDragged();
case CSSSelector::kPseudoFocus:
if (mode_ == kResolvingStyle) {
if (context.is_inside_has_pseudo_class) [[unlikely]] {
element.SetAncestorsOrSiblingsAffectedByFocusInHas();
} else {
if (ImpactsNonSubject(context)) {
element.SetChildrenOrSiblingsAffectedByFocus();
}
}
}
return MatchesFocusPseudoClass(element,
context.previously_matched_pseudo_element);
case CSSSelector::kPseudoFocusVisible:
if (mode_ == kResolvingStyle) {
if (context.is_inside_has_pseudo_class) [[unlikely]] {
element.SetAncestorsOrSiblingsAffectedByFocusVisibleInHas();
} else {
if (ImpactsNonSubject(context)) {
element.SetChildrenOrSiblingsAffectedByFocusVisible();
}
}
}
return MatchesFocusVisiblePseudoClass(element);
case CSSSelector::kPseudoFocusWithin:
if (mode_ == kResolvingStyle) {
if (context.is_inside_has_pseudo_class) [[unlikely]] {
element.SetAncestorsOrSiblingsAffectedByFocusInHas();
} else if (ImpactsNonSubject(context)) {
element.SetChildrenOrSiblingsAffectedByFocusWithin();
}
}
if (ImpactsSubject(context)) {
result.SetFlag(MatchFlag::kAffectedByFocusWithin);
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoFocusWithin,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
return element.HasFocusWithin();
case CSSSelector::kPseudoActiveOption:
if (auto* option = DynamicTo<HTMLOptionElement>(element)) {
if (RuntimeEnabledFeatures::CustomizableComboboxEnabled()) {
// This will only match for a base appearance combobox because
// HTMLDataListElement::ActiveOption will only return an option if the
// datalist is being rendered with base appearance.
if (HTMLDataListElement* datalist = option->OwnerDataListElement()) {
return datalist->ActiveOption() == option;
}
}
if (RuntimeEnabledFeatures::FilterableSelectEnabled()) {
if (HTMLSelectElement* select = option->OwnerSelectElement()) {
return option == select->ActiveOption();
}
}
}
return false;
case CSSSelector::kPseudoFiltered:
CHECK(RuntimeEnabledFeatures::CustomizableComboboxEnabled() ||
RuntimeEnabledFeatures::FilterableSelectEnabled());
if (auto* option = DynamicTo<HTMLOptionElement>(element)) {
return option->IsFiltered();
}
return false;
case CSSSelector::kPseudoInterestSource:
return element.GetInterestState() != Element::InterestState::kNoInterest;
case CSSSelector::kPseudoInterestTarget: {
Element* invoker = element.SourceInterestInvoker();
DCHECK(!invoker || invoker->GetInterestState() !=
Element::InterestState::kNoInterest);
return invoker;
}
case CSSSelector::kPseudoHasSlotted:
DCHECK(RuntimeEnabledFeatures::CSSPseudoHasSlottedEnabled());
if (auto* slot = DynamicTo<HTMLSlotElement>(element)) {
return slot->HasFlattenedAssignedNodesNoRecalc();
}
return false;
case CSSSelector::kPseudoHover:
if (mode_ == kResolvingStyle) {
if (context.is_inside_has_pseudo_class) [[unlikely]] {
element.SetAncestorsOrSiblingsAffectedByHoverInHas();
} else if (ImpactsNonSubject(context)) {
element.SetChildrenOrSiblingsAffectedByHover();
}
}
if (ImpactsSubject(context)) {
result.SetFlag(MatchFlag::kAffectedByHover);
}
if (!ShouldMatchHoverOrActive(context)) {
return false;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoHover,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
return element.IsHovered();
case CSSSelector::kPseudoActive:
if (mode_ == kResolvingStyle) {
if (context.is_inside_has_pseudo_class) [[unlikely]] {
element.SetAncestorsOrSiblingsAffectedByActiveInHas();
} else if (ImpactsNonSubject(context)) {
element.SetChildrenOrSiblingsAffectedByActive();
}
}
if (ImpactsSubject(context)) {
result.SetFlag(MatchFlag::kAffectedByActive);
}
if (!ShouldMatchHoverOrActive(context)) {
return false;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoActive,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
return element.IsActive();
case CSSSelector::kPseudoEnabled: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoEnabled,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoDisabled,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
return element.MatchesEnabledPseudoClass();
}
case CSSSelector::kPseudoFullPageMedia:
return element.GetDocument().IsMediaDocument();
case CSSSelector::kPseudoDefault:
return element.MatchesDefaultPseudoClass();
case CSSSelector::kPseudoDisabled:
probe::ForcePseudoState(&element, CSSSelector::kPseudoDisabled,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoEnabled,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
return element.MatchesDisabledPseudoClass();
case CSSSelector::kPseudoReadOnly: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoReadOnly,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoReadWrite,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
return element.MatchesReadOnlyPseudoClass();
}
case CSSSelector::kPseudoReadWrite: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoReadWrite,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoReadOnly,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
return element.MatchesReadWritePseudoClass();
}
case CSSSelector::kPseudoOptional: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoOptional,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoRequired,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
return element.IsOptionalFormControl();
}
case CSSSelector::kPseudoRequired: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoRequired,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoOptional,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
return element.IsRequiredFormControl();
}
case CSSSelector::kPseudoUserInvalid: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoUserInvalid,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoUserValid,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
if (auto* form_control =
DynamicTo<HTMLFormControlElementWithState>(element)) {
return form_control->MatchesUserInvalidPseudo();
}
return false;
}
case CSSSelector::kPseudoUserValid: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoUserValid,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoUserInvalid,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
if (auto* form_control =
DynamicTo<HTMLFormControlElementWithState>(element)) {
return form_control->MatchesUserValidPseudo();
}
return false;
}
case CSSSelector::kPseudoValid:
probe::ForcePseudoState(&element, CSSSelector::kPseudoValid,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoInvalid,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
return element.MatchesValidityPseudoClasses() && element.IsValidElement();
case CSSSelector::kPseudoInvalid: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoInvalid,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoValid,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
return element.MatchesValidityPseudoClasses() &&
!element.IsValidElement();
}
case CSSSelector::kPseudoChecked: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoChecked,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
if (auto* input_element = DynamicTo<HTMLInputElement>(element)) {
// Even though WinIE allows checked and indeterminate to
// co-exist, the CSS selector spec says that you can't be
// both checked and indeterminate. We will behave like WinIE
// behind the scenes and just obey the CSS spec here in the
// test for matching the pseudo.
if (input_element->ShouldAppearChecked() &&
!input_element->ShouldAppearIndeterminate()) {
return true;
}
} else if (auto* option_element = DynamicTo<HTMLOptionElement>(element)) {
if (option_element->Selected()) {
return true;
}
} else if (auto* menu_item_element =
DynamicTo<HTMLMenuItemElement>(element)) {
return menu_item_element->ShouldAppearChecked();
}
break;
}
case CSSSelector::kPseudoTargetCurrent: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoTargetCurrent,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
if (element.IsScrollMarkerPseudoElement()) {
return To<ScrollMarkerPseudoElement>(element).IsSelected();
}
if (auto* anchor_element = DynamicTo<HTMLAnchorElement>(element)) {
if (ScrollMarkerGroupData* data =
anchor_element->GetScrollTargetGroupContainerData()) {
return data->Selected() == element;
}
}
break;
}
case CSSSelector::kPseudoTargetBefore:
case CSSSelector::kPseudoTargetAfter: {
Element* scroll_marker = nullptr;
Element* active_scroll_marker = nullptr;
// ::scroll-marker pseudo element case.
if (auto* pseudo_scroll_marker =
DynamicTo<ScrollMarkerPseudoElement>(element)) {
if (auto* scroll_marker_group =
pseudo_scroll_marker->ScrollMarkerGroup()) {
scroll_marker = pseudo_scroll_marker;
active_scroll_marker = scroll_marker_group->Selected();
}
}
// html anchor scroll marker case.
if (auto* anchor_element = DynamicTo<HTMLAnchorElement>(element)) {
if (ScrollMarkerGroupData* data =
anchor_element->GetScrollTargetGroupContainerData()) {
scroll_marker = anchor_element;
active_scroll_marker = data->Selected();
}
}
// Compare the layout tree position of the scroll marker and the
// active scroll marker to determine before/after relationship.
if (scroll_marker && active_scroll_marker) {
int order_result =
LayoutTreeBuilderTraversal::ComparePreorderTreePosition(
*scroll_marker, *active_scroll_marker);
return selector.GetPseudoType() == CSSSelector::kPseudoTargetBefore
? order_result == -1
: order_result == 1;
}
break;
}
case CSSSelector::kPseudoTextField: {
if (auto* input = DynamicTo<HTMLInputElement>(element)) {
return input->IsTextField();
}
return false;
}
case CSSSelector::kPseudoIndeterminate: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoIndeterminate,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
return element.ShouldAppearIndeterminate();
}
case CSSSelector::kPseudoRoot:
return element == element.GetDocument().documentElement();
case CSSSelector::kPseudoLinkTo:
DCHECK(RuntimeEnabledFeatures::RouteMatchingEnabled());
return CheckPseudoLinkTo(context, result);
case CSSSelector::kPseudoActiveNavigation:
DCHECK(RuntimeEnabledFeatures::RouteMatchingEnabled());
return CheckPseudoActiveNavigation(context, result);
case CSSSelector::kPseudoLang: {
auto* vtt_element = DynamicTo<VTTElement>(element);
AtomicString value = vtt_element ? vtt_element->Language()
: element.ComputeInheritedLanguage();
// Malformed language tag values (RFC 5646) never match.
if (!value.empty() &&
!IsValidBCP47Value(value.GetString(), WildcardSubtags::kDisallow)) {
break;
}
if (!RuntimeEnabledFeatures::CSSLangExtendedRangesEnabled()) {
DCHECK_EQ(selector.ArgumentList()->size(), 1u);
const AtomicString& argument = (*selector.ArgumentList())[0];
if (value.empty() || !value.StartsWithIgnoringAsciiCase(argument)) {
break;
}
if (value.length() != argument.length() &&
value[argument.length()] != '-') {
break;
}
return true;
}
return MatchesLangPseudoClass(value, *selector.ArgumentList());
}
case CSSSelector::kPseudoDir: {
const AtomicString& argument = selector.Argument();
if (argument.empty()) {
break;
}
TextDirection direction;
if (EqualIgnoringAsciiCase(argument, "ltr")) {
direction = TextDirection::kLtr;
} else if (EqualIgnoringAsciiCase(argument, "rtl")) {
direction = TextDirection::kRtl;
} else {
break;
}
// Recomputing the slot assignment can update cached directionality. In
// most cases it's OK for this code to be run when slot assignments are
// dirty; however for API calls like Element.matches() we should recalc
// them now.
Document& document = element.GetDocument();
if (mode_ == kQueryingRules && document.IsSlotAssignmentDirty()) {
document.GetSlotAssignmentEngine().RecalcSlotAssignments();
}
return element.CachedDirectionality() == direction;
}
case CSSSelector::kPseudoDialogInTopLayer:
return element.IsDialogInTopLayer();
case CSSSelector::kPseudoPopoverInTopLayer:
return element.IsPopoverInTopLayer();
case CSSSelector::kPseudoPopoverOpen:
if (auto* html_element = DynamicTo<HTMLElement>(element);
html_element && html_element->IsPopover()) {
return html_element->popoverOpen();
}
return false;
case CSSSelector::kPseudoOverscrollOpen:
return element.MatchesOverscrollOpen();
case CSSSelector::kPseudoOpen:
probe::ForcePseudoState(&element, CSSSelector::kPseudoOpen,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
if (auto* dialog = DynamicTo<HTMLDialogElement>(element)) {
return dialog->FastHasAttribute(html_names::kOpenAttr);
} else if (auto* details = DynamicTo<HTMLDetailsElement>(element)) {
return details->FastHasAttribute(html_names::kOpenAttr);
} else if (auto* select = DynamicTo<HTMLSelectElement>(element)) {
return select->PopupIsVisible();
} else if (auto* input = DynamicTo<HTMLInputElement>(element)) {
return input->IsPickerVisible();
} else if (auto* menuitem = DynamicTo<HTMLMenuItemElement>(element)) {
return menuitem->IsSubmenuOpen();
}
return false;
case CSSSelector::kPseudoMenulistPopoverWithMenubarAnchor:
if (auto* menulist = DynamicTo<HTMLMenuListElement>(element)) {
if (auto* menuitem_anchor = DynamicTo<HTMLMenuItemElement>(
menulist->GetPopoverData()->invoker())) {
return IsA<HTMLMenuBarElement>(menuitem_anchor->OwningMenuElement());
}
}
return false;
case CSSSelector::kPseudoMenulistPopoverWithMenulistAnchor:
if (auto* menulist = DynamicTo<HTMLMenuListElement>(element)) {
if (auto* menuitem_anchor = DynamicTo<HTMLMenuItemElement>(
menulist->GetPopoverData()->invoker())) {
return IsA<HTMLMenuListElement>(menuitem_anchor->OwningMenuElement());
}
}
return false;
case CSSSelector::kPseudoFullscreen:
// fall through
case CSSSelector::kPseudoFullScreen:
return Fullscreen::IsFullscreenFlagSetFor(element);
case CSSSelector::kPseudoFullScreenAncestor:
return element.ContainsFullScreenElement();
case CSSSelector::kPseudoPermissionGranted: {
CHECK(RuntimeEnabledFeatures::GeolocationElementEnabled(
element.GetExecutionContext()) ||
RuntimeEnabledFeatures::UserMediaElementEnabled(
element.GetExecutionContext()) ||
RuntimeEnabledFeatures::InstallElementEnabled(
element.GetExecutionContext()));
auto* permission_element = DynamicTo<HTMLCapabilityElementBase>(element);
return permission_element && permission_element->granted();
}
case CSSSelector::kPseudoPictureInPicture:
return PictureInPictureController::IsElementInPictureInPicture(&element);
case CSSSelector::kPseudoPlaying: {
DCHECK(RuntimeEnabledFeatures::CSSMediaElementPseudosEnabled());
auto* media_element = DynamicTo<HTMLMediaElement>(element);
return media_element && !media_element->paused();
}
case CSSSelector::kPseudoPaused: {
DCHECK(RuntimeEnabledFeatures::CSSMediaElementPseudosEnabled());
auto* media_element = DynamicTo<HTMLMediaElement>(element);
return media_element && media_element->paused();
}
case CSSSelector::kPseudoSeeking: {
DCHECK(RuntimeEnabledFeatures::CSSMediaElementPseudosEnabled());
auto* media_element = DynamicTo<HTMLMediaElement>(element);
return media_element && media_element->seeking();
}
case CSSSelector::kPseudoBuffering: {
DCHECK(RuntimeEnabledFeatures::CSSMediaElementPseudosEnabled());
auto* media_element = DynamicTo<HTMLMediaElement>(element);
return media_element && media_element->MatchesBufferingPseudo();
}
case CSSSelector::kPseudoStalled: {
DCHECK(RuntimeEnabledFeatures::CSSMediaElementPseudosEnabled());
auto* media_element = DynamicTo<HTMLMediaElement>(element);
return media_element && media_element->MatchesStalledPseudo();
}
case CSSSelector::kPseudoMuted: {
DCHECK(RuntimeEnabledFeatures::CSSMediaElementPseudosEnabled());
auto* media_element = DynamicTo<HTMLMediaElement>(element);
return media_element && media_element->muted();
}
case CSSSelector::kPseudoVolumeLocked:
DCHECK(RuntimeEnabledFeatures::CSSMediaElementPseudosEnabled());
// :volume-locked never matches, but is supported so that it's possible to
// write cross-browser styles without guarding use of :volume-locked with
// @supports selector(:volume-locked) or other feature detection.
return false;
case CSSSelector::kPseudoVideoPersistent: {
DCHECK(is_ua_rule_);
auto* video_element = DynamicTo<HTMLVideoElement>(element);
return video_element && video_element->IsPersistent();
}
case CSSSelector::kPseudoVideoPersistentAncestor:
DCHECK(is_ua_rule_);
return element.ContainsPersistentVideo();
case CSSSelector::kPseudoXrOverlay:
// In immersive AR overlay mode, apply a pseudostyle to the DOM Overlay
// element. This is the same as the fullscreen element in the current
// implementation, but could be different for AR headsets.
return element.GetDocument().IsXrOverlay() &&
Fullscreen::IsFullscreenElement(element);
case CSSSelector::kPseudoInRange: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoInRange,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoOutOfRange,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
return element.IsInRange();
}
case CSSSelector::kPseudoOutOfRange: {
probe::ForcePseudoState(&element, CSSSelector::kPseudoOutOfRange,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoInRange,
&force_pseudo_state);
if (force_pseudo_state) {
return false;
}
return element.IsOutOfRange();
}
case CSSSelector::kPseudoFutureCue: {
auto* vtt_element = DynamicTo<VTTElement>(element);
return vtt_element && !vtt_element->IsPastNode();
}
case CSSSelector::kPseudoPastCue: {
auto* vtt_element = DynamicTo<VTTElement>(element);
return vtt_element && vtt_element->IsPastNode();
}
case CSSSelector::kPseudoScope:
return CheckPseudoScope(context, result);
case CSSSelector::kPseudoDefined:
return element.IsDefined();
case CSSSelector::kPseudoHostContext:
UseCounter::Count(
context.element->GetDocument(),
mode_ == kQueryingRules
? WebFeature::kCSSSelectorHostContextInSnapshotProfile
: WebFeature::kCSSSelectorHostContextInLiveProfile);
[[fallthrough]];
case CSSSelector::kPseudoHost:
DCHECK(!IsAtShadowHost(context));
return false;
case CSSSelector::kPseudoSpatialNavigationFocus:
DCHECK(is_ua_rule_);
return MatchesSpatialNavigationFocusPseudoClass(element);
case CSSSelector::kPseudoUnboundedElementInactive: {
DCHECK(is_ua_rule_);
DCHECK(RuntimeEnabledFeatures::UnboundedElementEnabled());
auto* html_element = DynamicTo<HTMLElement>(element);
return html_element &&
html_element->FastHasAttribute(html_names::kUnboundedAttr) &&
!html_element->IsUnboundedElementActive();
}
case CSSSelector::kPseudoHasDatalist:
DCHECK(is_ua_rule_);
return MatchesHasDatalistPseudoClass(element);
case CSSSelector::kPseudoHasOpenMenuitem:
DCHECK(is_ua_rule_);
return MatchesHasOpenMenuitemPseudoClass(element);
case CSSSelector::kPseudoIsHtml:
DCHECK(is_ua_rule_);
return IsA<HTMLDocument>(element.GetDocument());
case CSSSelector::kPseudoListBox:
DCHECK(is_ua_rule_);
return MatchesListBoxPseudoClass(element);
case CSSSelector::kPseudoMultiSelectFocus:
DCHECK(is_ua_rule_);
return MatchesMultiSelectFocusPseudoClass(element);
case CSSSelector::kPseudoHostHasNonAutoAppearance:
DCHECK(is_ua_rule_);
if (ShadowRoot* root = element.ContainingShadowRoot()) {
if (!root->IsUserAgent()) {
return false;
}
const ComputedStyle* style = root->host().GetComputedStyle();
return style && style->HasEffectiveAppearance();
}
return false;
case CSSSelector::kPseudoWindowInactive:
if (context.previously_matched_pseudo_element != kPseudoIdSelection) {
return false;
}
return !element.GetDocument().GetPage()->GetFocusController().IsActive();
case CSSSelector::kPseudoState: {
return element.DidAttachInternals() &&
element.EnsureElementInternals().HasState(selector.Argument());
}
case CSSSelector::kPseudoHorizontal:
case CSSSelector::kPseudoVertical:
case CSSSelector::kPseudoDecrement:
case CSSSelector::kPseudoIncrement:
case CSSSelector::kPseudoStart:
case CSSSelector::kPseudoEnd:
case CSSSelector::kPseudoDoubleButton:
case CSSSelector::kPseudoSingleButton:
case CSSSelector::kPseudoNoButton:
case CSSSelector::kPseudoCornerPresent:
return false;
case CSSSelector::kPseudoModal:
if (Fullscreen::IsFullscreenElement(element)) {
return true;
}
if (const auto* dialog_element = DynamicTo<HTMLDialogElement>(element)) {
return dialog_element->IsModal();
}
return false;
case CSSSelector::kPseudoHas:
return CheckPseudoHas(context, result);
case CSSSelector::kPseudoRelativeAnchor:
DCHECK(context.relative_anchor_element);
return context.relative_anchor_element == &element;
case CSSSelector::kPseudoActiveViewTransition: {
// The pseudo is only valid if there is a transition.
auto* transition = GetTransitionForScope(element);
if (!transition) {
return false;
}
// Ask the transition to match for active-view-transition.
return transition->MatchForActiveViewTransition();
}
case CSSSelector::kPseudoActiveViewTransitionType: {
// The pseudo is only valid if there is a transition.
auto* transition = GetTransitionForScope(element);
if (!transition) {
return false;
}
// Ask the transition to match based on the argument list.
return transition->MatchForActiveViewTransitionType(selector.IdentList());
}
case CSSSelector::kPseudoUnparsed:
// Only kept around for parsing; can never match anything
// (because we don't know what it's supposed to mean).
return false;
case CSSSelector::kPseudoCurrent:
if (context.previously_matched_pseudo_element != kPseudoIdSearchText) {
return false;
}
return context.search_text_request_is_current;
case CSSSelector::kPseudoOverscrollTarget:
return SelectorChecker::MatchesOverscrollTarget(element);
case CSSSelector::kPseudoUnknown:
default:
NOTREACHED();
}
return false;
}
static bool MatchesUAShadowElement(Element& element, const AtomicString& id) {
Element& originating_element =
element.IsPseudoElement()
? To<PseudoElement>(element).UltimateOriginatingElement()
: element;
ShadowRoot* root = originating_element.ContainingShadowRoot();
return root && root->IsUserAgent() &&
originating_element.ShadowPseudoId() == id;
}
bool SelectorChecker::CheckPseudoAutofill(CSSSelector::PseudoType pseudo_type,
Element& element) const {
bool force_pseudo_state = false;
probe::ForcePseudoState(&element, CSSSelector::kPseudoAutofill,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
HTMLFormControlElement* form_control_element =
DynamicTo<HTMLFormControlElement>(&element);
if (!form_control_element) {
return false;
}
switch (pseudo_type) {
case CSSSelector::kPseudoAutofill:
case CSSSelector::kPseudoWebKitAutofill:
return form_control_element->IsAutofilled() ||
form_control_element->IsPreviewed();
case CSSSelector::kPseudoAutofillPreviewed:
return form_control_element->GetAutofillState() ==
WebAutofillState::kPreviewed;
case CSSSelector::kPseudoAutofillSelected:
return form_control_element->IsAutofilled();
default:
NOTREACHED();
}
}
bool SelectorChecker::CheckPseudoElement(const SelectorCheckingContext& context,
MatchResult& result) const {
const CSSSelector& selector = *context.selector;
PseudoId pseudo_id = selector.GetPseudoId(selector.GetPseudoType());
// Only descend down the ancestors chain if matching a (real) PseudoElement.
if (pseudo_id != kPseudoIdNone && pseudo_id <= kLastPublicPseudoId) {
result.DescendToNextPseudoElement();
}
Element& element = GetCandidateElement(context, result);
if (!RuntimeEnabledFeatures::CSSLogicalCombinationPseudoEnabled() &&
context.in_nested_complex_selector) {
// This would normally be rejected parse-time, but can happen
// with the & selector, so reject it match-time.
// See https://github.com/w3c/csswg-drafts/issues/7912.
return false;
}
switch (selector.GetPseudoType()) {
case CSSSelector::kPseudoCue: {
SelectorCheckingContext sub_context(context);
sub_context.is_sub_selector = true;
sub_context.scope = nullptr;
sub_context.tree_scope = nullptr;
for (sub_context.selector = selector.SelectorList()->First();
sub_context.selector; sub_context.selector = CSSSelectorList::Next(
*sub_context.selector)) {
SubResult sub_result(result);
if (MatchSelector(sub_context, sub_result) == kSelectorMatches) {
return true;
}
}
return false;
}
case CSSSelector::kPseudoPart:
if (!part_names_) {
return false;
}
for (const auto& part_name : selector.IdentList()) {
if (!part_names_->Contains(part_name)) {
return false;
}
}
return true;
case CSSSelector::kPseudoFileSelectorButton:
return MatchesUAShadowElement(
element, shadow_element_names::kPseudoFileUploadButton);
case CSSSelector::kPseudoPicker:
if (selector.Argument() == "select") {
return MatchesUAShadowElement(element,
shadow_element_names::kPickerSelect);
} else {
return false;
}
case CSSSelector::kPseudoPlaceholder:
return MatchesUAShadowElement(
element, shadow_element_names::kPseudoInputPlaceholder);
case CSSSelector::kPseudoDetailsContent:
return MatchesUAShadowElement(element,
shadow_element_names::kIdDetailsContent);
case CSSSelector::kPseudoPermissionIcon:
return MatchesUAShadowElement(element,
shadow_element_names::kIdPermissionIcon);
case CSSSelector::kPseudoWebKitCustomElement:
return MatchesUAShadowElement(element, selector.Value());
case CSSSelector::kPseudoBlinkInternalElement:
DCHECK(is_ua_rule_);
return MatchesUAShadowElement(element, selector.Value());
case CSSSelector::kPseudoSlotted: {
SelectorCheckingContext sub_context(context);
sub_context.is_sub_selector = true;
sub_context.scope = nullptr;
sub_context.tree_scope = nullptr;
sub_context.pseudo_id = kPseudoIdNone;
sub_context.pseudo_element = nullptr;
// ::slotted() only allows one compound selector.
DCHECK(selector.SelectorList()->First());
DCHECK(!CSSSelectorList::Next(*selector.SelectorList()->First()));
sub_context.selector = selector.SelectorList()->First();
SubResult sub_result(result);
if (MatchSelector(sub_context, sub_result) != kSelectorMatches) {
return false;
}
return true;
}
case CSSSelector::kPseudoHighlight: {
result.dynamic_pseudo = PseudoId::kPseudoIdHighlight;
// A null pseudo_argument_ means we are matching rules on the originating
// element. We keep track of which pseudo-elements may match for the
// element through result.dynamic_pseudo. For ::highlight() pseudo-
// elements we have a single flag for tracking whether an element may
// match _any_ ::highlight() element (kPseudoIdHighlight).
if (!pseudo_argument_ || pseudo_argument_ == selector.Argument()) {
result.custom_highlight_name = selector.Argument().Impl();
return true;
}
return false;
}
case CSSSelector::kPseudoViewTransition:
case CSSSelector::kPseudoViewTransitionGroup:
case CSSSelector::kPseudoViewTransitionGroupChildren:
case CSSSelector::kPseudoViewTransitionImagePair:
case CSSSelector::kPseudoViewTransitionOld:
case CSSSelector::kPseudoViewTransitionNew: {
const PseudoId selector_pseudo_id =
CSSSelector::GetPseudoId(selector.GetPseudoType());
if (context.pseudo_id == kPseudoIdNone) {
ViewTransition* transition =
ViewTransitionUtils::GetTransition(element);
if (transition && transition->Scope() == &element) {
// We don't strictly need to use dynamic_pseudo since we don't rely on
// SetHasPseudoElementStyle but we need to return a match to
// invalidate the originating element and set dynamic_pseudo to avoid
// collecting it as a matched rule in ElementRuleCollector.
result.dynamic_pseudo = selector_pseudo_id;
return true;
}
}
// Here, and below, the IsPseudoElement check is for a new pseudo-element
// rules matching approach, where the matching is done based on actual
// PseudoElement object and not Element + pseudo_id. We need to keep both
// versions as sometimes the matching is happening the old way and
// sometimes the new one.
PseudoId pseudo_id_to_check =
element.IsPseudoElement() ? element.GetPseudoId() : context.pseudo_id;
if (selector_pseudo_id != pseudo_id_to_check) {
return false;
}
result.dynamic_pseudo = context.pseudo_id;
if (selector_pseudo_id == kPseudoIdViewTransition) {
return true;
}
CHECK(!selector.IdentList().empty());
const AtomicString& name_or_wildcard = selector.IdentList()[0];
const String& pseudo_argument =
element.IsPseudoElement()
? To<PseudoElement>(element).GetPseudoArgument()
: pseudo_argument_;
// note that the pseudo_ident_list is the class list, and
// pseudo_argument is the name, while in the selector the IdentList() is
// both the name and the classes.
if (name_or_wildcard != CSSSelector::UniversalSelectorAtom() &&
name_or_wildcard != pseudo_argument) {
return false;
}
// https://drafts.csswg.org/css-view-transitions-2/#typedef-pt-class-selector
// A named view transition pseudo-element selector which has one or more
// <custom-ident> values in its <pt-class-selector> would only match an
// element if the class list value in named elements for the
// pseudo-element’s view-transition-name contains all of those values.
const Vector<AtomicString>& pseudo_ident_list =
element.IsPseudoElement()
? To<ViewTransitionPseudoElementBase>(element)
.ViewTransitionClassList()
: pseudo_ident_list_;
// selector.IdentList() is equivalent to
// <pt-name-selector><pt-class-selector>, as in [name, class, class, ...]
// so we check that all of its items excluding the first one are
// contained in the pseudo-element's classes (pseudo_ident_list).
return std::ranges::all_of(base::span(selector.IdentList()).subspan(1ul),
[&](const AtomicString& class_from_selector) {
return std::ranges::contains(
pseudo_ident_list, class_from_selector);
});
}
case CSSSelector::kPseudoScrollbarButton:
case CSSSelector::kPseudoScrollbarCorner:
case CSSSelector::kPseudoScrollbarThumb:
case CSSSelector::kPseudoScrollbarTrack:
case CSSSelector::kPseudoScrollbarTrackPiece: {
if (CSSSelector::GetPseudoId(selector.GetPseudoType()) !=
context.pseudo_id) {
return false;
}
result.dynamic_pseudo = context.pseudo_id;
return true;
}
case CSSSelector::kPseudoOverscrollAreaParent: {
return element.GetPseudoIdForStyling() == pseudo_id;
}
case CSSSelector::kPseudoScrollButton:
return MatchScrollButton(element, context, result);
case CSSSelector::kPseudoTargetText:
if (!is_ua_rule_) {
UseCounter::Count(context.element->GetDocument(),
WebFeature::kCSSSelectorTargetText);
}
[[fallthrough]];
default:
DCHECK_NE(mode_, kQueryingRules);
if (RuntimeEnabledFeatures::CSSLogicalCombinationPseudoEnabled()) {
DCHECK_EQ(kPseudoIdNone, context.pseudo_id);
// TODO(crbug.com/444386484): Support all pseudo-elements.
switch (selector.GetPseudoType()) {
case CSSSelector::kPseudoBefore:
case CSSSelector::kPseudoAfter:
case CSSSelector::kPseudoMarker:
case CSSSelector::kPseudoBackdrop:
return element.GetPseudoIdForStyling() == pseudo_id;
default:
return false;
}
}
result.dynamic_pseudo =
CSSSelector::GetPseudoId(selector.GetPseudoType());
DCHECK_NE(result.dynamic_pseudo, kPseudoIdNone);
// Normally, we don't match elements against nested pseudo-selectors;
// a rule such as div::column::scroll-marker will never match div,
// and never create a ::column by itself (some other rule, such as
// div::column, will need to do that, via dynamic_pseudo).
// This case is handled later.
//
// However, if we are matching against a pseudo-element, we are in
// a different situation. Check that the current simple selector
// matches the current element from the ancestor pseudo-elements
// (`element` would be set to the pseudo-element one step up in the
// chain).
//
// E.g., when matching against a scroll marker pseudo-element that is
// generated from a column pseudo-element, which is in turn generated
// from a div element, and the selector is indeed
// div::column::scroll-marker, we can end up here with
//
// element = PseudoElement for column
// selector = ::column
//
// so return true. However, if the selector was div::after::scroll-marker,
// we would fail here, as ::after doesn't match a column pseudo-element.
if (context.pseudo_element) {
if (result.pseudo_ancestor_index ==
context.pseudo_element_ancestors.size() - 1 &&
context.pseudo_element == element) {
// We've matched the entire ancestor chain, so there are
// no more pseudo-elements to create.
result.dynamic_pseudo = kPseudoIdNone;
}
// If `pseudo_ancestor_index` == size (i.e., past the end of the chain),
// it means that we've matched the entire ancestor chain and are now
// collecting pseudo styles for the pseudo-element; always match in this
// case (the dynamic_pseudo logic will pick up our result and create
// a pseudo-element instead of actually applying the style rule).
//
// E.g., for a column pseudo-element and the rule
// div::column::scroll-marker, when we've matched ::column and then look
// at the ::scroll-marker part, then index == size == 1, so mark
// ::column as having a ::scroll-marker pseudo (dynamic_pseudo was set
// earlier).
if (result.pseudo_ancestor_index ==
context.pseudo_element_ancestors.size()) {
return true;
}
// If not, we are still in the process of testing the chain of
// pseudo-selectors.
return element.GetPseudoIdForStyling() ==
selector.GetPseudoId(selector.GetPseudoType());
}
// Don't allow matching nested pseudo-elements from regular elements,
// e.g., div::column::scroll-marker against a div.
return context.previously_matched_pseudo_element == kPseudoIdNone;
}
}
bool SelectorChecker::CheckPseudoHost(const SelectorCheckingContext& context,
MatchResult& result) const {
const CSSSelector& selector = *context.selector;
Element& element =
context.GetElementForMatching(result.pseudo_ancestor_index);
// :host only matches a shadow host when :host is in a shadow tree of the
// shadow host.
if (!context.tree_scope) {
return false;
}
const Element* shadow_host = ShadowHost(context);
if (!shadow_host || shadow_host != element) {
return false;
}
DCHECK(IsShadowHost(element));
DCHECK(element.GetShadowRoot());
// For the case with no parameters, i.e. just :host.
if (!selector.SelectorList()) {
return true;
}
DCHECK(selector.SelectorList()->IsSingleComplexSelector());
SelectorCheckingContext sub_context(context);
sub_context.is_sub_selector = true;
sub_context.selector = selector.SelectorList()->First();
sub_context.pseudo_id = kPseudoIdNone;
sub_context.pseudo_element = nullptr;
// "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." [1]
//
// This means that the host element should not match in the context of
// the shadow root it's holding, but rather in the tree scope that's holding
// that element. This effectively makes the host non-featureless for the
// following MatchSelector call, since we are no longer matching in *its*
// shadow-tree-context.
//
// [1] https://drafts.csswg.org/css-shadow-1/#host-selector
sub_context.tree_scope = &context.element->GetTreeScope();
sub_context.scope = &sub_context.tree_scope->RootNode();
// Use FlatTreeTraversal to traverse a composed ancestor list of a given
// element.
Element* next_element = &element;
SelectorCheckingContext host_context(sub_context);
do {
SubResult sub_result(result);
host_context.element = next_element;
if (MatchSelector(host_context, sub_result) == kSelectorMatches) {
return true;
}
// TODO(andruud): This does not look correct, (although may not cause
// any observable problem at the time of writing). I would expect this
// to match in the context of context.element->GetTreeScope().
host_context.scope = nullptr;
host_context.tree_scope = nullptr;
if (selector.GetPseudoType() == CSSSelector::kPseudoHost) {
break;
}
host_context.in_rightmost_compound = false;
host_context.impact = Impact::kNonSubject;
next_element = FlatTreeTraversal::ParentElement(*next_element);
} while (next_element);
// FIXME: this was a fallthrough condition.
return false;
}
bool SelectorChecker::CheckPseudoScope(const SelectorCheckingContext& context,
MatchResult& result) const {
Element& element = *context.element;
if (!context.scope) {
return false;
}
if (context.scope->IsElementNode()) {
return context.scope == &element;
}
return element == element.GetDocument().documentElement();
}
bool SelectorChecker::CheckScrollbarPseudoClass(
const SelectorCheckingContext& context,
MatchResult& result) const {
const CSSSelector& selector = *context.selector;
if (selector.GetPseudoType() == CSSSelector::kPseudoNot) {
return CheckPseudoNot(context, result);
}
// FIXME: This is a temporary hack for resizers and scrollbar corners.
// Eventually :window-inactive should become a real
// pseudo-class and just apply to everything.
if (selector.GetPseudoType() == CSSSelector::kPseudoWindowInactive) {
return !context.element->GetDocument()
.GetPage()
->GetFocusController()
.IsActive();
}
if (!scrollbar_) {
return false;
}
switch (selector.GetPseudoType()) {
case CSSSelector::kPseudoEnabled:
return scrollbar_->Enabled();
case CSSSelector::kPseudoDisabled:
return !scrollbar_->Enabled();
case CSSSelector::kPseudoHover: {
ScrollbarPart hovered_part = scrollbar_->HoveredPart();
if (scrollbar_part_ == kScrollbarBGPart) {
return hovered_part != kNoPart;
}
if (scrollbar_part_ == kTrackBGPart) {
return hovered_part == kBackTrackPart ||
hovered_part == kForwardTrackPart || hovered_part == kThumbPart;
}
return scrollbar_part_ == hovered_part;
}
case CSSSelector::kPseudoActive: {
ScrollbarPart pressed_part = scrollbar_->PressedPart();
if (scrollbar_part_ == kScrollbarBGPart) {
return pressed_part != kNoPart;
}
if (scrollbar_part_ == kTrackBGPart) {
return pressed_part == kBackTrackPart ||
pressed_part == kForwardTrackPart || pressed_part == kThumbPart;
}
return scrollbar_part_ == pressed_part;
}
case CSSSelector::kPseudoHorizontal:
return scrollbar_->Orientation() == kHorizontalScrollbar;
case CSSSelector::kPseudoVertical:
return scrollbar_->Orientation() == kVerticalScrollbar;
case CSSSelector::kPseudoDecrement:
return scrollbar_part_ == kBackButtonStartPart ||
scrollbar_part_ == kBackButtonEndPart ||
scrollbar_part_ == kBackTrackPart;
case CSSSelector::kPseudoIncrement:
return scrollbar_part_ == kForwardButtonStartPart ||
scrollbar_part_ == kForwardButtonEndPart ||
scrollbar_part_ == kForwardTrackPart;
case CSSSelector::kPseudoStart:
return scrollbar_part_ == kBackButtonStartPart ||
scrollbar_part_ == kForwardButtonStartPart ||
scrollbar_part_ == kBackTrackPart;
case CSSSelector::kPseudoEnd:
return scrollbar_part_ == kBackButtonEndPart ||
scrollbar_part_ == kForwardButtonEndPart ||
scrollbar_part_ == kForwardTrackPart;
case CSSSelector::kPseudoDoubleButton:
// :double-button matches nothing on all platforms.
return false;
case CSSSelector::kPseudoSingleButton:
if (!scrollbar_->GetTheme().NativeThemeHasButtons()) {
return false;
}
return scrollbar_part_ == kBackButtonStartPart ||
scrollbar_part_ == kForwardButtonEndPart ||
scrollbar_part_ == kBackTrackPart ||
scrollbar_part_ == kForwardTrackPart;
case CSSSelector::kPseudoNoButton:
if (scrollbar_->GetTheme().NativeThemeHasButtons()) {
return false;
}
return scrollbar_part_ == kBackTrackPart ||
scrollbar_part_ == kForwardTrackPart;
case CSSSelector::kPseudoCornerPresent:
return scrollbar_->IsScrollCornerVisible();
default:
return false;
}
}
// Check a pseudo-class or pseudo-element selector in a context which
// matches a "would-be" pseudo-element that is not backed by a real
// blink::PseudoElement (it is "virtual").
//
// We use this mode when figuring out the style for pseudo-elements
// that are simply not possible to create pseudo-elements for (like highlights),
// or when a JS API call needs the computed style of a pseudo-element that
// isn't necessary to create for rendering purposes,
// e.g. getComputedStyle(e, '::before').
bool SelectorChecker::CheckVirtualPseudo(const SelectorCheckingContext& context,
MatchResult& result) const {
DCHECK(RuntimeEnabledFeatures::CSSLogicalCombinationPseudoEnabled());
DCHECK_NE(kPseudoIdNone, context.pseudo_id);
const CSSSelector& selector = *context.selector;
switch (selector.Match()) {
case CSSSelector::kPseudoClass:
switch (selector.GetPseudoType()) {
case CSSSelector::kPseudoIs:
case CSSSelector::kPseudoWhere:
return MatchesAnyInList(context, selector.SelectorListOrParent(),
result);
case CSSSelector::kPseudoNot:
return CheckPseudoNot(context, result);
default:
return false;
}
case CSSSelector::kPseudoElement:
// TODO(crbug.com/444386484): Support all pseudo-elements.
switch (selector.GetPseudoType()) {
case CSSSelector::kPseudoBefore:
case CSSSelector::kPseudoAfter:
case CSSSelector::kPseudoMarker:
case CSSSelector::kPseudoBackdrop:
return context.pseudo_id ==
selector.GetPseudoId(selector.GetPseudoType());
default:
return false;
}
default:
return false;
}
}
bool SelectorChecker::MatchesActiveViewTransitionPseudoClass(
const Element& element) {
return GetTransitionForScope(element) != nullptr;
}
bool SelectorChecker::MatchesOverscrollTarget(const Element& element) {
return RuntimeEnabledFeatures::OverscrollGesturesEnabled() &&
element.GetDocument().OverscrollCommandTargets().Contains(&element);
}
bool SelectorChecker::MatchesFocusPseudoClass(
const Element& element,
PseudoId matching_for_pseudo_element) {
const Element* matching_element = &element;
if (matching_for_pseudo_element != kPseudoIdNone) {
matching_element = element.GetPseudoElement(matching_for_pseudo_element);
if (!matching_element) {
return false;
}
}
bool force_pseudo_state = false;
probe::ForcePseudoState(const_cast<Element*>(matching_element),
CSSSelector::kPseudoFocus, &force_pseudo_state);
if (force_pseudo_state) {
return true;
}
return matching_element->IsFocused() && IsFrameFocused(*matching_element);
}
bool SelectorChecker::MatchesFocusVisiblePseudoClass(const Element& element) {
bool force_pseudo_state = false;
probe::ForcePseudoState(const_cast<Element*>(&element),
CSSSelector::kPseudoFocusVisible,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
if (!element.IsFocused() || !IsFrameFocused(element)) {
return false;
}
const Document& document = element.GetDocument();
// Exclude shadow hosts with non-UA ShadowRoot.
if (document.FocusedElement() != element && element.GetShadowRoot() &&
!element.GetShadowRoot()->IsUserAgent()) {
return false;
}
const Settings* settings = document.GetSettings();
const FocusOptions* focus_options = element.GetDocument().GetFocusOptions();
const bool force_focus_invisible =
!settings->GetAccessibilityAlwaysShowFocus() && focus_options &&
focus_options->hasFocusVisible() && !focus_options->focusVisible();
if (force_focus_invisible) {
return false;
}
bool always_show_focus = settings->GetAccessibilityAlwaysShowFocus() ||
(focus_options && focus_options->hasFocusVisible() &&
focus_options->focusVisible());
bool is_text_input = element.MayTriggerVirtualKeyboard();
bool last_focus_from_mouse =
document.GetFrame() &&
document.GetFrame()->Selection().FrameIsFocusedAndActive() &&
document.LastFocusType() == mojom::blink::FocusType::kMouse;
bool had_keyboard_event = document.HadKeyboardEvent();
return (always_show_focus || is_text_input || !last_focus_from_mouse ||
had_keyboard_event);
}
namespace {
// CalculateActivations will not produce any activations unless there is
// an outer activation (i.e. an activation of the outer StyleScope). If there
// is no outer StyleScope, we use this DefaultActivations as the outer
// activation. The scope provided to DefaultActivations is typically
// a ShadowTree.
StyleScopeActivations& DefaultActivations(const ContainerNode* scope) {
auto* activations = MakeGarbageCollected<StyleScopeActivations>();
activations->vector = HeapVector<StyleScopeActivation>(
1, StyleScopeActivation{scope, std::numeric_limits<unsigned>::max()});
return *activations;
}
// The activation ceiling is the highest ancestor element that can
// match inside some StyleScopeActivation.
//
// You would think that only elements inside the scoping root (activation.root)
// could match, but it is possible for a selector to be matched with respect to
// some scoping root [1] without actually being scoped to that root [2].
//
// This is relevant when matching elements inside a shadow tree, where the root
// of the default activation will be the ShadowRoot, but the host element (which
// sits *above* the ShadowRoot) should still be reached with :host.
//
// [1] https://drafts.csswg.org/selectors-4/#the-scope-pseudo
// [2] https://drafts.csswg.org/selectors-4/#scoped-selector
const Element* ActivationCeiling(const StyleScopeActivation& activation) {
if (!activation.root) {
return nullptr;
}
if (auto* element = DynamicTo<Element>(activation.root.Get())) {
return element;
}
ShadowRoot* shadow_root = activation.root->GetShadowRoot();
return shadow_root ? &shadow_root->host() : nullptr;
}
// True if this StyleScope has an implicit root at the specified element.
// This is used to find the roots for prelude-less @scope rules.
bool HasImplicitRoot(const StyleScope& style_scope, Element& element) {
if (const StyleScopeData* style_scope_data = element.GetStyleScopeData()) {
return style_scope_data->TriggersScope(style_scope);
}
return false;
}
} // namespace
const StyleScopeActivations& SelectorChecker::EnsureActivations(
const SelectorCheckingContext& context,
const StyleScope& style_scope) const {
DCHECK(context.style_scope_frame);
// The *outer activations* are the activations of the outer StyleScope.
// If there is no outer StyleScope, we create a "default" activation to
// make the code in CalculateActivations more readable.
//
// Must not be confused with the *parent activations* (seen in
// CalculateActivations), which are the activations (for the same StyleScope)
// of the *parent element*.
const StyleScopeActivations* outer_activations =
style_scope.Parent() ? &EnsureActivations(context, *style_scope.Parent())
: &DefaultActivations(context.scope);
// The `match_visited` flag may have been set to false e.g. due to a link
// having been encountered (see DisallowMatchVisited), but scope activations
// are calculated lazily when :scope is first seen in a compound selector,
// and the scoping limit needs to evaluate according to the original setting.
//
// Consider the following, which should not match, because the :visited link
// is a scoping limit:
//
// @scope (#foo) to (:visited) { :scope a:visited { ... } }
//
// In the above selector, we first match a:visited, and set match_visited to
// false since a link was encountered. Then we encounter a compound
// with :scope, which causes scopes to be activated (NeedsScopeActivation
// ()). At this point we try to find the scoping limit (:visited), but it
// wouldn't match anything because match_visited is set to false, so the
// selector would incorrectly match. For this reason we need to evaluate the
// scoping root and limits with the original match_visited setting.
bool match_visited = context.match_visited || context.had_match_visited;
// We only use the cache when matching normal/non-visited rules. Otherwise
// we'd need to double up the cache.
StyleScopeFrame* style_scope_frame =
match_visited ? nullptr : context.style_scope_frame;
const StyleScopeActivations* activations = CalculateActivations(
context.tree_scope, context.style_scope_frame->element_, style_scope,
*outer_activations, style_scope_frame, match_visited);
DCHECK(activations);
return *activations;
}
// Calculates all activations (i.e. active scopes) for `element`.
//
// This function will traverse the whole ancestor chain in the worst case,
// however, if a StyleScopeFrame is provided, it will reuse cached results
// found on that StyleScopeFrame.
const StyleScopeActivations* SelectorChecker::CalculateActivations(
const TreeScope* tree_scope,
Element& element,
const StyleScope& style_scope,
const StyleScopeActivations& outer_activations,
StyleScopeFrame* style_scope_frame,
bool match_visited) const {
Member<const StyleScopeActivations>* cached_activations_entry = nullptr;
if (style_scope_frame) {
auto entry = style_scope_frame->data_.insert(&style_scope, nullptr);
// We must not modify `style_scope_frame->data_` for the remainder
// of this function, since `cached_activations_entry` now points into
// the hash table.
cached_activations_entry = &entry.stored_value->value;
if (!entry.is_new_entry) {
DCHECK(cached_activations_entry->Get());
return cached_activations_entry->Get();
}
}
auto* activations = MakeGarbageCollected<StyleScopeActivations>();
if (!outer_activations.vector.empty()) {
const StyleScopeActivations* parent_activations = nullptr;
// Remain within the outer scope. I.e. don't look at elements above the
// highest outer activation.
if (&element != ActivationCeiling(outer_activations.vector.front())) {
if (Element* parent = element.ParentOrShadowHostElement()) {
// When calculating the activations on the parent element, we pass
// the parent StyleScopeFrame (if we have it) to be able to use the
// cached results, and avoid traversing the ancestor chain.
StyleScopeFrame* parent_frame =
style_scope_frame ? style_scope_frame->GetParentFrameOrNull(*parent)
: nullptr;
// Disable :visited matching when encountering the first link.
// This matches the behavior for regular child/descendant combinators.
bool parent_match_visited = match_visited && !element.IsLink();
parent_activations = CalculateActivations(
tree_scope, *parent, style_scope, outer_activations, parent_frame,
parent_match_visited);
}
}
// The activations of the parent element are still active for this element,
// unless this element is a scoping limit.
if (parent_activations) {
activations->match_flags = parent_activations->match_flags;
for (const StyleScopeActivation& activation :
parent_activations->vector) {
if (!ElementIsScopingLimit(tree_scope, style_scope, activation, element,
match_visited, activations->match_flags)) {
activations->vector.push_back(
StyleScopeActivation{activation.root, activation.proximity + 1});
}
}
}
// Check if we need to add a new activation for this element.
for (const StyleScopeActivation& outer_activation :
outer_activations.vector) {
if (style_scope.From()
? MatchesWithScope(element, *style_scope.From(), tree_scope,
/*scope=*/outer_activation.root, match_visited,
activations->match_flags)
: HasImplicitRoot(style_scope, element)) {
StyleScopeActivation activation{&element, 0};
// It's possible for a newly created activation to be immediately
// limited (e.g. @scope (.x) to (.x)).
if (!ElementIsScopingLimit(tree_scope, style_scope, activation, element,
match_visited, activations->match_flags)) {
activations->vector.push_back(activation);
}
break;
}
// TODO(crbug.com/1280240): Break if we don't depend on :scope.
}
}
// Cache the result if possible.
if (cached_activations_entry) {
*cached_activations_entry = activations;
}
return activations;
}
bool SelectorChecker::MatchesWithScope(Element& element,
const CSSSelector& selector_list,
const TreeScope* tree_scope,
const ContainerNode* scope,
bool match_visited,
MatchFlags& match_flags) const {
SelectorCheckingContext context(&element);
context.tree_scope = tree_scope;
context.scope = scope;
context.match_visited = match_visited;
// We are matching this selector list with the intent of storing the result
// in a cache (StyleScopeFrame). The :scope pseudo-class which triggered
// this call to MatchesWithScope, is either part of the subject compound
// or *not* part of the subject compound, but subsequent cache hits which
// return this result may have the opposite subject/non-subject position.
// Therefore we're using Impact::kBoth, to ensure sufficient invalidation.
context.impact = Impact::kBoth;
for (context.selector = &selector_list; context.selector;
context.selector = CSSSelectorList::Next(*context.selector)) {
MatchResult match_result;
bool match = MatchSelector(context, match_result) ==
SelectorChecker::kSelectorMatches;
match_flags |= match_result.flags;
if (match) {
return true;
}
}
return false;
}
bool SelectorChecker::ElementIsScopingLimit(
const TreeScope* tree_scope,
const StyleScope& style_scope,
const StyleScopeActivation& activation,
Element& element,
bool match_visited,
MatchFlags& match_flags) const {
if (!style_scope.To()) {
return false;
}
return MatchesWithScope(element, *style_scope.To(), tree_scope,
/*scope=*/activation.root.Get(), match_visited,
match_flags);
}
} // namespace blink