blob: e7c45376513705b0193fa2276b5c57b308b6d3a1 [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 "config.h"
#include "core/css/SelectorChecker.h"
#include "core/HTMLNames.h"
#include "core/css/CSSSelectorList.h"
#include "core/css/SiblingTraversalStrategies.h"
#include "core/dom/Document.h"
#include "core/dom/ElementTraversal.h"
#include "core/dom/FullscreenElementStack.h"
#include "core/dom/NodeRenderStyle.h"
#include "core/dom/Text.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/parser/HTMLParserIdioms.h"
#include "core/html/track/vtt/VTTElement.h"
#include "core/inspector/InspectorInstrumentation.h"
#include "core/page/FocusController.h"
#include "core/rendering/RenderObject.h"
#include "core/rendering/RenderScrollbar.h"
#include "core/rendering/style/RenderStyle.h"
#include "platform/scroll/ScrollableArea.h"
#include "platform/scroll/ScrollbarTheme.h"
namespace WebCore {
using namespace HTMLNames;
SelectorChecker::SelectorChecker(Document& document, Mode mode)
: m_strictParsing(!document.inQuirksMode())
, m_documentIsHTML(document.isHTMLDocument())
, m_mode(mode)
{
}
static bool matchesCustomPseudoElement(const Element* element, const CSSSelector& selector)
{
ShadowRoot* root = element->containingShadowRoot();
if (!root || root->type() != ShadowRoot::UserAgentShadowRoot)
return false;
if (element->shadowPseudoId() != selector.value())
return false;
return true;
}
Element* SelectorChecker::parentElement(const SelectorCheckingContext& context, bool allowToCrossBoundary) const
{
// CrossesBoundary means we don't care any context.scope. So we can walk up from a shadow root to its shadow host.
if (allowToCrossBoundary)
return context.element->parentOrShadowHostElement();
// If context.scope is a shadow root, we should walk up to its shadow host.
if ((context.behaviorAtBoundary & SelectorChecker::ScopeIsShadowRoot) && context.scope == context.element->containingShadowRoot())
return context.element->parentOrShadowHostElement();
if ((context.behaviorAtBoundary & SelectorChecker::BoundaryBehaviorMask) != SelectorChecker::StaysWithinTreeScope)
return context.element->parentElement();
// 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->treeScope() == context.element->treeScope())
return context.element->parentOrShadowHostElement();
return context.element->parentElement();
}
bool SelectorChecker::scopeContainsLastMatchedElement(const SelectorCheckingContext& context) const
{
if (!(context.behaviorAtBoundary & SelectorChecker::ScopeContainsLastMatchedElement))
return true;
ASSERT(context.scope);
// If behaviorAtBoundary is not ScopeIsShadowRoot, we can use "contains".
if (!(context.behaviorAtBoundary & SelectorChecker::ScopeIsShadowRoot))
return context.scope->contains(context.element);
// If a given element is scope, i.e. shadow host, matches.
if (context.element == context.scope->shadowHost() && (!context.previousElement || context.previousElement->isInDescendantTreeOf(context.element)))
return true;
ShadowRoot* root = context.element->containingShadowRoot();
if (!root)
return false;
// If a host of the containing shadow root is scope, matches.
return root == context.scope;
}
static inline bool nextSelectorExceedsScope(const SelectorChecker::SelectorCheckingContext& context)
{
if (context.scope && context.scope->isInShadowTree())
return context.element == context.scope->shadowHost();
return false;
}
// 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
template<typename SiblingTraversalStrategy>
SelectorChecker::Match SelectorChecker::match(const SelectorCheckingContext& context, const SiblingTraversalStrategy& siblingTraversalStrategy, MatchResult* result) const
{
// first selector has to match
unsigned specificity = 0;
if (!checkOne(context, siblingTraversalStrategy, &specificity))
return SelectorFailsLocally;
if (context.selector->match() == CSSSelector::PseudoElement) {
if (context.selector->isCustomPseudoElement()) {
if (!matchesCustomPseudoElement(context.element, *context.selector))
return SelectorFailsLocally;
} else if (context.selector->isContentPseudoElement()) {
if (!context.element->isInShadowTree() || !context.element->isInsertionPoint())
return SelectorFailsLocally;
} else if (context.selector->isShadowPseudoElement()) {
if (!context.element->isInShadowTree() || !context.previousElement)
return SelectorFailsCompletely;
} else {
if ((!context.elementStyle && m_mode == ResolvingStyle) || m_mode == QueryingRules)
return SelectorFailsLocally;
PseudoId pseudoId = CSSSelector::pseudoId(context.selector->pseudoType());
if (pseudoId == FIRST_LETTER)
context.element->document().styleEngine()->setUsesFirstLetterRules(true);
if (pseudoId != NOPSEUDO && m_mode != SharingRules && result)
result->dynamicPseudo = pseudoId;
}
}
// Prepare next selector
const CSSSelector* historySelector = context.selector->tagHistory();
if (!historySelector) {
if (scopeContainsLastMatchedElement(context)) {
if (result)
result->specificity += specificity;
return SelectorMatches;
}
return SelectorFailsLocally;
}
Match match;
if (context.selector->relation() != CSSSelector::SubSelector) {
// Abort if the next selector would exceed the scope.
if (nextSelectorExceedsScope(context))
return SelectorFailsCompletely;
// Bail-out if this selector is irrelevant for the pseudoId
if (context.pseudoId != NOPSEUDO && (!result || context.pseudoId != result->dynamicPseudo))
return SelectorFailsCompletely;
if (result) {
TemporaryChange<PseudoId> dynamicPseudoScope(result->dynamicPseudo, NOPSEUDO);
match = matchForRelation(context, siblingTraversalStrategy, result);
} else {
return matchForRelation(context, siblingTraversalStrategy, 0);
}
} else {
match = matchForSubSelector(context, siblingTraversalStrategy, result);
}
if (match != SelectorMatches || !result)
return match;
result->specificity += specificity;
return SelectorMatches;
}
static inline SelectorChecker::SelectorCheckingContext prepareNextContextForRelation(const SelectorChecker::SelectorCheckingContext& context)
{
SelectorChecker::SelectorCheckingContext nextContext(context);
ASSERT(context.selector->tagHistory());
nextContext.selector = context.selector->tagHistory();
return nextContext;
}
static inline bool isAuthorShadowRoot(const Node* node)
{
return node && node->isShadowRoot() && toShadowRoot(node)->type() == ShadowRoot::AuthorShadowRoot;
}
template<typename SiblingTraversalStrategy>
SelectorChecker::Match SelectorChecker::matchForSubSelector(const SelectorCheckingContext& context, const SiblingTraversalStrategy& siblingTraversalStrategy, MatchResult* result) const
{
SelectorCheckingContext nextContext = prepareNextContextForRelation(context);
PseudoId dynamicPseudo = result ? result->dynamicPseudo : NOPSEUDO;
// 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.elementStyle || 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 match(nextContext, siblingTraversalStrategy, result);
}
static bool selectorMatchesShadowRoot(const CSSSelector* selector)
{
return selector && selector->isShadowPseudoElement();
}
template<typename SiblingTraversalStrategy>
SelectorChecker::Match SelectorChecker::matchForPseudoShadow(const ContainerNode* node, const SelectorCheckingContext& context, const SiblingTraversalStrategy& siblingTraversalStrategy, MatchResult* result) const
{
if (!isAuthorShadowRoot(node))
return SelectorFailsCompletely;
return match(context, siblingTraversalStrategy, result);
}
template<typename SiblingTraversalStrategy>
SelectorChecker::Match SelectorChecker::matchForRelation(const SelectorCheckingContext& context, const SiblingTraversalStrategy& siblingTraversalStrategy, 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(element, siblingTraversalStrategy, nextContext, result) == SelectorMatches)
return SelectorMatches;
}
return SelectorFailsCompletely;
}
nextContext.isSubSelector = false;
nextContext.elementStyle = 0;
if (selectorMatchesShadowRoot(nextContext.selector))
return matchForPseudoShadow(context.element->containingShadowRoot(), nextContext, siblingTraversalStrategy, result);
for (nextContext.element = parentElement(context); nextContext.element; nextContext.element = parentElement(nextContext)) {
Match match = this->match(nextContext, siblingTraversalStrategy, result);
if (match == SelectorMatches || match == SelectorFailsCompletely)
return match;
if (nextSelectorExceedsScope(nextContext))
return SelectorFailsCompletely;
}
return SelectorFailsCompletely;
case CSSSelector::Child:
{
if (context.selector->relationIsAffectedByPseudoContent())
return matchForShadowDistributed(context.element, siblingTraversalStrategy, nextContext, result);
nextContext.isSubSelector = false;
nextContext.elementStyle = 0;
if (selectorMatchesShadowRoot(nextContext.selector))
return matchForPseudoShadow(context.element->parentNode(), nextContext, siblingTraversalStrategy, result);
nextContext.element = parentElement(context);
if (!nextContext.element)
return SelectorFailsCompletely;
return match(nextContext, siblingTraversalStrategy, result);
}
case CSSSelector::DirectAdjacent:
// Shadow roots can't have sibling elements
if (selectorMatchesShadowRoot(nextContext.selector))
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.elementStyle = 0;
return match(nextContext, siblingTraversalStrategy, result);
case CSSSelector::IndirectAdjacent:
// Shadow roots can't have sibling elements
if (selectorMatchesShadowRoot(nextContext.selector))
return SelectorFailsCompletely;
if (m_mode == ResolvingStyle) {
if (ContainerNode* parent = context.element->parentElementOrShadowRoot())
parent->setChildrenAffectedByIndirectAdjacentRules();
}
nextContext.element = ElementTraversal::previousSibling(*context.element);
nextContext.isSubSelector = false;
nextContext.elementStyle = 0;
for (; nextContext.element; nextContext.element = ElementTraversal::previousSibling(*nextContext.element)) {
Match match = this->match(nextContext, siblingTraversalStrategy, result);
if (match == SelectorMatches || match == SelectorFailsAllSiblings || match == SelectorFailsCompletely)
return match;
};
return SelectorFailsAllSiblings;
case CSSSelector::ShadowPseudo:
{
// 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() && (context.behaviorAtBoundary & BoundaryBehaviorMask) != StaysWithinTreeScope)
return SelectorFailsCompletely;
Element* shadowHost = context.element->shadowHost();
if (!shadowHost)
return SelectorFailsCompletely;
nextContext.element = shadowHost;
nextContext.isSubSelector = false;
nextContext.elementStyle = 0;
return this->match(nextContext, siblingTraversalStrategy, result);
}
case CSSSelector::ShadowDeep:
{
nextContext.isSubSelector = false;
nextContext.elementStyle = 0;
for (nextContext.element = parentElement(context, true); nextContext.element; nextContext.element = parentElement(nextContext, true)) {
Match match = this->match(nextContext, siblingTraversalStrategy, result);
if (match == SelectorMatches || match == SelectorFailsCompletely)
return match;
if (nextSelectorExceedsScope(nextContext))
return SelectorFailsCompletely;
}
return SelectorFailsCompletely;
}
case CSSSelector::SubSelector:
ASSERT_NOT_REACHED();
}
ASSERT_NOT_REACHED();
return SelectorFailsCompletely;
}
template<typename SiblingTraversalStrategy>
SelectorChecker::Match SelectorChecker::matchForShadowDistributed(const Element* element, const SiblingTraversalStrategy& siblingTraversalStrategy, SelectorCheckingContext& nextContext, MatchResult* result) const
{
ASSERT(element);
WillBeHeapVector<RawPtrWillBeMember<InsertionPoint>, 8> insertionPoints;
const ContainerNode* scope = nextContext.scope;
BehaviorAtBoundary behaviorAtBoundary = nextContext.behaviorAtBoundary;
collectDestinationInsertionPoints(*element, insertionPoints);
for (size_t i = 0; i < insertionPoints.size(); ++i) {
nextContext.element = insertionPoints[i];
// If a given scope is a shadow host of an insertion point but behaviorAtBoundary doesn't have ScopeIsShadowRoot,
// we need to update behaviorAtBoundary to make selectors like ":host > ::content" work correctly.
if (m_mode == SharingRules) {
nextContext.behaviorAtBoundary = static_cast<BehaviorAtBoundary>(behaviorAtBoundary | ScopeIsShadowRoot);
nextContext.scope = insertionPoints[i]->containingShadowRoot();
} else if (scope == insertionPoints[i]->containingShadowRoot() && !(behaviorAtBoundary & ScopeIsShadowRoot)) {
nextContext.behaviorAtBoundary = static_cast<BehaviorAtBoundary>(behaviorAtBoundary | ScopeIsShadowRoot);
} else {
nextContext.behaviorAtBoundary = behaviorAtBoundary;
}
nextContext.isSubSelector = false;
nextContext.elementStyle = 0;
if (match(nextContext, siblingTraversalStrategy, result) == SelectorMatches)
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, bool caseSensitive)
{
const AtomicString& value = attributeItem.value();
if (value.isNull())
return false;
switch (match) {
case CSSSelector::Exact:
if (caseSensitive ? selectorValue != value : !equalIgnoringCase(selectorValue, value))
return false;
break;
case CSSSelector::List:
{
// 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, caseSensitive);
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;
}
break;
}
case CSSSelector::Contain:
if (!value.contains(selectorValue, caseSensitive) || selectorValue.isEmpty())
return false;
break;
case CSSSelector::Begin:
if (!value.startsWith(selectorValue, caseSensitive) || selectorValue.isEmpty())
return false;
break;
case CSSSelector::End:
if (!value.endsWith(selectorValue, caseSensitive) || selectorValue.isEmpty())
return false;
break;
case CSSSelector::Hyphen:
if (value.length() < selectorValue.length())
return false;
if (!value.startsWith(selectorValue, caseSensitive))
return false;
// It they start the same, check for exact match or following '-':
if (value.length() != selectorValue.length() && value[selectorValue.length()] != '-')
return false;
break;
case CSSSelector::PseudoClass:
case CSSSelector::PseudoElement:
default:
break;
}
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());
if (!element.hasAttributesWithoutUpdate())
return false;
const AtomicString& selectorValue = selector.value();
AttributeCollection attributes = element.attributes();
AttributeCollection::const_iterator end = attributes.end();
for (AttributeCollection::const_iterator it = attributes.begin(); it != end; ++it) {
const Attribute& attributeItem = *it;
if (!attributeItem.matches(selectorAttr))
continue;
if (attributeValueMatches(attributeItem, match, selectorValue, true))
return true;
// Case sensitivity for attribute matching is looser than hasAttribute or
// Element::shouldIgnoreAttributeCase() for now. Unclear if that's correct.
bool caseSensitive = !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 (!caseSensitive && attributeValueMatches(attributeItem, match, selectorValue, false)) {
UseCounter::count(element.document(), UseCounter::CaseInsensitiveAttrSelectorMatch);
return true;
}
}
return false;
}
template<typename SiblingTraversalStrategy>
bool SelectorChecker::checkOne(const SelectorCheckingContext& context, const SiblingTraversalStrategy& siblingTraversalStrategy, unsigned* specificity) const
{
ASSERT(context.element);
Element& element = *context.element;
ASSERT(context.selector);
const CSSSelector& selector = *context.selector;
bool elementIsHostInItsShadowTree = isHostInItsShadowTree(element, context.behaviorAtBoundary, context.scope);
// Only :host and :ancestor should match the host: http://drafts.csswg.org/css-scoping/#host-element
if (elementIsHostInItsShadowTree && !selector.isHostPseudoClass()
&& !(context.behaviorAtBoundary & TreatShadowHostAsNormalScope))
return false;
if (selector.match() == CSSSelector::Tag)
return SelectorChecker::tagMatches(element, selector.tagQName());
if (selector.match() == CSSSelector::Class)
return element.hasClass() && element.classNames().contains(selector.value());
if (selector.match() == CSSSelector::Id)
return element.hasID() && element.idForStyleResolution() == selector.value();
if (selector.isAttributeSelector())
return anyAttributeMatches(element, selector.match(), selector);
if (selector.match() == CSSSelector::PseudoClass) {
// Handle :not up front.
if (selector.pseudoType() == CSSSelector::PseudoNot) {
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, DOMSiblingTraversalStrategy()))
return true;
}
} else 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, &element.document(), selector);
} else if (context.hasSelectionPseudo) {
if (selector.pseudoType() == CSSSelector::PseudoWindowInactive)
return !element.document().page()->focusController().isActive();
}
// Normal element pseudo class checking.
switch (selector.pseudoType()) {
// Pseudo classes:
case CSSSelector::PseudoNot:
break; // Already handled up above.
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.elementStyle)
context.elementStyle->setEmptyState(result);
else if (element.renderStyle() && (element.document().styleEngine()->usesSiblingRules() || element.renderStyle()->unique()))
element.renderStyle()->setEmptyState(result);
}
return result;
}
case CSSSelector::PseudoFirstChild:
// first-child matches the first child that is an element
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
bool result = siblingTraversalStrategy.isFirstChild(element);
if (m_mode == ResolvingStyle) {
RenderStyle* childStyle = context.elementStyle ? context.elementStyle : element.renderStyle();
parent->setChildrenAffectedByFirstChildRules();
if (result && childStyle)
childStyle->setFirstChildState();
}
return result;
}
break;
case CSSSelector::PseudoFirstOfType:
// first-of-type matches the first element of its type
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
bool result = siblingTraversalStrategy.isFirstOfType(element, element.tagQName());
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByForwardPositionalRules();
return result;
}
break;
case CSSSelector::PseudoLastChild:
// last-child matches the last child that is an element
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
bool result = parent->isFinishedParsingChildren() && siblingTraversalStrategy.isLastChild(element);
if (m_mode == ResolvingStyle) {
RenderStyle* childStyle = context.elementStyle ? context.elementStyle : element.renderStyle();
parent->setChildrenAffectedByLastChildRules();
if (result && childStyle)
childStyle->setLastChildState();
}
return result;
}
break;
case CSSSelector::PseudoLastOfType:
// last-of-type matches the last element of its type
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByBackwardPositionalRules();
if (!parent->isFinishedParsingChildren())
return false;
return siblingTraversalStrategy.isLastOfType(element, element.tagQName());
}
break;
case CSSSelector::PseudoOnlyChild:
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
bool firstChild = siblingTraversalStrategy.isFirstChild(element);
bool onlyChild = firstChild && parent->isFinishedParsingChildren() && siblingTraversalStrategy.isLastChild(element);
if (m_mode == ResolvingStyle) {
RenderStyle* childStyle = context.elementStyle ? context.elementStyle : element.renderStyle();
parent->setChildrenAffectedByFirstChildRules();
parent->setChildrenAffectedByLastChildRules();
if (firstChild && childStyle)
childStyle->setFirstChildState();
if (onlyChild && childStyle)
childStyle->setLastChildState();
}
return onlyChild;
}
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 siblingTraversalStrategy.isFirstOfType(element, element.tagQName()) && siblingTraversalStrategy.isLastOfType(element, element.tagQName());
}
break;
case CSSSelector::PseudoNthChild:
if (!selector.parseNth())
break;
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
int count = 1 + siblingTraversalStrategy.countElementsBefore(element);
if (m_mode == ResolvingStyle) {
RenderStyle* childStyle = context.elementStyle ? context.elementStyle : element.renderStyle();
if (childStyle)
childStyle->setUnique();
parent->setChildrenAffectedByForwardPositionalRules();
}
if (selector.matchNth(count))
return true;
}
break;
case CSSSelector::PseudoNthOfType:
if (!selector.parseNth())
break;
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
int count = 1 + siblingTraversalStrategy.countElementsOfTypeBefore(element, element.tagQName());
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByForwardPositionalRules();
if (selector.matchNth(count))
return true;
}
break;
case CSSSelector::PseudoNthLastChild:
if (!selector.parseNth())
break;
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByBackwardPositionalRules();
if (!parent->isFinishedParsingChildren())
return false;
int count = 1 + siblingTraversalStrategy.countElementsAfter(element);
if (selector.matchNth(count))
return true;
}
break;
case CSSSelector::PseudoNthLastOfType:
if (!selector.parseNth())
break;
if (ContainerNode* parent = element.parentElementOrDocumentFragment()) {
if (m_mode == ResolvingStyle)
parent->setChildrenAffectedByBackwardPositionalRules();
if (!parent->isFinishedParsingChildren())
return false;
int count = 1 + siblingTraversalStrategy.countElementsOfTypeAfter(element, element.tagQName());
if (selector.matchNth(count))
return true;
}
break;
case CSSSelector::PseudoTarget:
if (element == element.document().cssTarget())
return true;
break;
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, siblingTraversalStrategy) == SelectorMatches)
return true;
}
}
break;
case CSSSelector::PseudoAutofill:
if (!element.isFormControlElement())
break;
return toHTMLFormControlElement(element).isAutofilled();
case CSSSelector::PseudoAnyLink:
case CSSSelector::PseudoLink:
// :visited and :link matches are separated later when applying the style. Here both classes match all links...
return element.isLink();
case CSSSelector::PseudoVisited:
// ...except if :visited matching is disabled for ancestor/sibling matching.
return element.isLink() && context.visitedMatchType == VisitedMatchEnabled;
case CSSSelector::PseudoDrag:
if (m_mode == ResolvingStyle) {
if (context.elementStyle)
context.elementStyle->setAffectedByDrag();
else
element.setChildrenOrSiblingsAffectedByDrag();
}
if (element.renderer() && element.renderer()->isDragging())
return true;
break;
case CSSSelector::PseudoFocus:
if (m_mode == ResolvingStyle) {
if (context.elementStyle)
context.elementStyle->setAffectedByFocus();
else
element.setChildrenOrSiblingsAffectedByFocus();
}
return matchesFocusPseudoClass(element);
case CSSSelector::PseudoHover:
// If we're in quirks mode, then hover should never match anchors with no
// href and *:hover should not match anything. This is important for sites like wsj.com.
if (m_strictParsing || context.isSubSelector || element.isLink()) {
if (m_mode == ResolvingStyle) {
if (context.elementStyle)
context.elementStyle->setAffectedByHover();
else
element.setChildrenOrSiblingsAffectedByHover();
}
if (element.hovered() || InspectorInstrumentation::forcePseudoState(&element, CSSSelector::PseudoHover))
return true;
}
break;
case CSSSelector::PseudoActive:
// If we're in quirks mode, then :active should never match anchors with no
// href and *:active should not match anything.
if (m_strictParsing || context.isSubSelector || element.isLink()) {
if (m_mode == ResolvingStyle) {
if (context.elementStyle)
context.elementStyle->setAffectedByActive();
else
element.setChildrenOrSiblingsAffectedByActive();
}
if (element.active() || InspectorInstrumentation::forcePseudoState(&element, CSSSelector::PseudoActive))
return true;
}
break;
case CSSSelector::PseudoEnabled:
if (element.isFormControlElement() || isHTMLOptionElement(element) || isHTMLOptGroupElement(element))
return !element.isDisabledFormControl();
break;
case CSSSelector::PseudoFullPageMedia:
return element.document().isMediaDocument();
break;
case CSSSelector::PseudoDefault:
return element.isDefaultButtonForForm();
case CSSSelector::PseudoDisabled:
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:
element.document().setContainsValidityStyleRules();
return element.willValidate() && element.isValidFormControlElement();
case CSSSelector::PseudoInvalid:
element.document().setContainsValidityStyleRules();
return element.willValidate() && !element.isValidFormControlElement();
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:
if (element == element.document().documentElement())
return true;
break;
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, false))
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;
if (FullscreenElementStack* fullscreen = FullscreenElementStack::fromIfExists(element.document())) {
if (!fullscreen->webkitIsFullScreen())
return false;
return element == fullscreen->webkitCurrentFullScreenElement();
}
return false;
case CSSSelector::PseudoFullScreenAncestor:
return element.containsFullScreenElement();
case CSSSelector::PseudoFullScreenDocument:
// While a Document is in the fullscreen state, the 'full-screen-document' pseudoclass applies
// to all elements of that Document.
if (!FullscreenElementStack::isFullScreen(element.document()))
return false;
return true;
case CSSSelector::PseudoInRange:
element.document().setContainsValidityStyleRules();
return element.isInRange();
case CSSSelector::PseudoOutOfRange:
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;
const Node* contextualReferenceNode = !context.scope ? element.document().documentElement() : context.scope;
if (element == contextualReferenceNode)
return true;
break;
}
case CSSSelector::PseudoUnresolved:
if (element.isUnresolvedCustomElement())
return true;
break;
case CSSSelector::PseudoHost:
case CSSSelector::PseudoHostContext:
{
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.behaviorAtBoundary = ScopeIsShadowHostInPseudoHostParameter;
subContext.scope = context.scope;
// Use NodeRenderingTraversal 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, siblingTraversalStrategy, &subResult) == SelectorMatches) {
matched = true;
// Consider div:host(div:host(div:host(div:host...))).
maxSpecificity = std::max(maxSpecificity, hostContext.selector->specificity() + subResult.specificity);
break;
}
hostContext.behaviorAtBoundary = DoesNotCrossBoundary;
hostContext.scope = 0;
if (selector.pseudoType() == CSSSelector::PseudoHost)
break;
hostContext.elementStyle = 0;
nextElement = NodeRenderingTraversal::parentElement(nextElement);
} while (nextElement);
}
if (matched) {
if (specificity)
*specificity = maxSpecificity;
return true;
}
}
break;
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:
case CSSSelector::PseudoNotParsed:
default:
ASSERT_NOT_REACHED();
break;
}
return false;
} else if (selector.match() == CSSSelector::PseudoElement && selector.pseudoType() == CSSSelector::PseudoCue) {
SelectorCheckingContext subContext(context);
subContext.isSubSelector = true;
subContext.behaviorAtBoundary = StaysWithinTreeScope;
const CSSSelector* contextSelector = context.selector;
ASSERT(contextSelector);
for (subContext.selector = contextSelector->selectorList()->first(); subContext.selector; subContext.selector = CSSSelectorList::next(*subContext.selector)) {
if (match(subContext, siblingTraversalStrategy) == SelectorMatches)
return true;
}
return false;
}
// ### add the rest of the checks...
return true;
}
bool SelectorChecker::checkScrollbarPseudoClass(const SelectorCheckingContext& context, Document* document, const CSSSelector& selector) const
{
RenderScrollbar* scrollbar = context.scrollbar;
ScrollbarPart part = context.scrollbarPart;
// 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 !document->page()->focusController().isActive();
if (!scrollbar)
return false;
ASSERT(selector.match() == CSSSelector::PseudoClass);
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:
{
ScrollbarButtonsPlacement buttonsPlacement = scrollbar->theme()->buttonsPlacement();
if (part == BackButtonStartPart || part == ForwardButtonStartPart || part == BackTrackPart)
return buttonsPlacement == ScrollbarButtonsDoubleStart || buttonsPlacement == ScrollbarButtonsDoubleBoth;
if (part == BackButtonEndPart || part == ForwardButtonEndPart || part == ForwardTrackPart)
return buttonsPlacement == ScrollbarButtonsDoubleEnd || buttonsPlacement == ScrollbarButtonsDoubleBoth;
return false;
}
case CSSSelector::PseudoSingleButton:
{
ScrollbarButtonsPlacement buttonsPlacement = scrollbar->theme()->buttonsPlacement();
if (part == BackButtonStartPart || part == ForwardButtonEndPart || part == BackTrackPart || part == ForwardTrackPart)
return buttonsPlacement == ScrollbarButtonsSingle;
return false;
}
case CSSSelector::PseudoNoButton:
{
ScrollbarButtonsPlacement buttonsPlacement = scrollbar->theme()->buttonsPlacement();
if (part == BackTrackPart)
return buttonsPlacement == ScrollbarButtonsNone || buttonsPlacement == ScrollbarButtonsDoubleEnd;
if (part == ForwardTrackPart)
return buttonsPlacement == ScrollbarButtonsNone || buttonsPlacement == ScrollbarButtonsDoubleStart;
return false;
}
case CSSSelector::PseudoCornerPresent:
return scrollbar->scrollableArea()->isScrollCornerVisible();
default:
return false;
}
}
unsigned SelectorChecker::determineLinkMatchType(const CSSSelector& selector)
{
unsigned linkMatchType = MatchAll;
// Statically determine if this selector will match a link in visited, unvisited or any state, or never.
// :visited never matches other elements than the innermost link element.
for (const CSSSelector* current = &selector; current; current = current->tagHistory()) {
switch (current->pseudoType()) {
case CSSSelector::PseudoNot:
{
// :not(:visited) is equivalent to :link. Parser enforces that :not can't nest.
ASSERT(current->selectorList());
for (const CSSSelector* subSelector = current->selectorList()->first(); subSelector; subSelector = subSelector->tagHistory()) {
CSSSelector::PseudoType subType = subSelector->pseudoType();
if (subType == CSSSelector::PseudoVisited)
linkMatchType &= ~SelectorChecker::MatchVisited;
else if (subType == CSSSelector::PseudoLink)
linkMatchType &= ~SelectorChecker::MatchLink;
}
}
break;
case CSSSelector::PseudoLink:
linkMatchType &= ~SelectorChecker::MatchVisited;
break;
case CSSSelector::PseudoVisited:
linkMatchType &= ~SelectorChecker::MatchLink;
break;
default:
// We don't support :link and :visited inside :-webkit-any.
break;
}
CSSSelector::Relation relation = current->relation();
if (relation == CSSSelector::SubSelector)
continue;
if (relation != CSSSelector::Descendant && relation != CSSSelector::Child)
return linkMatchType;
if (linkMatchType != MatchAll)
return linkMatchType;
}
return linkMatchType;
}
bool SelectorChecker::isFrameFocused(const Element& element)
{
return element.document().frame() && element.document().frame()->selection().isFocusedAndActive();
}
bool SelectorChecker::matchesFocusPseudoClass(const Element& element)
{
if (InspectorInstrumentation::forcePseudoState(const_cast<Element*>(&element), CSSSelector::PseudoFocus))
return true;
return element.focused() && isFrameFocused(element);
}
template
SelectorChecker::Match SelectorChecker::match(const SelectorCheckingContext&, const DOMSiblingTraversalStrategy&, MatchResult*) const;
template
SelectorChecker::Match SelectorChecker::match(const SelectorCheckingContext&, const ShadowDOMSiblingTraversalStrategy&, MatchResult*) const;
}