blob: 5ba131b588598f5a27ba38aa64a07349411ac9aa [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Copyright (C) 2016-2020 Apple Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "config.h"
#include "CSSSelectorParser.h"
#include "RuntimeEnabledFeatures.h"
#include <memory>
#include <wtf/OptionSet.h>
#include <wtf/SetForScope.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/StringToIntegerConversion.h>
namespace WebCore {
static AtomString serializeANPlusB(const std::pair<int, int>&);
static bool consumeANPlusB(CSSParserTokenRange&, std::pair<int, int>&);
std::optional<CSSSelectorList> parseCSSSelector(CSSParserTokenRange range, const CSSParserContext& context, StyleSheetContents* styleSheet)
{
CSSSelectorParser parser(context, styleSheet);
range.consumeWhitespace();
CSSSelectorList result = parser.consumeComplexSelectorList(range);
if (result.isEmpty() || !range.atEnd())
return { };
return result;
}
CSSSelectorParser::CSSSelectorParser(const CSSParserContext& context, StyleSheetContents* styleSheet)
: m_context(context)
, m_styleSheet(styleSheet)
{
}
CSSSelectorList CSSSelectorParser::consumeComplexSelectorList(CSSParserTokenRange& range)
{
Vector<std::unique_ptr<CSSParserSelector>> selectorList;
auto selector = consumeComplexSelector(range);
if (!selector)
return { };
selectorList.append(WTFMove(selector));
while (!range.atEnd() && range.peek().type() == CommaToken) {
range.consumeIncludingWhitespace();
selector = consumeComplexSelector(range);
if (!selector)
return { };
selectorList.append(WTFMove(selector));
}
if (m_failedParsing)
return { };
return CSSSelectorList { WTFMove(selectorList) };
}
template<typename ConsumeSelector>
CSSSelectorList CSSSelectorParser::consumeForgivingSelectorList(CSSParserTokenRange& range, ConsumeSelector&& consumeSelector)
{
if (m_failedParsing)
return { };
Vector<std::unique_ptr<CSSParserSelector>> selectorList;
auto consumeForgiving = [&] {
auto selector = consumeSelector(range);
if (m_failedParsing) {
selector = { };
m_failedParsing = false;
}
if (!range.atEnd() && range.peek().type() != CommaToken) {
while (!range.atEnd() && range.peek().type() != CommaToken)
range.consume();
return;
}
if (selector)
selectorList.append(WTFMove(selector));
};
consumeForgiving();
while (!range.atEnd() && range.peek().type() == CommaToken) {
range.consumeIncludingWhitespace();
consumeForgiving();
}
if (selectorList.isEmpty())
return { };
return CSSSelectorList { WTFMove(selectorList) };
}
CSSSelectorList CSSSelectorParser::consumeForgivingComplexSelectorList(CSSParserTokenRange& range)
{
return consumeForgivingSelectorList(range, [&](CSSParserTokenRange& range) {
return consumeComplexSelector(range);
});
}
CSSSelectorList CSSSelectorParser::consumeForgivingRelativeSelectorList(CSSParserTokenRange& range)
{
return consumeForgivingSelectorList(range, [&](CSSParserTokenRange& range) {
return consumeRelativeSelector(range);
});
}
bool CSSSelectorParser::supportsComplexSelector(CSSParserTokenRange range, const CSSParserContext& context)
{
range.consumeWhitespace();
CSSSelectorParser parser(context, nullptr);
// FIXME: https://bugs.webkit.org/show_bug.cgi?id=215635
// Unknown css selector combinator is not addressed correctly in |CSSSelectorParser::consumeComplexSelector|.
auto parserSelector = parser.consumeComplexSelector(range);
if (parser.m_failedParsing || !range.atEnd() || !parserSelector)
return false;
auto complexSelector = parserSelector->releaseSelector();
ASSERT(complexSelector);
return !containsUnknownWebKitPseudoElements(*complexSelector);
}
CSSSelectorList CSSSelectorParser::consumeCompoundSelectorList(CSSParserTokenRange& range)
{
Vector<std::unique_ptr<CSSParserSelector>> selectorList;
auto selector = consumeCompoundSelector(range);
range.consumeWhitespace();
if (!selector)
return CSSSelectorList();
selectorList.append(WTFMove(selector));
while (!range.atEnd() && range.peek().type() == CommaToken) {
range.consumeIncludingWhitespace();
selector = consumeCompoundSelector(range);
range.consumeWhitespace();
if (!selector)
return CSSSelectorList();
selectorList.append(WTFMove(selector));
}
return CSSSelectorList { WTFMove(selectorList) };
}
static bool consumeLangArgumentList(std::unique_ptr<Vector<AtomString>>& argumentList, CSSParserTokenRange& range)
{
const CSSParserToken& ident = range.consumeIncludingWhitespace();
if (ident.type() != IdentToken && ident.type() != StringToken)
return false;
StringView string = ident.value();
if (string.startsWith("--"))
return false;
argumentList->append(string.toAtomString());
while (!range.atEnd() && range.peek().type() == CommaToken) {
range.consumeIncludingWhitespace();
const CSSParserToken& ident = range.consumeIncludingWhitespace();
if (ident.type() != IdentToken && ident.type() != StringToken)
return false;
StringView string = ident.value();
if (string.startsWith("--"))
return false;
argumentList->append(string.toAtomString());
}
return range.atEnd();
}
enum class CompoundSelectorFlag {
HasPseudoElementForRightmostCompound = 1 << 0,
};
static OptionSet<CompoundSelectorFlag> extractCompoundFlags(const CSSParserSelector& simpleSelector, CSSParserMode parserMode)
{
if (simpleSelector.match() != CSSSelector::PseudoElement)
return { };
// FIXME: https://bugs.webkit.org/show_bug.cgi?id=161747
// The UASheetMode check is a work-around to allow this selector in mediaControls(New).css:
// input[type="range" i]::-webkit-media-slider-container > div {
if (parserMode == UASheetMode && simpleSelector.pseudoElementType() == CSSSelector::PseudoElementWebKitCustom)
return { };
return CompoundSelectorFlag::HasPseudoElementForRightmostCompound;
}
static bool isDescendantCombinator(CSSSelector::RelationType relation)
{
return relation == CSSSelector::DescendantSpace;
}
std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeComplexSelector(CSSParserTokenRange& range)
{
auto selector = consumeCompoundSelector(range);
if (!selector)
return nullptr;
OptionSet<CompoundSelectorFlag> previousCompoundFlags;
for (CSSParserSelector* simple = selector.get(); simple && !previousCompoundFlags; simple = simple->tagHistory())
previousCompoundFlags = extractCompoundFlags(*simple, m_context.mode);
while (auto combinator = consumeCombinator(range)) {
auto nextSelector = consumeCompoundSelector(range);
if (!nextSelector)
return isDescendantCombinator(combinator) ? WTFMove(selector) : nullptr;
if (previousCompoundFlags.contains(CompoundSelectorFlag::HasPseudoElementForRightmostCompound))
return nullptr;
CSSParserSelector* end = nextSelector.get();
auto compoundFlags = extractCompoundFlags(*end, m_context.mode);
while (end->tagHistory()) {
end = end->tagHistory();
compoundFlags.add(extractCompoundFlags(*end, m_context.mode));
}
end->setRelation(combinator);
previousCompoundFlags = compoundFlags;
end->setTagHistory(WTFMove(selector));
selector = WTFMove(nextSelector);
}
return selector;
}
std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeRelativeSelector(CSSParserTokenRange& range)
{
auto scopeCombinator = consumeCombinator(range);
if (scopeCombinator == CSSSelector::Subselector)
scopeCombinator = CSSSelector::DescendantSpace;
auto selector = consumeComplexSelector(range);
if (!selector)
return nullptr;
auto hasScopePseudoClass = [](auto& selector) {
return selector.match() == CSSSelector::PseudoClass && selector.pseudoClassType() == CSSSelector::PseudoClassScope;
};
bool hasExplicitScope = hasScopePseudoClass(*selector);
auto* end = selector.get();
while (end->tagHistory()) {
end = end->tagHistory();
if (hasScopePseudoClass(*end))
hasExplicitScope = true;
}
// If the selector doesn't have an explicit :scope on the left, add an implicit one.
if (!hasExplicitScope || scopeCombinator != CSSSelector::DescendantSpace) {
auto scopeSelector = makeUnique<CSSParserSelector>();
scopeSelector->setMatch(CSSSelector::PseudoClass);
scopeSelector->setPseudoClassType(CSSSelector::PseudoClassRelativeScope);
end->setRelation(scopeCombinator);
end->setTagHistory(WTFMove(scopeSelector));
}
return selector;
}
static bool isScrollbarPseudoClass(CSSSelector::PseudoClassType pseudo)
{
switch (pseudo) {
case CSSSelector::PseudoClassEnabled:
case CSSSelector::PseudoClassDisabled:
case CSSSelector::PseudoClassHover:
case CSSSelector::PseudoClassActive:
case CSSSelector::PseudoClassHorizontal:
case CSSSelector::PseudoClassVertical:
case CSSSelector::PseudoClassDecrement:
case CSSSelector::PseudoClassIncrement:
case CSSSelector::PseudoClassStart:
case CSSSelector::PseudoClassEnd:
case CSSSelector::PseudoClassDoubleButton:
case CSSSelector::PseudoClassSingleButton:
case CSSSelector::PseudoClassNoButton:
case CSSSelector::PseudoClassCornerPresent:
case CSSSelector::PseudoClassWindowInactive:
return true;
default:
return false;
}
}
static bool isUserActionPseudoClass(CSSSelector::PseudoClassType pseudo)
{
switch (pseudo) {
case CSSSelector::PseudoClassHover:
case CSSSelector::PseudoClassFocus:
case CSSSelector::PseudoClassActive:
case CSSSelector::PseudoClassFocusVisible:
case CSSSelector::PseudoClassFocusWithin:
return true;
default:
return false;
}
}
static bool isLogicalCombinationPseudoClass(CSSSelector::PseudoClassType pseudo)
{
switch (pseudo) {
case CSSSelector::PseudoClassIs:
case CSSSelector::PseudoClassWhere:
case CSSSelector::PseudoClassNot:
case CSSSelector::PseudoClassAny:
case CSSSelector::PseudoClassMatches:
case CSSSelector::PseudoClassHas:
return true;
default:
return false;
}
}
static bool isPseudoClassValidAfterPseudoElement(CSSSelector::PseudoClassType pseudoClass, CSSSelector::PseudoElementType compoundPseudoElement)
{
// Validity of these is determined by their content.
if (isLogicalCombinationPseudoClass(pseudoClass))
return true;
switch (compoundPseudoElement) {
case CSSSelector::PseudoElementPart:
return !isTreeStructuralPseudoClass(pseudoClass);
case CSSSelector::PseudoElementSlotted:
return false;
case CSSSelector::PseudoElementResizer:
case CSSSelector::PseudoElementScrollbar:
case CSSSelector::PseudoElementScrollbarCorner:
case CSSSelector::PseudoElementScrollbarButton:
case CSSSelector::PseudoElementScrollbarThumb:
case CSSSelector::PseudoElementScrollbarTrack:
case CSSSelector::PseudoElementScrollbarTrackPiece:
return isScrollbarPseudoClass(pseudoClass);
case CSSSelector::PseudoElementSelection:
return pseudoClass == CSSSelector::PseudoClassWindowInactive;
case CSSSelector::PseudoElementWebKitCustom:
case CSSSelector::PseudoElementWebKitCustomLegacyPrefixed:
return isUserActionPseudoClass(pseudoClass);
default:
return false;
}
}
static bool isTreeAbidingPseudoElement(CSSSelector::PseudoElementType pseudoElementType)
{
switch (pseudoElementType) {
// FIXME: This list should also include ::placeholder and ::file-selector-button
case CSSSelector::PseudoElementBefore:
case CSSSelector::PseudoElementAfter:
case CSSSelector::PseudoElementMarker:
return true;
default:
return false;
}
}
static bool isSimpleSelectorValidAfterPseudoElement(const CSSParserSelector& simpleSelector, CSSSelector::PseudoElementType compoundPseudoElement)
{
ASSERT(compoundPseudoElement != CSSSelector::PseudoElementUnknown);
if (compoundPseudoElement == CSSSelector::PseudoElementPart) {
if (simpleSelector.match() == CSSSelector::PseudoElement && simpleSelector.pseudoElementType() != CSSSelector::PseudoElementPart)
return true;
}
if (compoundPseudoElement == CSSSelector::PseudoElementSlotted) {
if (simpleSelector.match() == CSSSelector::PseudoElement && isTreeAbidingPseudoElement(simpleSelector.pseudoElementType()))
return true;
}
if (simpleSelector.match() != CSSSelector::PseudoClass)
return false;
return isPseudoClassValidAfterPseudoElement(simpleSelector.pseudoClassType(), compoundPseudoElement);
}
static bool atEndIgnoringWhitespace(CSSParserTokenRange range)
{
range.consumeWhitespace();
return range.atEnd();
}
std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeCompoundSelector(CSSParserTokenRange& range)
{
ASSERT(!m_precedingPseudoElement || m_disallowPseudoElements);
std::unique_ptr<CSSParserSelector> compoundSelector;
AtomString namespacePrefix;
AtomString elementName;
const bool hasName = consumeName(range, elementName, namespacePrefix);
if (!hasName) {
compoundSelector = consumeSimpleSelector(range);
if (!compoundSelector)
return nullptr;
if (compoundSelector->match() == CSSSelector::PseudoElement)
m_precedingPseudoElement = compoundSelector->pseudoElementType();
}
while (auto simpleSelector = consumeSimpleSelector(range)) {
if (simpleSelector->match() == CSSSelector::PseudoElement)
m_precedingPseudoElement = simpleSelector->pseudoElementType();
if (compoundSelector)
compoundSelector->appendTagHistory(CSSSelector::Subselector, WTFMove(simpleSelector));
else
compoundSelector = WTFMove(simpleSelector);
}
if (!m_disallowPseudoElements)
m_precedingPseudoElement = { };
// While inside a nested selector like :is(), the default namespace shall be ignored when [1]:
// * The compound selector represents the subject [2], and
// * The compound selector does not contain a type/universal selector.
//
// [1] https://drafts.csswg.org/selectors/#matches
// [2] https://drafts.csswg.org/selectors/#selector-subject
SetForScope<bool> ignoreDefaultNamespace(m_ignoreDefaultNamespace, m_resistDefaultNamespace && !hasName && atEndIgnoringWhitespace(range));
if (!compoundSelector) {
AtomString namespaceURI = determineNamespace(namespacePrefix);
if (namespaceURI.isNull()) {
m_failedParsing = true;
return nullptr;
}
if (namespaceURI == defaultNamespace())
namespacePrefix = nullAtom();
return makeUnique<CSSParserSelector>(QualifiedName(namespacePrefix, elementName, namespaceURI));
}
prependTypeSelectorIfNeeded(namespacePrefix, elementName, *compoundSelector);
return splitCompoundAtImplicitShadowCrossingCombinator(WTFMove(compoundSelector), m_context);
}
std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeSimpleSelector(CSSParserTokenRange& range)
{
const CSSParserToken& token = range.peek();
std::unique_ptr<CSSParserSelector> selector;
if (token.type() == HashToken)
selector = consumeId(range);
else if (token.type() == DelimiterToken && token.delimiter() == '.')
selector = consumeClass(range);
else if (token.type() == LeftBracketToken)
selector = consumeAttribute(range);
else if (token.type() == ColonToken)
selector = consumePseudo(range);
else
return nullptr;
if (!selector) {
m_failedParsing = true;
return nullptr;
}
if (m_precedingPseudoElement) {
// FIXME: https://bugs.webkit.org/show_bug.cgi?id=161747
// The UASheetMode check is a work-around to allow this selector in mediaControls(New).css:
// video::-webkit-media-text-track-region-container.scrolling
if (m_context.mode != UASheetMode && !isSimpleSelectorValidAfterPseudoElement(*selector, *m_precedingPseudoElement))
m_failedParsing = true;
}
return selector;
}
bool CSSSelectorParser::consumeName(CSSParserTokenRange& range, AtomString& name, AtomString& namespacePrefix)
{
name = nullAtom();
namespacePrefix = nullAtom();
const CSSParserToken& firstToken = range.peek();
if (firstToken.type() == IdentToken) {
name = firstToken.value().toAtomString();
range.consume();
} else if (firstToken.type() == DelimiterToken && firstToken.delimiter() == '*') {
name = starAtom();
range.consume();
} else if (firstToken.type() == DelimiterToken && firstToken.delimiter() == '|') {
// This is an empty namespace, which'll get assigned this value below
name = emptyAtom();
} else
return false;
if (range.peek().type() != DelimiterToken || range.peek().delimiter() != '|')
return true;
range.consume();
namespacePrefix = name;
const CSSParserToken& nameToken = range.consume();
if (nameToken.type() == IdentToken) {
name = nameToken.value().toAtomString();
} else if (nameToken.type() == DelimiterToken && nameToken.delimiter() == '*')
name = starAtom();
else {
name = nullAtom();
namespacePrefix = nullAtom();
return false;
}
return true;
}
std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeId(CSSParserTokenRange& range)
{
ASSERT(range.peek().type() == HashToken);
if (range.peek().getHashTokenType() != HashTokenId)
return nullptr;
auto selector = makeUnique<CSSParserSelector>();
selector->setMatch(CSSSelector::Id);
// FIXME-NEWPARSER: Avoid having to do this, but the old parser does and we need
// to be compatible for now.
CSSParserToken token = range.consume();
selector->setValue(token.value().toAtomString(), m_context.mode == HTMLQuirksMode);
return selector;
}
std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeClass(CSSParserTokenRange& range)
{
ASSERT(range.peek().type() == DelimiterToken);
ASSERT(range.peek().delimiter() == '.');
range.consume();
if (range.peek().type() != IdentToken)
return nullptr;
auto selector = makeUnique<CSSParserSelector>();
selector->setMatch(CSSSelector::Class);
// FIXME-NEWPARSER: Avoid having to do this, but the old parser does and we need
// to be compatible for now.
CSSParserToken token = range.consume();
selector->setValue(token.value().toAtomString(), m_context.mode == HTMLQuirksMode);
return selector;
}
std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeAttribute(CSSParserTokenRange& range)
{
ASSERT(range.peek().type() == LeftBracketToken);
CSSParserTokenRange block = range.consumeBlock();
block.consumeWhitespace();
AtomString namespacePrefix;
AtomString attributeName;
if (!consumeName(block, attributeName, namespacePrefix))
return nullptr;
block.consumeWhitespace();
AtomString namespaceURI = determineNamespace(namespacePrefix);
if (namespaceURI.isNull())
return nullptr;
QualifiedName qualifiedName = namespacePrefix.isNull()
? QualifiedName(nullAtom(), attributeName, nullAtom())
: QualifiedName(namespacePrefix, attributeName, namespaceURI);
auto selector = makeUnique<CSSParserSelector>();
if (block.atEnd()) {
selector->setAttribute(qualifiedName, m_context.isHTMLDocument, CSSSelector::CaseSensitive);
selector->setMatch(CSSSelector::Set);
return selector;
}
selector->setMatch(consumeAttributeMatch(block));
const CSSParserToken& attributeValue = block.consumeIncludingWhitespace();
if (attributeValue.type() != IdentToken && attributeValue.type() != StringToken)
return nullptr;
selector->setValue(attributeValue.value().toAtomString());
selector->setAttribute(qualifiedName, m_context.isHTMLDocument, consumeAttributeFlags(block));
if (!block.atEnd())
return nullptr;
return selector;
}
static bool isOnlyPseudoClassFunction(CSSSelector::PseudoClassType pseudoClassType)
{
switch (pseudoClassType) {
case CSSSelector::PseudoClassNot:
case CSSSelector::PseudoClassIs:
case CSSSelector::PseudoClassMatches:
case CSSSelector::PseudoClassWhere:
case CSSSelector::PseudoClassNthChild:
case CSSSelector::PseudoClassNthLastChild:
case CSSSelector::PseudoClassNthOfType:
case CSSSelector::PseudoClassNthLastOfType:
case CSSSelector::PseudoClassLang:
case CSSSelector::PseudoClassAny:
#if ENABLE(CSS_SELECTORS_LEVEL4)
case CSSSelector::PseudoClassDir:
case CSSSelector::PseudoClassRole:
#endif
return true;
default:
break;
}
return false;
}
static bool isOnlyPseudoElementFunction(CSSSelector::PseudoElementType pseudoElementType)
{
// Note that we omit cue since it can be either an ident or a function.
switch (pseudoElementType) {
case CSSSelector::PseudoElementPart:
case CSSSelector::PseudoElementSlotted:
return true;
default:
break;
}
return false;
}
std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumePseudo(CSSParserTokenRange& range)
{
ASSERT(range.peek().type() == ColonToken);
range.consume();
int colons = 1;
if (range.peek().type() == ColonToken) {
range.consume();
colons++;
}
const CSSParserToken& token = range.peek();
if (token.type() != IdentToken && token.type() != FunctionToken)
return nullptr;
std::unique_ptr<CSSParserSelector> selector;
if (colons == 1) {
selector = CSSParserSelector::parsePseudoClassSelector(token.value());
if (!selector)
return nullptr;
if (selector->match() == CSSSelector::PseudoClass) {
if (m_context.mode != UASheetMode && selector->pseudoClassType() == CSSSelector::PseudoClassDirectFocus)
return nullptr;
if (m_context.mode != UASheetMode && selector->pseudoClassType() == CSSSelector::PseudoClassModalDialog)
return nullptr;
if (!m_context.focusVisibleEnabled && selector->pseudoClassType() == CSSSelector::PseudoClassFocusVisible)
return nullptr;
if (!m_context.hasPseudoClassEnabled && selector->pseudoClassType() == CSSSelector::PseudoClassHas)
return nullptr;
#if ENABLE(ATTACHMENT_ELEMENT)
if (!m_context.attachmentEnabled && selector->pseudoClassType() == CSSSelector::PseudoClassHasAttachment)
return nullptr;
#endif
}
} else {
selector = CSSParserSelector::parsePseudoElementSelector(token.value());
#if ENABLE(VIDEO)
// Treat the ident version of cue as PseudoElementWebkitCustom.
if (token.type() == IdentToken && selector && selector->match() == CSSSelector::PseudoElement && selector->pseudoElementType() == CSSSelector::PseudoElementCue)
selector->setPseudoElementType(CSSSelector::PseudoElementWebKitCustom);
#endif
}
if (!selector)
return nullptr;
// Pseudo-elements are not allowed inside pseudo-classes or pseudo-elements.
if (selector->match() == CSSSelector::PseudoElement && m_disallowPseudoElements)
return nullptr;
SetForScope disallowPseudoElementsScope(m_disallowPseudoElements, true);
if (token.type() == IdentToken) {
range.consume();
if ((selector->match() == CSSSelector::PseudoElement && (selector->pseudoElementType() == CSSSelector::PseudoElementUnknown || isOnlyPseudoElementFunction(selector->pseudoElementType())))
|| (selector->match() == CSSSelector::PseudoClass && (selector->pseudoClassType() == CSSSelector::PseudoClassUnknown || isOnlyPseudoClassFunction(selector->pseudoClassType()))))
return nullptr;
return selector;
}
CSSParserTokenRange block = range.consumeBlock();
block.consumeWhitespace();
if (token.type() != FunctionToken)
return nullptr;
if (selector->match() == CSSSelector::PseudoClass) {
switch (selector->pseudoClassType()) {
case CSSSelector::PseudoClassNot: {
SetForScope<bool> resistDefaultNamespace(m_resistDefaultNamespace, true);
auto selectorList = makeUnique<CSSSelectorList>();
*selectorList = consumeComplexSelectorList(block);
if (!selectorList->first() || !block.atEnd())
return nullptr;
selector->setSelectorList(WTFMove(selectorList));
return selector;
}
case CSSSelector::PseudoClassNthChild:
case CSSSelector::PseudoClassNthLastChild:
case CSSSelector::PseudoClassNthOfType:
case CSSSelector::PseudoClassNthLastOfType: {
std::pair<int, int> ab;
if (!consumeANPlusB(block, ab))
return nullptr;
block.consumeWhitespace();
// FIXME: We should be able to do this lazily. See: https://bugs.webkit.org/show_bug.cgi?id=217149
selector->setArgument(serializeANPlusB(ab));
if (!block.atEnd()) {
if (block.peek().type() != IdentToken)
return nullptr;
const CSSParserToken& ident = block.consume();
if (!equalIgnoringASCIICase(ident.value(), "of"))
return nullptr;
if (block.peek().type() != WhitespaceToken)
return nullptr;
block.consumeWhitespace();
auto selectorList = makeUnique<CSSSelectorList>();
*selectorList = consumeComplexSelectorList(block);
if (selectorList->isEmpty() || !block.atEnd())
return nullptr;
selector->setSelectorList(WTFMove(selectorList));
}
selector->setNth(ab.first, ab.second);
return selector;
}
case CSSSelector::PseudoClassLang: {
// FIXME: CSS Selectors Level 4 allows :lang(*-foo)
auto argumentList = makeUnique<Vector<AtomString>>();
if (!consumeLangArgumentList(argumentList, block))
return nullptr;
selector->setArgumentList(WTFMove(argumentList));
return selector;
}
case CSSSelector::PseudoClassIs:
case CSSSelector::PseudoClassWhere:
case CSSSelector::PseudoClassMatches:
case CSSSelector::PseudoClassAny: {
SetForScope<bool> resistDefaultNamespace(m_resistDefaultNamespace, true);
auto selectorList = makeUnique<CSSSelectorList>();
*selectorList = consumeForgivingComplexSelectorList(block);
if (!block.atEnd())
return nullptr;
selector->setSelectorList(WTFMove(selectorList));
return selector;
}
case CSSSelector::PseudoClassHost: {
auto selectorList = makeUnique<CSSSelectorList>();
*selectorList = consumeCompoundSelectorList(block);
if (selectorList->isEmpty() || !block.atEnd())
return nullptr;
selector->setSelectorList(WTFMove(selectorList));
return selector;
}
case CSSSelector::PseudoClassHas: {
auto selectorList = makeUnique<CSSSelectorList>();
*selectorList = consumeForgivingRelativeSelectorList(block);
if (selectorList->isEmpty() || !block.atEnd())
return nullptr;
selector->setSelectorList(WTFMove(selectorList));
return selector;
}
#if ENABLE(CSS_SELECTORS_LEVEL4)
case CSSSelector::PseudoClassDir:
case CSSSelector::PseudoClassRole: {
const CSSParserToken& ident = block.consumeIncludingWhitespace();
if (ident.type() != IdentToken || !block.atEnd())
return nullptr;
selector->setArgument(ident.value().toAtomString());
return selector;
}
#endif
default:
break;
}
}
if (selector->match() == CSSSelector::PseudoElement) {
switch (selector->pseudoElementType()) {
#if ENABLE(VIDEO)
case CSSSelector::PseudoElementCue: {
auto selectorList = makeUnique<CSSSelectorList>();
*selectorList = consumeCompoundSelectorList(block);
if (selectorList->isEmpty() || !block.atEnd())
return nullptr;
selector->setSelectorList(WTFMove(selectorList));
return selector;
}
#endif
case CSSSelector::PseudoElementHighlight: {
auto& ident = block.consumeIncludingWhitespace();
if (ident.type() != IdentToken || !block.atEnd())
return nullptr;
auto argumentList = makeUnique<Vector<AtomString>>();
argumentList->append(ident.value().toAtomString());
selector->setArgumentList(WTFMove(argumentList));
return selector;
}
case CSSSelector::PseudoElementPart: {
auto argumentList = makeUnique<Vector<AtomString>>();
do {
auto& ident = block.consumeIncludingWhitespace();
if (ident.type() != IdentToken)
return nullptr;
argumentList->append(ident.value().toAtomString());
} while (!block.atEnd());
selector->setArgumentList(WTFMove(argumentList));
return selector;
}
case CSSSelector::PseudoElementSlotted: {
auto innerSelector = consumeCompoundSelector(block);
block.consumeWhitespace();
if (!innerSelector || !block.atEnd())
return nullptr;
selector->adoptSelectorVector(Vector<std::unique_ptr<CSSParserSelector>>::from(WTFMove(innerSelector)));
return selector;
}
default:
break;
}
}
return nullptr;
}
CSSSelector::RelationType CSSSelectorParser::consumeCombinator(CSSParserTokenRange& range)
{
auto fallbackResult = CSSSelector::Subselector;
while (range.peek().type() == WhitespaceToken) {
range.consume();
fallbackResult = CSSSelector::DescendantSpace;
}
if (range.peek().type() != DelimiterToken)
return fallbackResult;
UChar delimiter = range.peek().delimiter();
if (delimiter == '+' || delimiter == '~' || delimiter == '>') {
range.consumeIncludingWhitespace();
if (delimiter == '+')
return CSSSelector::DirectAdjacent;
if (delimiter == '~')
return CSSSelector::IndirectAdjacent;
return CSSSelector::Child;
}
return fallbackResult;
}
CSSSelector::Match CSSSelectorParser::consumeAttributeMatch(CSSParserTokenRange& range)
{
const CSSParserToken& token = range.consumeIncludingWhitespace();
switch (token.type()) {
case IncludeMatchToken:
return CSSSelector::List;
case DashMatchToken:
return CSSSelector::Hyphen;
case PrefixMatchToken:
return CSSSelector::Begin;
case SuffixMatchToken:
return CSSSelector::End;
case SubstringMatchToken:
return CSSSelector::Contain;
case DelimiterToken:
if (token.delimiter() == '=')
return CSSSelector::Exact;
FALLTHROUGH;
default:
m_failedParsing = true;
return CSSSelector::Exact;
}
}
CSSSelector::AttributeMatchType CSSSelectorParser::consumeAttributeFlags(CSSParserTokenRange& range)
{
if (range.peek().type() != IdentToken)
return CSSSelector::CaseSensitive;
const CSSParserToken& flag = range.consumeIncludingWhitespace();
if (equalIgnoringASCIICase(flag.value(), "i"))
return CSSSelector::CaseInsensitive;
m_failedParsing = true;
return CSSSelector::CaseSensitive;
}
// <an+b> token sequences have special serialization rules: https://www.w3.org/TR/css-syntax-3/#serializing-anb
static AtomString serializeANPlusB(const std::pair<int, int>& ab)
{
if (!ab.first)
return AtomString::number(ab.second);
StringBuilder builder;
if (ab.first == -1)
builder.append('-');
else if (ab.first != 1)
builder.append(ab.first);
builder.append('n');
if (ab.second) {
if (ab.second > 0)
builder.append('+');
builder.append(ab.second);
}
return builder.toAtomString();
}
static bool consumeANPlusB(CSSParserTokenRange& range, std::pair<int, int>& result)
{
const CSSParserToken& token = range.consume();
if (token.type() == NumberToken && token.numericValueType() == IntegerValueType) {
result = std::make_pair(0, static_cast<int>(token.numericValue()));
return true;
}
if (token.type() == IdentToken) {
if (equalIgnoringASCIICase(token.value(), "odd")) {
result = std::make_pair(2, 1);
return true;
}
if (equalIgnoringASCIICase(token.value(), "even")) {
result = std::make_pair(2, 0);
return true;
}
}
// The 'n' will end up as part of an ident or dimension. For a valid <an+b>,
// this will store a string of the form 'n', 'n-', or 'n-123'.
String nString;
if (token.type() == DelimiterToken && token.delimiter() == '+' && range.peek().type() == IdentToken) {
result.first = 1;
nString = range.consume().value().toString();
} else if (token.type() == DimensionToken && token.numericValueType() == IntegerValueType) {
result.first = token.numericValue();
nString = token.unitString().toString();
} else if (token.type() == IdentToken) {
if (token.value()[0] == '-') {
result.first = -1;
nString = token.value().substring(1).toString();
} else {
result.first = 1;
nString = token.value().toString();
}
}
range.consumeWhitespace();
if (nString.isEmpty() || !isASCIIAlphaCaselessEqual(nString[0], 'n'))
return false;
if (nString.length() > 1 && nString[1] != '-')
return false;
if (nString.length() > 2) {
auto parsedNumber = parseInteger<int>(StringView { nString }.substring(1));
result.second = parsedNumber.value_or(0);
return parsedNumber.has_value();
}
NumericSign sign = nString.length() == 1 ? NoSign : MinusSign;
if (sign == NoSign && range.peek().type() == DelimiterToken) {
char delimiterSign = range.consumeIncludingWhitespace().delimiter();
if (delimiterSign == '+')
sign = PlusSign;
else if (delimiterSign == '-')
sign = MinusSign;
else
return false;
}
if (sign == NoSign && range.peek().type() != NumberToken) {
result.second = 0;
return true;
}
const CSSParserToken& b = range.consume();
if (b.type() != NumberToken || b.numericValueType() != IntegerValueType)
return false;
if ((b.numericSign() == NoSign) == (sign == NoSign))
return false;
result.second = b.numericValue();
if (sign == MinusSign)
result.second = -result.second;
return true;
}
const AtomString& CSSSelectorParser::defaultNamespace() const
{
if (!m_styleSheet || m_ignoreDefaultNamespace)
return starAtom();
return m_styleSheet->defaultNamespace();
}
const AtomString& CSSSelectorParser::determineNamespace(const AtomString& prefix)
{
if (prefix.isNull())
return defaultNamespace();
if (prefix.isEmpty())
return emptyAtom(); // No namespace. If an element/attribute has a namespace, we won't match it.
if (prefix == starAtom())
return starAtom(); // We'll match any namespace.
if (!m_styleSheet)
return nullAtom(); // Cannot resolve prefix to namespace without a stylesheet, syntax error.
return m_styleSheet->namespaceURIFromPrefix(prefix);
}
void CSSSelectorParser::prependTypeSelectorIfNeeded(const AtomString& namespacePrefix, const AtomString& elementName, CSSParserSelector& compoundSelector)
{
bool isShadowDOM = compoundSelector.needsImplicitShadowCombinatorForMatching();
if (elementName.isNull() && defaultNamespace() == starAtom() && !isShadowDOM)
return;
AtomString determinedElementName = elementName.isNull() ? starAtom() : elementName;
AtomString namespaceURI = determineNamespace(namespacePrefix);
if (namespaceURI.isNull()) {
m_failedParsing = true;
return;
}
AtomString determinedPrefix = namespacePrefix;
if (namespaceURI == defaultNamespace())
determinedPrefix = nullAtom();
QualifiedName tag(determinedPrefix, determinedElementName, namespaceURI);
// *:host never matches, so we can't discard the *,
// otherwise we can't tell the difference between *:host and just :host.
//
// Also, selectors where we use a ShadowPseudo combinator between the
// element and the pseudo element for matching (custom pseudo elements,
// ::cue), we need a universal selector to set the combinator
// (relation) on in the cases where there are no simple selectors preceding
// the pseudo element.
bool explicitForHost = compoundSelector.isHostPseudoSelector() && !elementName.isNull();
if (tag != anyQName() || explicitForHost || isShadowDOM)
compoundSelector.prependTagSelector(tag, determinedPrefix == nullAtom() && determinedElementName == starAtom() && !explicitForHost);
}
std::unique_ptr<CSSParserSelector> CSSSelectorParser::splitCompoundAtImplicitShadowCrossingCombinator(std::unique_ptr<CSSParserSelector> compoundSelector, const CSSParserContext& context)
{
// The tagHistory is a linked list that stores combinator separated compound selectors
// from right-to-left. Yet, within a single compound selector, stores the simple selectors
// from left-to-right.
//
// ".a.b > div#id" is stored in a tagHistory as [div, #id, .a, .b], each element in the
// list stored with an associated relation (combinator or Subselector).
//
// ::cue, ::shadow, and custom pseudo elements have an implicit ShadowPseudo combinator
// to their left, which really makes for a new compound selector, yet it's consumed by
// the selector parser as a single compound selector.
//
// Example: input#x::-webkit-inner-spin-button -> [ ::-webkit-inner-spin-button, input, #x ]
//
CSSParserSelector* splitAfter = compoundSelector.get();
while (splitAfter->tagHistory() && !splitAfter->tagHistory()->needsImplicitShadowCombinatorForMatching())
splitAfter = splitAfter->tagHistory();
if (!splitAfter || !splitAfter->tagHistory())
return compoundSelector;
// ::part() combines with other pseudo elements.
bool isPart = splitAfter->tagHistory()->match() == CSSSelector::PseudoElement && splitAfter->tagHistory()->pseudoElementType() == CSSSelector::PseudoElementPart;
// ::slotted() combines with other pseudo elements.
bool isSlotted = splitAfter->tagHistory()->match() == CSSSelector::PseudoElement && splitAfter->tagHistory()->pseudoElementType() == CSSSelector::PseudoElementSlotted;
std::unique_ptr<CSSParserSelector> secondCompound;
if (context.mode == UASheetMode || isPart) {
// FIXME: https://bugs.webkit.org/show_bug.cgi?id=161747
// We have to recur, since we have rules in media controls like video::a::b. This should not be allowed, and
// we should remove this recursion once those rules are gone.
secondCompound = splitCompoundAtImplicitShadowCrossingCombinator(splitAfter->releaseTagHistory(), context);
} else
secondCompound = splitAfter->releaseTagHistory();
auto relation = [&] {
if (isSlotted)
return CSSSelector::ShadowSlotted;
if (isPart)
return CSSSelector::ShadowPartDescendant;
return CSSSelector::ShadowDescendant;
}();
secondCompound->appendTagHistory(relation, WTFMove(compoundSelector));
return secondCompound;
}
bool CSSSelectorParser::containsUnknownWebKitPseudoElements(const CSSSelector& complexSelector)
{
for (auto current = &complexSelector; current; current = current->tagHistory()) {
if (current->match() == CSSSelector::PseudoElement && current->pseudoElementType() == CSSSelector::PseudoElementWebKitCustom)
return true;
}
return false;
}
} // namespace WebCore