blob: 18cbcaac963a189bb695b4bac29c8a5bb9906ee5 [file] [log] [blame]
/*
* 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 "base/auto_reset.h"
#include "style_rule.h"
#include "third_party/blink/public/mojom/input/focus_type.mojom-blink.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/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/dom/css_toggle.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/node_computed_style.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/shadow_root.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_form_control_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_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_slot_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/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/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/view_transition/view_transition.h"
#include "third_party/blink/renderer/core/view_transition/view_transition_utils.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.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->list();
}
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) {
if (tag_q_name == AnyQName()) {
return true;
}
const AtomicString& local_name = tag_q_name.LocalName();
if (local_name != CSSSelector::UniversalSelectorAtom() &&
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 Element* ParentElement(
const SelectorChecker::SelectorCheckingContext& context) {
// - If context.scope is a shadow root, we should walk up to its shadow host.
// - If context.scope is some element in some shadow tree and querySelector
// initialized the context, e.g. shadowRoot.querySelector(':host *'),
// (a) context.element has the same treescope as context.scope, need to walk
// up to its shadow host.
// (b) Otherwise, should not walk up from a shadow root to a shadow host.
if (context.scope &&
(context.scope == context.element->ContainingShadowRoot() ||
context.scope->GetTreeScope() == context.element->GetTreeScope())) {
return context.element->ParentOrShadowHostElement();
}
return context.element->parentElement();
}
// 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.scope) {
return context.element->AssignedSlot();
}
for (const HTMLSlotElement* slot = context.element->AssignedSlot(); slot;
slot = slot->AssignedSlot()) {
if (slot->GetTreeScope() == context.scope->GetTreeScope()) {
return slot;
}
}
return nullptr;
}
static inline bool NextSelectorExceedsScope(
const SelectorChecker::SelectorCheckingContext& context) {
if (context.scope && context.scope->IsInShadowTree()) {
return context.element == context.scope->OwnerShadowHost();
}
return false;
}
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 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));
}
bool SelectorChecker::Match(const SelectorCheckingContext& context,
MatchResult& result) const {
DCHECK(context.selector);
#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 (UNLIKELY(context.vtt_originating_element)) {
// A kUAShadow combinator is required for VTT matching.
if (context.selector->IsLastInComplexSelector()) {
return false;
}
}
return MatchSelector(context, result) == kSelectorMatches;
}
// 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 {
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).
#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;
}
if (sub_result.dynamic_pseudo != kPseudoIdNone) {
result.dynamic_pseudo = sub_result.dynamic_pseudo;
result.custom_highlight_name = std::move(sub_result.custom_highlight_name);
}
if (context.selector->IsLastInComplexSelector()) {
return kSelectorMatches;
}
MatchStatus match;
if (context.selector->Relation() != CSSSelector::kSubSelector) {
if (NextSelectorExceedsScope(context)) {
return kSelectorFailsCompletely;
}
if (context.pseudo_id != kPseudoIdNone &&
context.pseudo_id != result.dynamic_pseudo) {
return kSelectorFailsCompletely;
}
base::AutoReset<PseudoId> dynamic_pseudo_scope(&result.dynamic_pseudo,
kPseudoIdNone);
match = MatchForRelation(context, result);
} else {
match = MatchForSubSelector(context, result);
}
return match;
}
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);
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_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;
}
// 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.has_selection_pseudo = dynamic_pseudo == kPseudoIdSelection;
next_context.is_sub_selector = true;
return MatchSelector(next_context, result);
}
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.is_sub_selector || context.in_nested_complex_selector) &&
(context.element->IsLink() || (relation != CSSSelector::kDescendant &&
relation != CSSSelector::kChild))) {
next_context.match_visited = false;
}
next_context.in_rightmost_compound = false;
next_context.is_sub_selector = false;
next_context.previous_element = context.element;
next_context.pseudo_id = kPseudoIdNone;
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:
if (next_context.selector->GetPseudoType() == CSSSelector::kPseudoScope) {
if (next_context.selector->IsLastInComplexSelector()) {
if (context.scope && context.scope->IsDocumentFragment()) {
return kSelectorMatches;
}
}
}
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 (NextSelectorExceedsScope(next_context)) {
return kSelectorFailsCompletely;
}
if (next_context.element->IsLink()) {
next_context.match_visited = false;
}
}
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;
}
return MatchSelector(next_context, result);
}
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 =
ElementTraversal::PreviousSibling(*context.element);
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 =
ElementTraversal::PreviousSibling(*context.element);
for (; next_context.element;
next_context.element =
ElementTraversal::PreviousSibling(*next_context.element)) {
MatchStatus match = MatchSelector(next_context, result);
if (match == kSelectorMatches || match == kSelectorFailsAllSiblings ||
match == kSelectorFailsCompletely) {
return match;
}
}
return kSelectorFailsAllSiblings;
case CSSSelector::kUAShadow: {
// 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 (context.scope && context.scope->OwnerShadowHost() &&
context.scope->OwnerShadowHost()->GetTreeScope() ==
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;
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 (next_context.element) {
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.
TreeScope& host_tree_scope =
next_context.selector->IsHostPseudoClass()
? *context.scope->GetTreeScope().ParentTreeScope()
: context.scope->GetTreeScope();
if (next_context.element->GetTreeScope() == host_tree_scope) {
return MatchSelector(next_context, result);
}
}
return kSelectorFailsCompletely;
case CSSSelector::kSubSelector:
break;
case CSSSelector::kScopeActivation:
if (context.style_scope) {
const StyleScopeActivations& activations =
EnsureActivations(context, *context.style_scope);
if (activations.empty()) {
return kSelectorFailsCompletely;
}
for (const StyleScopeActivation& activation : activations) {
next_context.style_scope = nullptr;
next_context.scope = activation.root;
if (MatchSelector(next_context, result) == kSelectorMatches) {
result.proximity = activation.proximity;
return kSelectorMatches;
}
}
return kSelectorFailsLocally;
}
return MatchSelector(next_context, result);
}
NOTREACHED();
return kSelectorFailsCompletely;
}
static bool AttributeValueMatches(const Attribute& attribute_item,
CSSSelector::MatchType match,
const AtomicString& selector_value,
TextCaseSensitivity case_sensitivity) {
// TODO(esprehn): How do we get here with a null value?
const AtomicString& value = attribute_item.Value();
if (value.IsNull()) {
return false;
}
switch (match) {
case CSSSelector::kAttributeExact:
if (case_sensitivity == kTextCaseSensitive) {
return selector_value == value;
}
return 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 =
value.Find(selector_value, start_search_at, case_sensitivity);
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 value.Contains(selector_value, case_sensitivity);
case CSSSelector::kAttributeBegin:
if (selector_value.empty()) {
return false;
}
return value.StartsWith(selector_value, case_sensitivity);
case CSSSelector::kAttributeEnd:
if (selector_value.empty()) {
return false;
}
return value.EndsWith(selector_value, case_sensitivity);
case CSSSelector::kAttributeHyphen:
if (value.length() < selector_value.length()) {
return false;
}
if (!value.StartsWith(selector_value, case_sensitivity)) {
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();
return false;
}
}
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());
const AtomicString& selector_value = selector.Value();
TextCaseSensitivity case_sensitivity =
(selector.AttributeMatch() ==
CSSSelector::AttributeMatchType::kCaseInsensitive)
? kTextCaseASCIIInsensitive
: kTextCaseSensitive;
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 (AttributeValueMatches(attribute_item, match, selector_value,
case_sensitivity)) {
return true;
}
if (case_sensitivity == kTextCaseASCIIInsensitive) {
if (selector_attr.NamespaceURI() != g_star_atom) {
return false;
}
continue;
}
// 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.
bool legacy_case_insensitive = IsA<HTMLDocument>(element.GetDocument()) &&
!selector.IsCaseSensitiveAttribute();
// If case-insensitive, re-check, and count if result differs.
// See http://code.google.com/p/chromium/issues/detail?id=327060
if (legacy_case_insensitive &&
AttributeValueMatches(attribute_item, match, selector_value,
kTextCaseASCIIInsensitive)) {
// If the `s` modifier is in the attribute selector, return false
// despite of legacy_case_insensitive.
if (selector.AttributeMatch() ==
CSSSelector::AttributeMatchType::kCaseSensitiveAlways) {
DCHECK(RuntimeEnabledFeatures::CSSCaseSensitiveSelectorEnabled());
return false;
}
UseCounter::Count(element.GetDocument(),
WebFeature::kCaseInsensitiveAttrSelectorMatch);
return true;
}
if (selector_attr.NamespaceURI() != g_star_atom) {
return false;
}
}
return false;
}
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;
// Only :host and :host-context() should match the host:
// http://drafts.csswg.org/css-scoping/#host-element
if (context.scope && context.scope->OwnerShadowHost() == element &&
(!selector.IsHostPseudoClass() &&
selector.GetPseudoType() != CSSSelector::kPseudoScope &&
!context.treat_shadow_host_as_normal_scope &&
selector.Match() != CSSSelector::kPseudoElement)) {
return false;
}
switch (selector.Match()) {
case CSSSelector::kTag:
return MatchesTagName(element, selector.TagQName());
case CSSSelector::kClass:
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();
return false;
}
}
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;
sub_context.pseudo_id = kPseudoIdNone;
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* TraverseToParent(Element* element) {
return element->parentElement();
}
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, TraverseToParent);
case CSSSelector::kRelativeChild:
return CacheMatchedElementsAndReturnMatchedResultForDirectRelation(
has_anchor_element, has_argument_leftmost_compound_matches,
cache_scope_context, TraverseToParent);
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();
return false;
}
}
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();
break;
}
}
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_->parentElement();
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;
}
} // namespace
bool SelectorChecker::CheckPseudoHas(const SelectorCheckingContext& context,
MatchResult& result) const {
CheckPseudoHasCacheScope check_pseudo_has_cache_scope(
&context.element->GetDocument());
Element* has_anchor_element = context.element;
Document& document = has_anchor_element->GetDocument();
DCHECK(document.GetCheckPseudoHasCacheScope());
SelectorCheckingContext sub_context(has_anchor_element);
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;
DCHECK(context.selector->SelectorList());
for (const CSSSelector* selector = context.selector->SelectorList()->First();
selector; selector = CSSSelectorList::Next(*selector)) {
CheckPseudoHasArgumentContext argument_context(selector);
CSSSelector::RelationType leftmost_relation =
argument_context.LeftmostRelation();
CheckPseudoHasCacheScope::Context cache_scope_context(&document,
argument_context);
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::CheckPseudoClass(const SelectorCheckingContext& context,
MatchResult& result) const {
Element& element = *context.element;
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::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: {
if (IsTransitionPseudoElement(context.pseudo_id)) {
DCHECK(element.IsDocumentElement());
DCHECK(context.pseudo_argument);
auto* transition =
ViewTransitionUtils::GetActiveTransition(element.GetDocument());
DCHECK(transition);
return transition->MatchForOnlyChild(context.pseudo_id,
*context.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:
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::kPseudoSelectorFragmentAnchor:
return MatchesSelectorFragmentAnchorPseudoClass(element);
case CSSSelector::kPseudoTarget:
probe::ForcePseudoState(&element, CSSSelector::kPseudoTarget,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
return element == element.GetDocument().CssTarget();
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: {
auto* html_form_element = DynamicTo<HTMLFormControlElement>(&element);
return html_form_element && html_form_element->IsAutofilled();
}
case CSSSelector::kPseudoAutofillPreviewed: {
auto* html_form_element = DynamicTo<HTMLFormControlElement>(&element);
return html_form_element && html_form_element->GetAutofillState() ==
WebAutofillState::kPreviewed;
}
case CSSSelector::kPseudoAutofillSelected: {
auto* html_form_element = DynamicTo<HTMLFormControlElement>(&element);
return html_form_element && html_form_element->HighlightAutofilled();
}
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 (!context.in_rightmost_compound) {
element.SetChildrenOrSiblingsAffectedByDrag();
}
}
if (context.in_rightmost_compound) {
result.SetFlag(MatchFlag::kAffectedByDrag);
}
return element.IsDragged();
case CSSSelector::kPseudoFocus:
if (mode_ == kResolvingStyle) {
if (UNLIKELY(context.is_inside_has_pseudo_class)) {
element.SetAncestorsOrSiblingsAffectedByFocusInHas();
} else {
if (!context.in_rightmost_compound) {
element.SetChildrenOrSiblingsAffectedByFocus();
}
}
}
return MatchesFocusPseudoClass(element);
case CSSSelector::kPseudoFocusVisible:
if (mode_ == kResolvingStyle) {
if (UNLIKELY(context.is_inside_has_pseudo_class)) {
element.SetAncestorsOrSiblingsAffectedByFocusVisibleInHas();
} else {
if (!context.in_rightmost_compound) {
element.SetChildrenOrSiblingsAffectedByFocusVisible();
}
}
}
return MatchesFocusVisiblePseudoClass(element);
case CSSSelector::kPseudoFocusWithin:
if (mode_ == kResolvingStyle) {
if (UNLIKELY(context.is_inside_has_pseudo_class)) {
element.SetAncestorsOrSiblingsAffectedByFocusInHas();
} else if (!context.in_rightmost_compound) {
element.SetChildrenOrSiblingsAffectedByFocusWithin();
}
}
if (context.in_rightmost_compound) {
result.SetFlag(MatchFlag::kAffectedByFocusWithin);
}
probe::ForcePseudoState(&element, CSSSelector::kPseudoFocusWithin,
&force_pseudo_state);
if (force_pseudo_state) {
return true;
}
return element.HasFocusWithin();
case CSSSelector::kPseudoHover:
if (mode_ == kResolvingStyle) {
if (UNLIKELY(context.is_inside_has_pseudo_class)) {
element.SetAncestorsOrSiblingsAffectedByHoverInHas();
} else if (!context.in_rightmost_compound) {
element.SetChildrenOrSiblingsAffectedByHover();
}
}
if (context.in_rightmost_compound) {
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 (UNLIKELY(context.is_inside_has_pseudo_class)) {
element.SetAncestorsOrSiblingsAffectedByActiveInHas();
} else if (!context.in_rightmost_compound) {
element.SetChildrenOrSiblingsAffectedByActive();
}
}
if (context.in_rightmost_compound) {
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:
return element.MatchesEnabledPseudoClass();
case CSSSelector::kPseudoFullPageMedia:
return element.GetDocument().IsMediaDocument();
case CSSSelector::kPseudoDefault:
return element.MatchesDefaultPseudoClass();
case CSSSelector::kPseudoDisabled:
return element.IsDisabledFormControl();
case CSSSelector::kPseudoReadOnly:
return element.MatchesReadOnlyPseudoClass();
case CSSSelector::kPseudoReadWrite:
return element.MatchesReadWritePseudoClass();
case CSSSelector::kPseudoOptional:
return element.IsOptionalFormControl();
case CSSSelector::kPseudoRequired:
return element.IsRequiredFormControl();
case CSSSelector::kPseudoValid:
return element.MatchesValidityPseudoClasses() && element.IsValidElement();
case CSSSelector::kPseudoInvalid:
return element.MatchesValidityPseudoClasses() &&
!element.IsValidElement();
case CSSSelector::kPseudoChecked: {
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;
}
}
break;
}
case CSSSelector::kPseudoIndeterminate:
return element.ShouldAppearIndeterminate();
case CSSSelector::kPseudoRoot:
return element == element.GetDocument().documentElement();
case CSSSelector::kPseudoLang: {
auto* vtt_element = DynamicTo<VTTElement>(element);
AtomicString value = vtt_element ? vtt_element->Language()
: element.ComputeInheritedLanguage();
const AtomicString& argument = selector.Argument();
if (value.empty() ||
!value.StartsWith(argument, kTextCaseASCIIInsensitive)) {
break;
}
if (value.length() != argument.length() &&
value[argument.length()] != '-') {
break;
}
return true;
}
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;
}
if (auto* html_element = DynamicTo<HTMLElement>(element)) {
return html_element->CachedDirectionality() == direction;
}
break;
}
case CSSSelector::kPseudoPopoverOpen:
if (auto* html_element = DynamicTo<HTMLElement>(element);
html_element && html_element->HasPopoverAttribute()) {
return html_element->popoverOpen();
}
return false;
case CSSSelector::kPseudoOpen:
if (auto* selectmenu = DynamicTo<HTMLSelectMenuElement>(element)) {
return selectmenu->open();
}
return false;
case CSSSelector::kPseudoClosed:
if (auto* selectmenu = DynamicTo<HTMLSelectMenuElement>(element)) {
return !selectmenu->open();
}
return false;
case CSSSelector::kPseudoFullscreen:
// fall through
case CSSSelector::kPseudoFullScreen:
return Fullscreen::IsFullscreenFlagSetFor(element);
case CSSSelector::kPseudoFullScreenAncestor:
return element.ContainsFullScreenElement();
case CSSSelector::kPseudoPaused: {
DCHECK(RuntimeEnabledFeatures::CSSPseudoPlayingPausedEnabled());
auto* media_element = DynamicTo<HTMLMediaElement>(element);
return media_element && media_element->paused();
}
case CSSSelector::kPseudoPictureInPicture:
return PictureInPictureController::IsElementInPictureInPicture(&element);
case CSSSelector::kPseudoPlaying: {
DCHECK(RuntimeEnabledFeatures::CSSPseudoPlayingPausedEnabled());
auto* media_element = DynamicTo<HTMLMediaElement>(element);
return media_element && !media_element->paused();
}
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:
return element.IsInRange();
case CSSSelector::kPseudoOutOfRange:
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:
return CheckPseudoHost(context, result);
case CSSSelector::kPseudoSpatialNavigationFocus:
DCHECK(is_ua_rule_);
return MatchesSpatialNavigationFocusPseudoClass(element);
case CSSSelector::kPseudoSpatialNavigationInterest:
DCHECK(is_ua_rule_);
return MatchesSpatialNavigationInterestPseudoClass(element);
case CSSSelector::kPseudoHasDatalist:
DCHECK(is_ua_rule_);
return MatchesHasDatalistPseudoClass(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::kPseudoHostHasAppearance:
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.has_selection_pseudo) {
return false;
}
return !element.GetDocument().GetPage()->GetFocusController().IsActive();
case CSSSelector::kPseudoState: {
return element.DidAttachInternals() &&
element.EnsureElementInternals().HasState(selector.Value());
}
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:
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 (context.in_rightmost_compound) {
element.SetAffectedBySubjectHas();
} else {
element.SetAffectedByNonSubjectHas();
}
if (selector.ContainsPseudoInsideHasPseudoClass()) {
element.SetAffectedByPseudoInHas();
}
if (selector.ContainsComplexLogicalCombinationsInsideHasPseudoClass()) {
element.SetAffectedByLogicalCombinationsInHas();
}
}
return CheckPseudoHas(context, result);
case CSSSelector::kPseudoRelativeAnchor:
DCHECK(context.relative_anchor_element);
return context.relative_anchor_element == &element;
case CSSSelector::kPseudoToggle: {
using State = ToggleRoot::State;
const AtomicString& name = selector.Argument();
const State* value = selector.ToggleValue();
CSSToggle* toggle = CSSToggle::FindToggleInScope(element, name);
// An element matches :toggle() if the element is in scope for a toggle
// with the name given by <custom-ident>, and ...
if (!toggle) {
return false;
}
if (value) {
// ... either the toggle’s value matches the provided <toggle-value>,
// ...
return toggle->ValueMatches(*value);
} else {
// ... or the <toggle-value> is omitted and the toggle is in any
// active value.
return !toggle->ValueMatches(State(0));
}
}
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::kPseudoTrue:
return true;
case CSSSelector::kPseudoUnknown:
default:
NOTREACHED();
break;
}
return false;
}
static bool MatchesUAShadowElement(Element& element, const AtomicString& id) {
ShadowRoot* root = element.ContainingShadowRoot();
return root && root->IsUserAgent() && element.ShadowPseudoId() == id;
}
bool SelectorChecker::CheckPseudoElement(const SelectorCheckingContext& context,
MatchResult& result) const {
const CSSSelector& selector = *context.selector;
Element& element = *context.element;
if (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.treat_shadow_host_as_normal_scope = false;
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:
DCHECK(part_names_);
for (const auto& part_name : *selector.PartNames()) {
if (!part_names_->Contains(part_name)) {
return false;
}
}
return true;
case CSSSelector::kPseudoFileSelectorButton:
return MatchesUAShadowElement(
element, shadow_element_names::kPseudoFileUploadButton);
case CSSSelector::kPseudoPlaceholder:
return MatchesUAShadowElement(
element, shadow_element_names::kPseudoInputPlaceholder);
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.treat_shadow_host_as_normal_scope = false;
// ::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::kPseudoViewTransitionGroup:
case CSSSelector::kPseudoViewTransitionImagePair:
case CSSSelector::kPseudoViewTransitionOld:
case CSSSelector::kPseudoViewTransitionNew: {
if (CSSSelector::GetPseudoId(selector.GetPseudoType()) !=
context.pseudo_id) {
return false;
}
result.dynamic_pseudo = context.pseudo_id;
return selector.Argument() == CSSSelector::UniversalSelectorAtom() ||
selector.Argument() == pseudo_argument_;
}
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::kPseudoTargetText:
if (!is_ua_rule_) {
UseCounter::Count(context.element->GetDocument(),
WebFeature::kCSSSelectorTargetText);
}
[[fallthrough]];
default:
DCHECK_NE(mode_, kQueryingRules);
result.dynamic_pseudo =
CSSSelector::GetPseudoId(selector.GetPseudoType());
DCHECK_NE(result.dynamic_pseudo, kPseudoIdNone);
return true;
}
}
bool SelectorChecker::CheckPseudoHost(const SelectorCheckingContext& context,
MatchResult& result) const {
const CSSSelector& selector = *context.selector;
Element& element = *context.element;
// :host only matches a shadow host when :host is in a shadow tree of the
// shadow host.
if (!context.scope) {
return false;
}
const ContainerNode* shadow_host = context.scope->OwnerShadowHost();
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()->HasOneSelector());
SelectorCheckingContext sub_context(context);
sub_context.is_sub_selector = true;
sub_context.selector = selector.SelectorList()->First();
sub_context.treat_shadow_host_as_normal_scope = true;
sub_context.scope = context.scope;
// 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;
}
host_context.treat_shadow_host_as_normal_scope = false;
host_context.scope = nullptr;
if (selector.GetPseudoType() == CSSSelector::kPseudoHost) {
break;
}
host_context.in_rightmost_compound = false;
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_->GetScrollableArea() &&
scrollbar_->GetScrollableArea()->IsScrollCornerVisible();
default:
return false;
}
}
bool SelectorChecker::MatchesSelectorFragmentAnchorPseudoClass(
const Element& element) {
return element == element.GetDocument().CssTarget() &&
element.GetDocument().View()->GetFragmentAnchor() &&
element.GetDocument()
.View()
->GetFragmentAnchor()
->IsSelectorFragmentAnchor();
}
bool SelectorChecker::MatchesFocusPseudoClass(const Element& element) {
bool force_pseudo_state = false;
probe::ForcePseudoState(const_cast<Element*>(&element),
CSSSelector::kPseudoFocus, &force_pseudo_state);
if (force_pseudo_state) {
return true;
}
return element.IsFocused() && IsFrameFocused(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();
bool always_show_focus = settings->GetAccessibilityAlwaysShowFocus();
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);
}
// static
bool SelectorChecker::MatchesSpatialNavigationInterestPseudoClass(
const Element& element) {
if (!IsSpatialNavigationEnabled(element.GetDocument().GetFrame())) {
return false;
}
if (!RuntimeEnabledFeatures::FocuslessSpatialNavigationEnabled()) {
return false;
}
DCHECK(element.GetDocument().GetPage());
Element* interested_element = element.GetDocument()
.GetPage()
->GetSpatialNavigationController()
.GetInterestedElement();
return interested_element && *interested_element == element;
}
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 DefaultActivation as the outer
// activation. The scope provided to DefaultActivation is typically
// a ShadowTree.
StyleScopeActivation DefaultActivation(const ContainerNode* scope) {
return StyleScopeActivation{scope, std::numeric_limits<unsigned>::max()};
}
// 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;
}
} // 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())
: MakeGarbageCollected<StyleScopeActivations>(
1, DefaultActivation(context.scope));
const StyleScopeActivations* activations =
CalculateActivations(context.style_scope_frame->element_, style_scope,
*outer_activations, context.style_scope_frame);
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(
Element& element,
const StyleScope& style_scope,
const StyleScopeActivations& outer_activations,
StyleScopeFrame* style_scope_frame) 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.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.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;
parent_activations = CalculateActivations(
*parent, style_scope, outer_activations, parent_frame);
}
}
// The activations of the parent element are still active for this element,
// unless this element is a scoping limit.
if (parent_activations) {
for (const StyleScopeActivation& activation : *parent_activations) {
if (!ElementIsScopingLimit(style_scope, activation, element)) {
activations->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) {
if (style_scope.From() ? MatchesWithScope(element, *style_scope.From(),
outer_activation.root)
: style_scope.HasImplicitRoot(&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(style_scope, activation, element)) {
activations->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 ContainerNode* scope) const {
SelectorCheckingContext context(&element);
context.scope = scope;
for (context.selector = &selector_list; context.selector;
context.selector = CSSSelectorList::Next(*context.selector)) {
SelectorChecker::MatchResult ignore_result;
if (MatchSelector(context, ignore_result) ==
SelectorChecker::kSelectorMatches) {
return true;
}
}
return false;
}
bool SelectorChecker::ElementIsScopingLimit(
const StyleScope& style_scope,
const StyleScopeActivation& activation,
Element& element) const {
if (!style_scope.To()) {
return false;
}
return MatchesWithScope(element, *style_scope.To(), activation.root.Get());
}
} // namespace blink