blob: 8fde38869a74ba53acf808314d203501da1e7d5e [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 "core/css/SelectorChecker.h"
#include "core/HTMLNames.h"
#include "core/css/CSSSelectorList.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/ElementTraversal.h"
#include "core/dom/Fullscreen.h"
#include "core/dom/NodeComputedStyle.h"
#include "core/dom/NthIndexCache.h"
#include "core/dom/StyleEngine.h"
#include "core/dom/Text.h"
#include "core/dom/shadow/ComposedTreeTraversal.h"
#include "core/dom/shadow/InsertionPoint.h"
#include "core/dom/shadow/ShadowRoot.h"
#include "core/editing/FrameSelection.h"
#include "core/frame/LocalFrame.h"
#include "core/html/HTMLDocument.h"
#include "core/html/HTMLFrameElementBase.h"
#include "core/html/HTMLInputElement.h"
#include "core/html/HTMLOptionElement.h"
#include "core/html/HTMLSelectElement.h"
#include "core/html/HTMLSlotElement.h"
#include "core/html/parser/HTMLParserIdioms.h"
#include "core/html/track/vtt/VTTElement.h"
#include "core/inspector/InspectorInstrumentation.h"
#include "core/layout/LayoutObject.h"
#include "core/layout/LayoutScrollbar.h"
#include "core/page/FocusController.h"
#include "core/page/Page.h"
#include "core/style/ComputedStyle.h"
#include "platform/scroll/ScrollableArea.h"
#include "platform/scroll/ScrollbarTheme.h"
namespace blink {
using namespace HTMLNames;
SelectorChecker::SelectorChecker(Mode mode)
: m_mode(mode)
{
}
static bool isFrameFocused(const Element& element)
{
return element.document().frame() && element.document().frame()->selection().isFocusedAndActive();
}
static bool matchesSpatialNavigationFocusPseudoClass(const Element& element)
{
return isHTMLOptionElement(element) && toHTMLOptionElement(element).spatialNavigationFocused() && isFrameFocused(element);
}
static bool matchesListBoxPseudoClass(const Element& element)
{
return isHTMLSelectElement(element) && !toHTMLSelectElement(element).usesMenuList();
}
static bool matchesTagName(const Element& element, const QualifiedName& tagQName)
{
if (tagQName == anyQName())
return true;
const AtomicString& localName = tagQName.localName();
if (localName != starAtom && localName != element.localName()) {
if (element.isHTMLElement() || !element.document().isHTMLDocument())
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() != tagQName.localNameUpper())
return false;
}
const AtomicString& namespaceURI = tagQName.namespaceURI();
return namespaceURI == starAtom || namespaceURI == 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->treeScope() == context.element->treeScope()))
return context.element->parentOrShadowHostElement();
return context.element->parentElement();
}
static const HTMLSlotElement* findSlotElementInScope(const SelectorChecker::SelectorCheckingContext& context)
{
if (!context.scope)
return nullptr;
const HTMLSlotElement* slot = context.element->assignedSlot();
while (slot) {
if (slot->treeScope() == context.scope->treeScope())
return slot;
slot = slot->assignedSlot();
}
return nullptr;
}
static bool scopeContainsLastMatchedElement(const SelectorChecker::SelectorCheckingContext& context)
{
// If this context isn't scoped, skip checking.
if (!context.scope)
return true;
if (context.scope->treeScope() == context.element->treeScope())
return true;
// Because Blink treats a shadow host's TreeScope as a separate one from its descendent shadow roots,
// if the last matched element is a shadow host, the condition above isn't met, even though it
// should be.
return context.element == context.scope->shadowHost() && (!context.previousElement || context.previousElement->isInDescendantTreeOf(context.element));
}
static inline bool nextSelectorExceedsScope(const SelectorChecker::SelectorCheckingContext& context)
{
if (context.scope && context.scope->isInShadowTree())
return context.element == context.scope->shadowHost();
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->document().inQuirksMode())
return true;
if (context.isSubSelector)
return true;
if (context.selector->relation() == CSSSelector::SubSelector && context.selector->tagHistory())
return true;
return context.element->isLink();
}
static bool isFirstChild(Element& element)
{
return !ElementTraversal::previousSibling(element);
}
static bool isLastChild(Element& element)
{
return !ElementTraversal::nextSibling(element);
}
static bool isFirstOfType(Element& element, const QualifiedName& type)
{
return !ElementTraversal::previousSibling(element, HasTagName(type));
}
static bool isLastOfType(Element& element, const QualifiedName& type)
{
return !ElementTraversal::nextSibling(element, HasTagName(type));
}
static int nthChildIndex(Element& element)
{
if (NthIndexCache* nthIndexCache = element.document().nthIndexCache())
return nthIndexCache->nthChildIndex(element);
int index = 1;
for (const Element* sibling = ElementTraversal::previousSibling(element); sibling; sibling = ElementTraversal::previousSibling(*sibling))
index++;
return index;
}
static int nthOfTypeIndex(Element& element, const QualifiedName& type)
{
if (NthIndexCache* nthIndexCache = element.document().nthIndexCache())
return nthIndexCache->nthChildIndexOfType(element, type);
int index = 1;
for (const Element* sibling = ElementTraversal::previousSibling(element, HasTagName(type)); sibling; sibling = ElementTraversal::previousSibling(*sibling, HasTagName(type)))
++index;
return index;
}
static int nthLastChildIndex(Element& element)
{
if (NthIndexCache* nthIndexCache = element.document().nthIndexCache())
return nthIndexCache->nthLastChildIndex(element);
int index = 1;
for (const Element* sibling = ElementTraversal::nextSibling(element); sibling; sibling = ElementTraversal::nextSibling(*sibling))
++index;
return index;
}
static int nthLastOfTypeIndex(Element& element, const QualifiedName& type)
{
if (NthIndexCache* nthIndexCache = element.document().nthIndexCache())
return nthIndexCache->nthLastChildIndexOfType(element, type);
int index = 1;
for (const Element* sibling = ElementTraversal::nextSibling(element, HasTagName(type)); sibling; sibling = ElementTraversal::nextSibling(*sibling, HasTagName(type)))
++index;
return index;
}
bool SelectorChecker::match(const SelectorCheckingContext& context, MatchResult& result) const
{
ASSERT(context.selector);
return matchSelector(context, result) == SelectorMatches;
}
bool SelectorChecker::match(const SelectorCheckingContext& context) const
{
MatchResult ignoreResult;
return match(context, ignoreResult);
}
// 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::Match SelectorChecker::matchSelector(const SelectorCheckingContext& context, MatchResult& result) const
{
MatchResult subResult;
if (!checkOne(context, subResult))
return SelectorFailsLocally;
if (subResult.dynamicPseudo != NOPSEUDO)
result.dynamicPseudo = subResult.dynamicPseudo;
if (context.selector->isLastInTagHistory()) {
if (scopeContainsLastMatchedElement(context)) {
result.specificity += subResult.specificity;
return SelectorMatches;
}
return SelectorFailsLocally;
}
Match match;
if (context.selector->relation() != CSSSelector::SubSelector) {
if (nextSelectorExceedsScope(context))
return SelectorFailsCompletely;
if (context.pseudoId != NOPSEUDO && context.pseudoId != result.dynamicPseudo)
return SelectorFailsCompletely;
TemporaryChange<PseudoId> dynamicPseudoScope(result.dynamicPseudo, NOPSEUDO);
match = matchForRelation(context, result);
} else {
match = matchForSubSelector(context, result);
}
if (match == SelectorMatches)
result.specificity += subResult.specificity;
return match;
}
static inline SelectorChecker::SelectorCheckingContext prepareNextContextForRelation(const SelectorChecker::SelectorCheckingContext& context)
{
SelectorChecker::SelectorCheckingContext nextContext(context);
ASSERT(context.selector->tagHistory());
nextContext.selector = context.selector->tagHistory();
return nextContext;
}
SelectorChecker::Match SelectorChecker::matchForSubSelector(const SelectorCheckingContext& context, MatchResult& result) const
{
SelectorCheckingContext nextContext = prepareNextContextForRelation(context);
PseudoId dynamicPseudo = result.dynamicPseudo;
// a selector is invalid if something follows a pseudo-element
// We make an exception for scrollbar pseudo elements and allow a set of pseudo classes (but nothing else)
// to follow the pseudo elements.
nextContext.hasScrollbarPseudo = dynamicPseudo != NOPSEUDO && (context.scrollbar || dynamicPseudo == SCROLLBAR_CORNER || dynamicPseudo == RESIZER);
nextContext.hasSelectionPseudo = dynamicPseudo == SELECTION;
if ((context.inRightmostCompound || m_mode == CollectingCSSRules || m_mode == CollectingStyleRules || m_mode == QueryingRules) && dynamicPseudo != NOPSEUDO
&& !nextContext.hasSelectionPseudo
&& !(nextContext.hasScrollbarPseudo && nextContext.selector->match() == CSSSelector::PseudoClass))
return SelectorFailsCompletely;
nextContext.isSubSelector = true;
return matchSelector(nextContext, result);
}
static inline bool isV0ShadowRoot(const Node* node)
{
return node && node->isShadowRoot() && toShadowRoot(node)->type() == ShadowRootType::V0;
}
SelectorChecker::Match SelectorChecker::matchForPseudoShadow(const SelectorCheckingContext& context, const ContainerNode* node, MatchResult& result) const
{
if (!isV0ShadowRoot(node))
return SelectorFailsCompletely;
if (!context.previousElement)
return SelectorFailsCompletely;
return matchSelector(context, result);
}
static inline Element* parentOrV0ShadowHostElement(const Element& element)
{
if (element.parentNode() && element.parentNode()->isShadowRoot()) {
if (toShadowRoot(element.parentNode())->type() != ShadowRootType::V0)
return nullptr;
}
return element.parentOrShadowHostElement();
}
SelectorChecker::Match SelectorChecker::matchForRelation(const SelectorCheckingContext& context, MatchResult& result) const
{
SelectorCheckingContext nextContext = prepareNextContextForRelation(context);
nextContext.previousElement = context.element;
CSSSelector::Relation relation = context.selector->relation();
// Disable :visited matching when we see the first link or try to match anything else than an ancestors.
if (!context.isSubSelector && (context.element->isLink() || (relation != CSSSelector::Descendant && relation != CSSSelector::Child)))
nextContext.visitedMatchType = VisitedMatchDisabled;
nextContext.pseudoId = NOPSEUDO;
switch (relation) {
case CSSSelector::Descendant:
if (context.selector->relationIsAffectedByPseudoContent()) {
for (Element* element = context.element; element; element = element->parentElement()) {
if (matchForShadowDistributed(nextContext, *element, result) == SelectorMatches)
return SelectorMatches;
}
return SelectorFailsCompletely;
}
nextContext.isSubSelector = false;
nextContext.inRightmostCompound = false;
if (nextContext.selector->pseudoType() == CSSSelector::PseudoShadow)
return matchForPseudoShadow(nextContext, context.element->containingShadowRoot(), result);
for (nextContext.element = parentElement(context); nextContext.element; nextContext.element = parentElement(nextContext)) {
Match match = matchSelector(nextContext, result);
if (match == SelectorMatches || match == SelectorFailsCompletely)
return match;
if (nextSelectorExceedsScope(nextContext))
return SelectorFailsCompletely;
}
return SelectorFailsCompletely;
case CSSSelector::Child:
{
if (context.selector->relationIsAffectedByPseudoContent())
return matchForShadowDistributed(nextContext, *context.element, result);
nextContext.isSubSelector = false;
nextContext.inRightmostCompound = false;
if (nextContext.selector->pseudoType() == CSSSelector::PseudoShadow)
return matchForPseudoShadow(nextContext, context.element->parentNode(), result);
nextContext.element = parentElement(context);
if (!nextContext.element)
return SelectorFailsCompletely;
return matchSelector(nextContext, result);
}
case CSSSelector::DirectAdjacent:
// Shadow roots can't have sibling elements
if (nextContext.selector->pseudoType() == CSSSelector::PseudoShadow)
return SelectorFailsCompletely;
if (m_mode == ResolvingStyle) {
if (ContainerNode* parent = context.element->parentElementOrShadowRoot())
parent->setChildrenAffectedByDirectAdjacentRules();
}
nextContext.element = ElementTraversal::previousSibling(*context.element);
if (!nextContext.element)
return SelectorFailsAllSiblings;
nextContext.isSubSelector = false;
nextContext.inRightmostCompound = false;
return matchSelector(nextContext, result);
case CSSSelector::IndirectAdjacent:
// Shadow roots can't have sibling elements
if (nextContext.selector->pseudoType() == CSSSelector::PseudoShadow)
return SelectorFailsCompletely;
if (m_mode == ResolvingStyle) {
if (ContainerNode* parent = context.element->parentElementOrShadowRoot())
parent->setChildrenAffectedByIndirectAdjacentRules();
}
nextContext.element = ElementTraversal::previousSibling(*context.element);
nextContext.isSubSelector = false;
nextContext.inRightmostCompound = false;
for (; nextContext.element; nextContext.element = ElementTraversal::previousSibling(*nextContext.element)) {
Match match = matchSelector(nextContext, result);
if (match == SelectorMatches || match == SelectorFailsAllSiblings || match == SelectorFailsCompletely)
return match;
}
return SelectorFailsAllSiblings;
case CSSSelector::ShadowPseudo:
{
if (!context.isUARule && context.selector->pseudoType() == CSSSelector::PseudoShadow)
UseCounter::countDeprecation(context.element->document(), UseCounter::CSSSelectorPseudoShadow);
// If we're in the same tree-scope as the scoping element, then following a shadow descendant combinator would escape that and thus the scope.
if (context.scope && context.scope->shadowHost() && context.scope->shadowHost()->treeScope() == context.element->treeScope())
return SelectorFailsCompletely;
Element* shadowHost = context.element->shadowHost();
if (!shadowHost)
return SelectorFailsCompletely;
nextContext.element = shadowHost;
nextContext.isSubSelector = false;
nextContext.inRightmostCompound = false;
return matchSelector(nextContext, result);
}
case CSSSelector::ShadowDeep:
{
if (!context.isUARule)
UseCounter::countDeprecation(context.element->document(), UseCounter::CSSDeepCombinator);
if (ShadowRoot* root = context.element->containingShadowRoot()) {
if (root->type() == ShadowRootType::UserAgent)
return SelectorFailsCompletely;
}
if (context.selector->relationIsAffectedByPseudoContent()) {
// TODO(kochi): closed mode tree should be handled as well for ::content.
for (Element* element = context.element; element; element = element->parentOrShadowHostElement()) {
if (matchForShadowDistributed(nextContext, *element, result) == SelectorMatches)
return SelectorMatches;
}
return SelectorFailsCompletely;
}
nextContext.isSubSelector = false;
nextContext.inRightmostCompound = false;
for (nextContext.element = parentOrV0ShadowHostElement(*context.element); nextContext.element; nextContext.element = parentOrV0ShadowHostElement(*nextContext.element)) {
Match match = matchSelector(nextContext, result);
if (match == SelectorMatches || match == SelectorFailsCompletely)
return match;
if (nextSelectorExceedsScope(nextContext))
return SelectorFailsCompletely;
}
return SelectorFailsCompletely;
}
case CSSSelector::ShadowSlot:
{
const HTMLSlotElement* slot = findSlotElementInScope(context);
if (!slot)
return SelectorFailsCompletely;
nextContext.element = const_cast<HTMLSlotElement*>(slot);
return matchSelector(nextContext, result);
}
case CSSSelector::SubSelector:
ASSERT_NOT_REACHED();
}
ASSERT_NOT_REACHED();
return SelectorFailsCompletely;
}
SelectorChecker::Match SelectorChecker::matchForShadowDistributed(const SelectorCheckingContext& context, const Element& element, MatchResult& result) const
{
WillBeHeapVector<RawPtrWillBeMember<InsertionPoint>, 8> insertionPoints;
collectDestinationInsertionPoints(element, insertionPoints);
SelectorCheckingContext nextContext(context);
nextContext.isSubSelector = false;
nextContext.inRightmostCompound = false;
for (const auto& insertionPoint : insertionPoints) {
nextContext.element = insertionPoint;
// TODO(esprehn): Why does SharingRules have a special case?
if (m_mode == SharingRules)
nextContext.scope = insertionPoint->containingShadowRoot();
if (match(nextContext, result))
return SelectorMatches;
}
return SelectorFailsLocally;
}
template<typename CharType>
static inline bool containsHTMLSpaceTemplate(const CharType* string, unsigned length)
{
for (unsigned i = 0; i < length; ++i) {
if (isHTMLSpace<CharType>(string[i]))
return true;
}
return false;
}
static inline bool containsHTMLSpace(const AtomicString& string)
{
if (LIKELY(string.is8Bit()))
return containsHTMLSpaceTemplate<LChar>(string.characters8(), string.length());
return containsHTMLSpaceTemplate<UChar>(string.characters16(), string.length());
}
static bool attributeValueMatches(const Attribute& attributeItem, CSSSelector::Match match, const AtomicString& selectorValue, TextCaseSensitivity caseSensitivity)
{
// TODO(esprehn): How do we get here with a null value?
const AtomicString& value = attributeItem.value();
if (value.isNull())
return false;
switch (match) {
case CSSSelector::AttributeExact:
if (caseSensitivity == TextCaseSensitive)
return selectorValue == value;
return equalIgnoringASCIICase(selectorValue, value);
case CSSSelector::AttributeSet:
return true;
case CSSSelector::AttributeList:
{
// Ignore empty selectors or selectors containing HTML spaces
if (selectorValue.isEmpty() || containsHTMLSpace(selectorValue))
return false;
unsigned startSearchAt = 0;
while (true) {
size_t foundPos = value.find(selectorValue, startSearchAt, caseSensitivity);
if (foundPos == kNotFound)
return false;
if (!foundPos || isHTMLSpace<UChar>(value[foundPos - 1])) {
unsigned endStr = foundPos + selectorValue.length();
if (endStr == value.length() || isHTMLSpace<UChar>(value[endStr]))
break; // We found a match.
}
// No match. Keep looking.
startSearchAt = foundPos + 1;
}
return true;
}
case CSSSelector::AttributeContain:
if (selectorValue.isEmpty())
return false;
return value.contains(selectorValue, caseSensitivity);
case CSSSelector::AttributeBegin:
if (selectorValue.isEmpty())
return false;
return value.startsWith(selectorValue, caseSensitivity);
case CSSSelector::AttributeEnd:
if (selectorValue.isEmpty())
return false;
return value.endsWith(selectorValue, caseSensitivity);
case CSSSelector::AttributeHyphen:
if (value.length() < selectorValue.length())
return false;
if (!value.startsWith(selectorValue, caseSensitivity))
return false;
// It they start the same, check for exact match or following '-':
if (value.length() != selectorValue.length() && value[selectorValue.length()] != '-')
return false;
return true;
default:
break;
}
ASSERT_NOT_REACHED();
return true;
}
static bool anyAttributeMatches(Element& element, CSSSelector::Match match, const CSSSelector& selector)
{
const QualifiedName& selectorAttr = selector.attribute();
ASSERT(selectorAttr.localName() != starAtom); // Should not be possible from the CSS grammar.
// Synchronize the attribute in case it is lazy-computed.
// Currently all lazy properties have a null namespace, so only pass localName().
element.synchronizeAttribute(selectorAttr.localName());
const AtomicString& selectorValue = selector.value();
TextCaseSensitivity caseSensitivity = (selector.attributeMatchType() == CSSSelector::CaseInsensitive) ? TextCaseASCIIInsensitive : TextCaseSensitive;
AttributeCollection attributes = element.attributesWithoutUpdate();
for (const auto& attributeItem: attributes) {
if (!attributeItem.matches(selectorAttr))
continue;
if (attributeValueMatches(attributeItem, match, selectorValue, caseSensitivity))
return true;
if (caseSensitivity == TextCaseASCIIInsensitive) {
if (selectorAttr.namespaceURI() != starAtom)
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 legacyCaseInsensitive = element.document().isHTMLDocument() && !HTMLDocument::isCaseSensitiveAttribute(selectorAttr);
// If case-insensitive, re-check, and count if result differs.
// See http://code.google.com/p/chromium/issues/detail?id=327060
if (legacyCaseInsensitive && attributeValueMatches(attributeItem, match, selectorValue, TextCaseASCIIInsensitive)) {
UseCounter::count(element.document(), UseCounter::CaseInsensitiveAttrSelectorMatch);
return true;
}
if (selectorAttr.namespaceURI() != starAtom)
return false;
}
return false;
}
bool SelectorChecker::checkOne(const SelectorCheckingContext& context, MatchResult& result) const
{
ASSERT(context.element);
Element& element = *context.element;
ASSERT(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->shadowHost() == element && (!selector.isHostPseudoClass()
&& !context.treatShadowHostAsNormalScope
&& selector.match() != CSSSelector::PseudoElement))
return false;
switch (selector.match()) {
case CSSSelector::Tag:
return matchesTagName(element, selector.tagQName());
case CSSSelector::Class:
return element.hasClass() && element.classNames().contains(selector.value());
case CSSSelector::Id:
return element.hasID() && element.idForStyleResolution() == selector.value();
// Attribute selectors
case CSSSelector::AttributeExact:
case CSSSelector::AttributeSet:
case CSSSelector::AttributeHyphen:
case CSSSelector::AttributeList:
case CSSSelector::AttributeContain:
case CSSSelector::AttributeBegin:
case CSSSelector::AttributeEnd:
return anyAttributeMatches(element, selector.match(), selector);
case CSSSelector::PseudoClass:
return checkPseudoClass(context, result);
case CSSSelector::PseudoElement:
return checkPseudoElement(context, result);
case CSSSelector::PagePseudoClass:
// FIXME: what?
return true;
case CSSSelector::Unknown:
// FIXME: what?
return true;
}
ASSERT_NOT_REACHED();
return true;
}
bool SelectorChecker::checkPseudoNot(const SelectorCheckingContext& context, MatchResult& result) const
{
const CSSSelector& selector = *context.selector;
SelectorCheckingContext subContext(context);
subContext.isSubSelector = true;
ASSERT(selector.selectorList());
for (subContext.selector = selector.selectorList()->first(); subContext.selector; subContext.selector = subContext.selector->tagHistory()) {
// :not cannot nest. I don't really know why this is a
// restriction in CSS3, but it is, so let's honor it.
// the parser enforces that this never occurs
ASSERT(subContext.selector->pseudoType() != CSSSelector::PseudoNot);
// We select between :visited and :link when applying. We don't know which one applied (or not) yet.
if (subContext.selector->pseudoType() == CSSSelector::PseudoVisited || (subContext.selector->pseudoType() == CSSSelector::PseudoLink && subContext.visitedMatchType == VisitedMatchEnabled))
return true;
// context.scope is not available if m_mode == SharingRules.
// We cannot determine whether :host or :scope matches a given element or not.
if (m_mode == SharingRules && (subContext.selector->isHostPseudoClass() || subContext.selector->pseudoType() == CSSSelector::PseudoScope))
return true;
if (!checkOne(subContext, result))
return true;
}
return false;
}
bool SelectorChecker::checkPseudoClass(const SelectorCheckingContext& context, MatchResult& result) const
{
Element& element = *context.element;
const CSSSelector& selector = *context.selector;
if (context.hasScrollbarPseudo) {
// 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.pseudoType()) {
case CSSSelector::PseudoNot:
return checkPseudoNot(context, result);
case CSSSelector::PseudoEmpty:
{
bool result = true;
for (Node* n = element.firstChild(); n; n = n->nextSibling()) {
if (n->isElementNode()) {
result = false;
break;
}
if (n->isTextNode()) {
Text* textNode = toText(n);
if (!textNode->data().isEmpty()) {
result = false;
break;
}
}
}
if (m_mode == ResolvingStyle) {
element.setStyleAffectedByEmpty();
if (context.inRightmostCompound)
context.elementStyle->setEmptyState(result);
else if (element.computedStyle() && (element.document().styleEngine().usesSiblingRules() || element.computedStyle()->unique()))
element.mutableComputedStyle()->setEmptyState(result);
}
return result;
}
case CSSSelector::PseudoFirstChild:
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle) {
parent->setChildrenAffectedByFirstChildRules();
element.setAffectedByFirstChildRules();
}
return isFirstChild(element);
}
break;
case CSSSelector::PseudoFirstOfType:
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByForwardPositionalRules();
return isFirstOfType(element, element.tagQName());
}
break;
case CSSSelector::PseudoLastChild:
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle) {
parent->setChildrenAffectedByLastChildRules();
element.setAffectedByLastChildRules();
}
if (!parent->isFinishedParsingChildren())
return false;
return isLastChild(element);
}
break;
case CSSSelector::PseudoLastOfType:
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByBackwardPositionalRules();
if (!parent->isFinishedParsingChildren())
return false;
return isLastOfType(element, element.tagQName());
}
break;
case CSSSelector::PseudoOnlyChild:
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle) {
parent->setChildrenAffectedByFirstChildRules();
parent->setChildrenAffectedByLastChildRules();
element.setAffectedByFirstChildRules();
element.setAffectedByLastChildRules();
}
if (!parent->isFinishedParsingChildren())
return false;
return isFirstChild(element) && isLastChild(element);
}
break;
case CSSSelector::PseudoOnlyOfType:
// FIXME: This selector is very slow.
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle) {
parent->setChildrenAffectedByForwardPositionalRules();
parent->setChildrenAffectedByBackwardPositionalRules();
}
if (!parent->isFinishedParsingChildren())
return false;
return isFirstOfType(element, element.tagQName()) && isLastOfType(element, element.tagQName());
}
break;
case CSSSelector::PseudoPlaceholderShown:
if (isHTMLTextFormControlElement(element))
return toHTMLTextFormControlElement(element).isPlaceholderVisible();
break;
case CSSSelector::PseudoNthChild:
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByForwardPositionalRules();
return selector.matchNth(nthChildIndex(element));
}
break;
case CSSSelector::PseudoNthOfType:
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByForwardPositionalRules();
return selector.matchNth(nthOfTypeIndex(element, element.tagQName()));
}
break;
case CSSSelector::PseudoNthLastChild:
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByBackwardPositionalRules();
if (!parent->isFinishedParsingChildren())
return false;
return selector.matchNth(nthLastChildIndex(element));
}
break;
case CSSSelector::PseudoNthLastOfType:
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByBackwardPositionalRules();
if (!parent->isFinishedParsingChildren())
return false;
return selector.matchNth(nthLastOfTypeIndex(element, element.tagQName()));
}
break;
case CSSSelector::PseudoTarget:
return element == element.document().cssTarget();
case CSSSelector::PseudoAny:
{
SelectorCheckingContext subContext(context);
subContext.isSubSelector = true;
ASSERT(selector.selectorList());
for (subContext.selector = selector.selectorList()->first(); subContext.selector; subContext.selector = CSSSelectorList::next(*subContext.selector)) {
if (match(subContext))
return true;
}
}
break;
case CSSSelector::PseudoAutofill:
return element.isFormControlElement() && toHTMLFormControlElement(element).isAutofilled();
case CSSSelector::PseudoAnyLink:
case CSSSelector::PseudoLink:
return element.isLink();
case CSSSelector::PseudoVisited:
return element.isLink() && context.visitedMatchType == VisitedMatchEnabled;
case CSSSelector::PseudoDrag:
if (m_mode == ResolvingStyle) {
if (context.inRightmostCompound) {
context.elementStyle->setAffectedByDrag();
} else {
context.elementStyle->setUnique();
element.setChildrenOrSiblingsAffectedByDrag();
}
}
return element.layoutObject() && element.layoutObject()->isDragging();
case CSSSelector::PseudoFocus:
if (m_mode == ResolvingStyle) {
if (context.inRightmostCompound) {
context.elementStyle->setAffectedByFocus();
} else {
context.elementStyle->setUnique();
element.setChildrenOrSiblingsAffectedByFocus();
}
}
return matchesFocusPseudoClass(element);
case CSSSelector::PseudoHover:
if (m_mode == ResolvingStyle) {
if (context.inRightmostCompound) {
context.elementStyle->setAffectedByHover();
} else {
context.elementStyle->setUnique();
element.setChildrenOrSiblingsAffectedByHover();
}
}
if (!shouldMatchHoverOrActive(context))
return false;
if (InspectorInstrumentation::forcePseudoState(&element, CSSSelector::PseudoHover))
return true;
return element.hovered();
case CSSSelector::PseudoActive:
if (m_mode == ResolvingStyle) {
if (context.inRightmostCompound) {
context.elementStyle->setAffectedByActive();
} else {
context.elementStyle->setUnique();
element.setChildrenOrSiblingsAffectedByActive();
}
}
if (!shouldMatchHoverOrActive(context))
return false;
if (InspectorInstrumentation::forcePseudoState(&element, CSSSelector::PseudoActive))
return true;
return element.active();
case CSSSelector::PseudoEnabled:
if (element.isFormControlElement() || isHTMLOptionElement(element) || isHTMLOptGroupElement(element))
return !element.isDisabledFormControl();
if (isHTMLAnchorElement(element) || isHTMLAreaElement(element))
return element.isLink();
break;
case CSSSelector::PseudoFullPageMedia:
return element.document().isMediaDocument();
case CSSSelector::PseudoDefault:
return element.isDefaultButtonForForm();
case CSSSelector::PseudoDisabled:
// TODO(esprehn): Why not just always return isDisabledFormControl()?
// Can it be true for elements not in the list below?
if (element.isFormControlElement() || isHTMLOptionElement(element) || isHTMLOptGroupElement(element))
return element.isDisabledFormControl();
break;
case CSSSelector::PseudoReadOnly:
return element.matchesReadOnlyPseudoClass();
case CSSSelector::PseudoReadWrite:
return element.matchesReadWritePseudoClass();
case CSSSelector::PseudoOptional:
return element.isOptionalFormControl();
case CSSSelector::PseudoRequired:
return element.isRequiredFormControl();
case CSSSelector::PseudoValid:
if (m_mode == ResolvingStyle)
element.document().setContainsValidityStyleRules();
return element.matchesValidityPseudoClasses() && element.isValidElement();
case CSSSelector::PseudoInvalid:
if (m_mode == ResolvingStyle)
element.document().setContainsValidityStyleRules();
return element.matchesValidityPseudoClasses() && !element.isValidElement();
case CSSSelector::PseudoChecked:
{
if (isHTMLInputElement(element)) {
HTMLInputElement& inputElement = toHTMLInputElement(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 (inputElement.shouldAppearChecked() && !inputElement.shouldAppearIndeterminate())
return true;
} else if (isHTMLOptionElement(element) && toHTMLOptionElement(element).selected()) {
return true;
}
break;
}
case CSSSelector::PseudoIndeterminate:
return element.shouldAppearIndeterminate();
case CSSSelector::PseudoRoot:
return element == element.document().documentElement();
case CSSSelector::PseudoLang:
{
AtomicString value;
if (element.isVTTElement())
value = toVTTElement(element).language();
else
value = element.computeInheritedLanguage();
const AtomicString& argument = selector.argument();
if (value.isEmpty() || !value.startsWith(argument, TextCaseASCIIInsensitive))
break;
if (value.length() != argument.length() && value[argument.length()] != '-')
break;
return true;
}
case CSSSelector::PseudoFullScreen:
// While a Document is in the fullscreen state, and the document's current fullscreen
// element is an element in the document, the 'full-screen' pseudoclass applies to
// that element. Also, an <iframe>, <object> or <embed> element whose child browsing
// context's Document is in the fullscreen state has the 'full-screen' pseudoclass applied.
if (isHTMLFrameElementBase(element) && element.containsFullScreenElement())
return true;
return Fullscreen::isActiveFullScreenElement(element);
case CSSSelector::PseudoFullScreenAncestor:
return element.containsFullScreenElement();
case CSSSelector::PseudoInRange:
if (m_mode == ResolvingStyle)
element.document().setContainsValidityStyleRules();
return element.isInRange();
case CSSSelector::PseudoOutOfRange:
if (m_mode == ResolvingStyle)
element.document().setContainsValidityStyleRules();
return element.isOutOfRange();
case CSSSelector::PseudoFutureCue:
return element.isVTTElement() && !toVTTElement(element).isPastNode();
case CSSSelector::PseudoPastCue:
return element.isVTTElement() && toVTTElement(element).isPastNode();
case CSSSelector::PseudoScope:
if (m_mode == SharingRules)
return true;
if (context.scope == &element.document())
return element == element.document().documentElement();
return context.scope == &element;
case CSSSelector::PseudoUnresolved:
return element.isUnresolvedCustomElement();
case CSSSelector::PseudoHost:
case CSSSelector::PseudoHostContext:
return checkPseudoHost(context, result);
case CSSSelector::PseudoSpatialNavigationFocus:
return context.isUARule && matchesSpatialNavigationFocusPseudoClass(element);
case CSSSelector::PseudoListBox:
return context.isUARule && matchesListBoxPseudoClass(element);
case CSSSelector::PseudoWindowInactive:
if (!context.hasSelectionPseudo)
return false;
return !element.document().page()->focusController().isActive();
case CSSSelector::PseudoHorizontal:
case CSSSelector::PseudoVertical:
case CSSSelector::PseudoDecrement:
case CSSSelector::PseudoIncrement:
case CSSSelector::PseudoStart:
case CSSSelector::PseudoEnd:
case CSSSelector::PseudoDoubleButton:
case CSSSelector::PseudoSingleButton:
case CSSSelector::PseudoNoButton:
case CSSSelector::PseudoCornerPresent:
return false;
case CSSSelector::PseudoUnknown:
default:
ASSERT_NOT_REACHED();
break;
}
return false;
}
bool SelectorChecker::checkPseudoElement(const SelectorCheckingContext& context, MatchResult& result) const
{
const CSSSelector& selector = *context.selector;
Element& element = *context.element;
switch (selector.pseudoType()) {
case CSSSelector::PseudoCue:
{
SelectorCheckingContext subContext(context);
subContext.isSubSelector = true;
subContext.scope = nullptr;
subContext.treatShadowHostAsNormalScope = false;
for (subContext.selector = selector.selectorList()->first(); subContext.selector; subContext.selector = CSSSelectorList::next(*subContext.selector)) {
if (match(subContext))
return true;
}
return false;
}
case CSSSelector::PseudoWebKitCustomElement:
{
if (ShadowRoot* root = element.containingShadowRoot())
return root->type() == ShadowRootType::UserAgent && element.shadowPseudoId() == selector.value();
return false;
}
case CSSSelector::PseudoSlotted:
{
SelectorCheckingContext subContext(context);
subContext.isSubSelector = true;
subContext.scope = nullptr;
subContext.treatShadowHostAsNormalScope = false;
// ::slotted() only allows one compound selector.
ASSERT(selector.selectorList()->first());
ASSERT(!CSSSelectorList::next(*selector.selectorList()->first()));
subContext.selector = selector.selectorList()->first();
return match(subContext);
}
case CSSSelector::PseudoContent:
return element.isInShadowTree() && element.isInsertionPoint();
case CSSSelector::PseudoShadow:
return element.isInShadowTree() && context.previousElement;
default:
break;
}
if (m_mode == QueryingRules)
return false;
if (m_mode == SharingRules)
return true;
result.dynamicPseudo = CSSSelector::pseudoId(selector.pseudoType());
ASSERT(result.dynamicPseudo != NOPSEUDO);
return true;
}
bool SelectorChecker::checkPseudoHost(const SelectorCheckingContext& context, MatchResult& result) const
{
const CSSSelector& selector = *context.selector;
Element& element = *context.element;
if (m_mode == SharingRules)
return true;
// :host only matches a shadow host when :host is in a shadow tree of the shadow host.
if (!context.scope)
return false;
const ContainerNode* shadowHost = context.scope->shadowHost();
if (!shadowHost || shadowHost != element)
return false;
ASSERT(element.shadow());
// For empty parameter case, i.e. just :host or :host().
if (!selector.selectorList()) // Use *'s specificity. So just 0.
return true;
SelectorCheckingContext subContext(context);
subContext.isSubSelector = true;
bool matched = false;
unsigned maxSpecificity = 0;
// If one of simple selectors matches an element, returns SelectorMatches. Just "OR".
for (subContext.selector = selector.selectorList()->first(); subContext.selector; subContext.selector = CSSSelectorList::next(*subContext.selector)) {
subContext.treatShadowHostAsNormalScope = true;
subContext.scope = context.scope;
// Use ComposedTreeTraversal to traverse a composed ancestor list of a given element.
Element* nextElement = &element;
SelectorCheckingContext hostContext(subContext);
do {
MatchResult subResult;
hostContext.element = nextElement;
if (match(hostContext, subResult)) {
matched = true;
// Consider div:host(div:host(div:host(div:host...))).
maxSpecificity = std::max(maxSpecificity, hostContext.selector->specificity() + subResult.specificity);
break;
}
hostContext.treatShadowHostAsNormalScope = false;
hostContext.scope = nullptr;
if (selector.pseudoType() == CSSSelector::PseudoHost)
break;
hostContext.inRightmostCompound = false;
nextElement = ComposedTreeTraversal::parentElement(*nextElement);
} while (nextElement);
}
if (matched) {
result.specificity += maxSpecificity;
return true;
}
// FIXME: this was a fallthrough condition.
return false;
}
bool SelectorChecker::checkScrollbarPseudoClass(const SelectorCheckingContext& context, MatchResult& result) const
{
const CSSSelector& selector = *context.selector;
LayoutScrollbar* scrollbar = context.scrollbar;
ScrollbarPart part = context.scrollbarPart;
if (selector.pseudoType() == CSSSelector::PseudoNot)
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.pseudoType() == CSSSelector::PseudoWindowInactive)
return !context.element->document().page()->focusController().isActive();
if (!scrollbar)
return false;
switch (selector.pseudoType()) {
case CSSSelector::PseudoEnabled:
return scrollbar->enabled();
case CSSSelector::PseudoDisabled:
return !scrollbar->enabled();
case CSSSelector::PseudoHover:
{
ScrollbarPart hoveredPart = scrollbar->hoveredPart();
if (part == ScrollbarBGPart)
return hoveredPart != NoPart;
if (part == TrackBGPart)
return hoveredPart == BackTrackPart || hoveredPart == ForwardTrackPart || hoveredPart == ThumbPart;
return part == hoveredPart;
}
case CSSSelector::PseudoActive:
{
ScrollbarPart pressedPart = scrollbar->pressedPart();
if (part == ScrollbarBGPart)
return pressedPart != NoPart;
if (part == TrackBGPart)
return pressedPart == BackTrackPart || pressedPart == ForwardTrackPart || pressedPart == ThumbPart;
return part == pressedPart;
}
case CSSSelector::PseudoHorizontal:
return scrollbar->orientation() == HorizontalScrollbar;
case CSSSelector::PseudoVertical:
return scrollbar->orientation() == VerticalScrollbar;
case CSSSelector::PseudoDecrement:
return part == BackButtonStartPart || part == BackButtonEndPart || part == BackTrackPart;
case CSSSelector::PseudoIncrement:
return part == ForwardButtonStartPart || part == ForwardButtonEndPart || part == ForwardTrackPart;
case CSSSelector::PseudoStart:
return part == BackButtonStartPart || part == ForwardButtonStartPart || part == BackTrackPart;
case CSSSelector::PseudoEnd:
return part == BackButtonEndPart || part == ForwardButtonEndPart || part == ForwardTrackPart;
case CSSSelector::PseudoDoubleButton:
{
WebScrollbarButtonsPlacement buttonsPlacement = scrollbar->theme().buttonsPlacement();
if (part == BackButtonStartPart || part == ForwardButtonStartPart || part == BackTrackPart)
return buttonsPlacement == WebScrollbarButtonsPlacementDoubleStart || buttonsPlacement == WebScrollbarButtonsPlacementDoubleBoth;
if (part == BackButtonEndPart || part == ForwardButtonEndPart || part == ForwardTrackPart)
return buttonsPlacement == WebScrollbarButtonsPlacementDoubleEnd || buttonsPlacement == WebScrollbarButtonsPlacementDoubleBoth;
return false;
}
case CSSSelector::PseudoSingleButton:
{
WebScrollbarButtonsPlacement buttonsPlacement = scrollbar->theme().buttonsPlacement();
if (part == BackButtonStartPart || part == ForwardButtonEndPart || part == BackTrackPart || part == ForwardTrackPart)
return buttonsPlacement == WebScrollbarButtonsPlacementSingle;
return false;
}
case CSSSelector::PseudoNoButton:
{
WebScrollbarButtonsPlacement buttonsPlacement = scrollbar->theme().buttonsPlacement();
if (part == BackTrackPart)
return buttonsPlacement == WebScrollbarButtonsPlacementNone || buttonsPlacement == WebScrollbarButtonsPlacementDoubleEnd;
if (part == ForwardTrackPart)
return buttonsPlacement == WebScrollbarButtonsPlacementNone || buttonsPlacement == WebScrollbarButtonsPlacementDoubleStart;
return false;
}
case CSSSelector::PseudoCornerPresent:
return scrollbar->scrollableArea() && scrollbar->scrollableArea()->isScrollCornerVisible();
default:
return false;
}
}
bool SelectorChecker::matchesFocusPseudoClass(const Element& element)
{
if (InspectorInstrumentation::forcePseudoState(const_cast<Element*>(&element), CSSSelector::PseudoFocus))
return true;
return element.focused() && isFrameFocused(element);
}
}