Skip to content

Commit

Permalink
[CSS] Scoped style rules accept relative selector list
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=265289
rdar://118749004

Reviewed by Antti Koivisto.

Like nested style rule, scoped style rules accept a relative selector
list as the prelude for their rule list.
The implicit ":scope" or "&" in their prelude has to be added explicitely when necessary.

https://drafts.csswg.org/css-cascade-6/#scoped-style-rules

* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-cssom.html:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-nesting-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-specificity-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-cascade/scope-visited-cssom-expected.txt:

For the moment, until the @scope feature is more complete, those tests passing or failing don't mean anything.

* Source/WebCore/css/CSSSelector.cpp:
(WebCore::CSSSelector::hasExplicitPseudoClassScope const):
* Source/WebCore/css/CSSSelector.h:
* Source/WebCore/css/StyleRule.h:
(WebCore::StyleRuleBase::isGroupRule const):
* Source/WebCore/css/parser/CSSParserImpl.cpp:
(WebCore::CSSParserImpl::consumeRegularRuleList):
(WebCore::CSSParserImpl::consumeScopeRule):
(WebCore::CSSParserImpl::consumeStyleRule):
* Source/WebCore/css/parser/CSSParserImpl.h:
(WebCore::CSSParserImpl::isStyleNestedContext):
(WebCore::CSSParserImpl::isNestedContext):
* Source/WebCore/css/parser/CSSParserSelector.cpp:
(WebCore::CSSParserSelector::hasExplicitPseudoClassScope const):
(WebCore::CSSParserSelector::appendTagHistoryAsRelative):
* Source/WebCore/css/parser/CSSParserSelector.h:
* Source/WebCore/css/parser/CSSSelectorParser.cpp:
(WebCore::CSSSelectorParser::consumeNestedComplexSelector):

We move the code to add the prepend the implicit selector from CSSSelectorParser
to CSSParserImpl where we know the type of the parent rule
(because we need to preprend either & or :scope)

(WebCore::CSSSelectorParser::resolveNestingParent):

Canonical link: https://commits.webkit.org/271336@main
  • Loading branch information
mdubet committed Nov 30, 2023
1 parent 200b6e4 commit f658dff
Show file tree
Hide file tree
Showing 11 changed files with 119 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
}, 'CSSScopeRule.cssText, root only');

test(() => {
assert_equals(style.sheet.rules[2].cssText, '@scope (.a) to (.b) {\n div { display: block; }\n}');
assert_equals(style.sheet.rules[2].cssText, '@scope (.a) to (.b) {\n :scope div { display: block; }\n}');
}, 'CSSScopeRule.cssText, root and limit');

test(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

FAIL @scope (#main) { .b { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) to (.b) { .a { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main, .foo, .bar) { #a { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { div.b { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { .b { } } assert_equals: unscoped + scoped expected "2" but got "1"
FAIL @scope (#main) to (.b) { .a { } } assert_equals: unscoped + scoped expected "2" but got "1"
FAIL @scope (#main, .foo, .bar) { #a { } } assert_equals: unscoped + scoped expected "2" but got "1"
FAIL @scope (#main) { div.b { } } assert_equals: unscoped + scoped expected "2" but got "1"
FAIL @scope (#main) { :scope .b { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { & .b { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { div .b { } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { @scope (.a) { .b { } } } assert_equals: scoped + unscoped expected "1" but got "2"
FAIL @scope (#main) { div .b { } } assert_equals: unscoped + scoped expected "2" but got "1"
FAIL @scope (#main) { @scope (.a) { .b { } } } assert_equals: unscoped + scoped expected "2" but got "1"

Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ FAIL :link as scoping root, :scope assert_equals: expected "rgb(0, 128, 0)" but
PASS :visited as scoping root, :scope
FAIL :not(:visited) as scoping root, :scope assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 255, 255)"
PASS :not(:link) as scoping root, :scope
PASS :link as scoping limit
FAIL :visited as scoping limit assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 255, 255)"
FAIL :not(:link) as scoping limit assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 255, 255)"
PASS :not(:visited) as scoping limit
FAIL :link as scoping limit assert_equals: expected "rgb(255, 255, 255)" but got "rgb(0, 128, 0)"
PASS :visited as scoping limit
PASS :not(:link) as scoping limit
FAIL :not(:visited) as scoping limit assert_equals: expected "rgb(255, 255, 255)" but got "rgb(0, 128, 0)"

12 changes: 12 additions & 0 deletions Source/WebCore/css/CSSSelector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1102,4 +1102,16 @@ bool CSSSelector::hasExplicitNestingParent() const
return visitAllSimpleSelectors(checkForExplicitParent);
}

bool CSSSelector::hasExplicitPseudoClassScope() const
{
auto check = [] (const CSSSelector& selector) {
if (selector.match() == Match::PseudoClass && selector.pseudoClassType() == PseudoClassType::Scope)
return true;

return false;
};

return visitAllSimpleSelectors(check);
}

} // namespace WebCore
1 change: 1 addition & 0 deletions Source/WebCore/css/CSSSelector.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ struct PossiblyQuotedIdentifier {
bool visitAllSimpleSelectors(auto& apply) const;

bool hasExplicitNestingParent() const;
bool hasExplicitPseudoClassScope() const;
void resolveNestingParentSelectors(const CSSSelectorList& parent);
void replaceNestingParentByPseudoClassScope();

Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/css/StyleRule.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class StyleRuleBase : public RefCounted<StyleRuleBase> {
bool isPageRule() const { return type() == StyleRuleType::Page; }
bool isStyleRule() const { return type() == StyleRuleType::Style || type() == StyleRuleType::StyleWithNesting; }
bool isStyleRuleWithNesting() const { return type() == StyleRuleType::StyleWithNesting; }
bool isGroupRule() const { return type() == StyleRuleType::Media || type() == StyleRuleType::Supports || type() == StyleRuleType::LayerBlock || type() == StyleRuleType::Container; }
bool isGroupRule() const { return type() == StyleRuleType::Media || type() == StyleRuleType::Supports || type() == StyleRuleType::LayerBlock || type() == StyleRuleType::Container || type() == StyleRuleType::Scope; }
bool isSupportsRule() const { return type() == StyleRuleType::Supports; }
bool isImportRule() const { return type() == StyleRuleType::Import; }
bool isLayerRule() const { return type() == StyleRuleType::LayerBlock || type() == StyleRuleType::LayerStatement; }
Expand Down
55 changes: 48 additions & 7 deletions Source/WebCore/css/parser/CSSParserImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
#include "StyleRule.h"
#include "StyleRuleImport.h"
#include "StyleSheetContents.h"
#include "css/parser/CSSParserEnum.h"
#include <bitset>
#include <memory>
#include <optional>
Expand Down Expand Up @@ -591,7 +592,7 @@ Vector<Ref<StyleRuleBase>> CSSParserImpl::consumeRegularRuleList(CSSParserTokenR
return { };

Vector<Ref<StyleRuleBase>> rules;
if (isNestedContext()) {
if (isStyleNestedContext()) {
runInNewNestingContext([&] {
consumeStyleBlock(block, StyleRuleType::Style, ParsingStyleDeclarationsInRuleList::Yes);

Expand Down Expand Up @@ -951,7 +952,8 @@ RefPtr<StyleRuleScope> CSSParserImpl::consumeScopeRule(CSSParserTokenRange prelu
return nullptr;

// FIXME: implement mixing style nesting and scope nesting
if (isNestedContext())
// https://bugs.webkit.org/show_bug.cgi?id=265368
if (isStyleNestedContext())
return nullptr;

auto preludeRangeCopy = prelude;
Expand All @@ -960,7 +962,7 @@ RefPtr<StyleRuleScope> CSSParserImpl::consumeScopeRule(CSSParserTokenRange prelu

if (!prelude.atEnd()) {
auto consumePrelude = [&] {
auto consumeScope = [&](auto& scope) {
auto consumeScope = [&](auto& scope, bool nestedContext) {
// Consume the left parenthesis
if (prelude.peek().type() != LeftParenthesisToken)
return false;
Expand All @@ -973,7 +975,7 @@ RefPtr<StyleRuleScope> CSSParserImpl::consumeScopeRule(CSSParserTokenRange prelu
CSSParserTokenRange selectorListRange = prelude.makeSubRange(selectorListRangeStart, &prelude.peek());

// Parse the selector list range
auto selectorList = parseCSSSelectorList(selectorListRange, m_context, m_styleSheet.get(), isNestedContext() ? CSSParserEnum::IsNestedContext::Yes : CSSParserEnum::IsNestedContext::No, CSSParserEnum::IsForgiving::Yes);
auto selectorList = parseCSSSelectorList(selectorListRange, m_context, m_styleSheet.get(), nestedContext ? CSSParserEnum::IsNestedContext::Yes : CSSParserEnum::IsNestedContext::No, CSSParserEnum::IsForgiving::Yes);
if (!selectorList)
return false;

Expand All @@ -986,15 +988,15 @@ RefPtr<StyleRuleScope> CSSParserImpl::consumeScopeRule(CSSParserTokenRange prelu
scope = WTFMove(*selectorList);
return true;
};
auto successScopeStart = consumeScope(scopeStart);
auto successScopeStart = consumeScope(scopeStart, isNestedContext());
if (successScopeStart && prelude.atEnd())
return true;
if (prelude.peek().type() != IdentToken)
return false;
auto to = prelude.consumeIncludingWhitespace();
if (!equalLettersIgnoringASCIICase(to.value(), "to"_s))
return false;
if (!consumeScope(scopeEnd))
if (!consumeScope(scopeEnd, true)) // scopeEnd is always considered nested, at least by the scopeStart
return false;
if (!prelude.atEnd())
return false;
Expand All @@ -1011,7 +1013,13 @@ RefPtr<StyleRuleScope> CSSParserImpl::consumeScopeRule(CSSParserTokenRange prelu
m_observerWrapper->observer().endRuleBody(m_observerWrapper->endOffset(block));
}

auto rules = consumeRegularRuleList(block);
NestingLevelIncrementer incrementer { m_scopeRuleNestingLevel };
m_ancestorRuleTypeStack.append(AncestorRuleType::Scope);
Vector<Ref<StyleRuleBase>> rules;
consumeRuleList(block, RegularRuleList, [&rules](Ref<StyleRuleBase> rule) {
rules.append(rule);
});
m_ancestorRuleTypeStack.removeLast();
return StyleRuleScope::create(WTFMove(scopeStart), WTFMove(scopeEnd), WTFMove(rules));
}

Expand Down Expand Up @@ -1213,6 +1221,37 @@ RefPtr<StyleRuleBase> CSSParserImpl::consumeStyleRule(CSSParserTokenRange prelud
if (parserSelectorList.isEmpty())
return nullptr; // Parse error, invalid selector list

if (!m_ancestorRuleTypeStack.isEmpty()) {
// https://drafts.csswg.org/css-nesting/#cssom
// Relative selector should be absolutized (only when not "nest-containing" for the descendant one),
// with the implied nesting selector inserted.
auto last = m_ancestorRuleTypeStack.last();
auto appendImplicitIfNeeded = [&](auto& selector) {
auto selectorStartWithExplicitCombinator = [&] {
auto relation = selector->leftmostSimpleSelector()->selector()->relation();
return relation != CSSSelector::RelationType::Subselector && relation != CSSSelector::RelationType::DescendantSpace;
}();
// For a rule inside a style rule, we had the implicit & if it's not there already or if it starts with a combinator > ~ +
if (last == AncestorRuleType::Style) {
if (!selector->hasExplicitNestingParent() || selectorStartWithExplicitCombinator) {
auto nestingParentSelector = makeUnique<CSSParserSelector>();
nestingParentSelector->setMatch(CSSSelector::Match::NestingParent);
selector->appendTagHistoryAsRelative(WTFMove(nestingParentSelector));
}
// For a rule inside a scope rule, we had the implicit ":scope" if there is no explicit & or :scope already
} else if (last == AncestorRuleType::Scope) {
if ((!selector->hasExplicitNestingParent() && !selector->hasExplicitPseudoClassScope()) || selectorStartWithExplicitCombinator) {
auto scopeSelector = makeUnique<CSSParserSelector>();
scopeSelector->setMatch(CSSSelector::Match::PseudoClass);
scopeSelector->setPseudoClassType(CSSSelector::PseudoClassType::Scope);
selector->appendTagHistoryAsRelative(WTFMove(scopeSelector));
}
}
};
for (auto& parserSelector : parserSelectorList)
appendImplicitIfNeeded(parserSelector);
}

auto selectorList = CSSSelectorList { WTFMove(parserSelectorList) };
ASSERT(!selectorList.isEmpty());

Expand All @@ -1224,7 +1263,9 @@ RefPtr<StyleRuleBase> CSSParserImpl::consumeStyleRule(CSSParserTokenRange prelud
runInNewNestingContext([&] {
{
NestingLevelIncrementer incrementer { m_styleRuleNestingLevel };
m_ancestorRuleTypeStack.append(AncestorRuleType::Style);
consumeStyleBlock(block, StyleRuleType::Style);
m_ancestorRuleTypeStack.removeLast();
}

auto nestedRules = WTFMove(topContext().m_parsedRules);
Expand Down
17 changes: 16 additions & 1 deletion Source/WebCore/css/parser/CSSParserImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,29 @@ class CSSParserImpl {
ASSERT(!m_nestingContextStack.isEmpty());
return m_nestingContextStack.last();
}
bool isNestedContext()

bool isStyleNestedContext()
{
return (m_isAlwaysNestedContext == CSSParserEnum::IsNestedContext::Yes || m_styleRuleNestingLevel) && context().cssNestingEnabled;
}

bool isNestedContext()
{
return m_scopeRuleNestingLevel || isStyleNestedContext();
}

CSSParserEnum::IsNestedContext m_isAlwaysNestedContext { CSSParserEnum::IsNestedContext::No }; // Do we directly start in a nested context (for CSSOM)

// FIXME: we could unify all those into a single stack data structure.
// https://bugs.webkit.org/show_bug.cgi?id=265566
unsigned m_styleRuleNestingLevel { 0 };
unsigned m_scopeRuleNestingLevel { 0 };
unsigned m_ruleListNestingLevel { 0 };
enum class AncestorRuleType : bool {
Style,
Scope,
};
Vector<AncestorRuleType, 16> m_ancestorRuleTypeStack;

Vector<NestingContext> m_nestingContextStack { NestingContext { } };
const CSSParserContext& m_context;
Expand Down
25 changes: 25 additions & 0 deletions Source/WebCore/css/parser/CSSParserSelector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@ bool CSSParserSelector::hasExplicitNestingParent() const
return false;
}

bool CSSParserSelector::hasExplicitPseudoClassScope() const
{
auto selector = this;
while (selector) {
if (selector->selector()->hasExplicitPseudoClassScope())
return true;

selector = selector->tagHistory();
}
return false;
}

static bool selectorListMatchesPseudoElement(const CSSSelectorList* selectorList)
{
if (!selectorList)
Expand Down Expand Up @@ -206,6 +218,19 @@ void CSSParserSelector::appendTagHistory(CSSSelector::RelationType relation, std
end->setTagHistory(WTFMove(selector));
}

void CSSParserSelector::appendTagHistoryAsRelative(std::unique_ptr<CSSParserSelector> selector)
{
auto lastSelector = leftmostSimpleSelector()->selector();
ASSERT(lastSelector);

// Relation is Descendant by default.
auto relation = lastSelector->relation();
if (relation == CSSSelector::RelationType::Subselector)
relation = CSSSelector::RelationType::DescendantSpace;

appendTagHistory(relation, WTFMove(selector));
}

void CSSParserSelector::appendTagHistory(CSSParserSelectorCombinator relation, std::unique_ptr<CSSParserSelector> selector)
{
CSSParserSelector* end = this;
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/css/parser/CSSParserSelector.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class CSSParserSelector {
bool isHostPseudoSelector() const;

bool hasExplicitNestingParent() const;
bool hasExplicitPseudoClassScope() const;

// FIXME-NEWPARSER: "slotted" was removed here for now, since it leads to a combinator
// connection of ShadowDescendant, and the current shadow DOM code doesn't expect this. When
Expand All @@ -99,6 +100,7 @@ class CSSParserSelector {
void insertTagHistory(CSSSelector::RelationType before, std::unique_ptr<CSSParserSelector>, CSSSelector::RelationType after);
void appendTagHistory(CSSSelector::RelationType, std::unique_ptr<CSSParserSelector>);
void appendTagHistory(CSSParserSelectorCombinator, std::unique_ptr<CSSParserSelector>);
void appendTagHistoryAsRelative(std::unique_ptr<CSSParserSelector>);
void prependTagSelector(const QualifiedName&, bool tagIsForNamespaceRule = false);
std::unique_ptr<CSSParserSelector> releaseTagHistory();

Expand Down
32 changes: 3 additions & 29 deletions Source/WebCore/css/parser/CSSSelectorParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -300,39 +300,13 @@ static bool isDescendantCombinator(CSSSelector::RelationType relation)

std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeNestedComplexSelector(CSSParserTokenRange& range)
{
auto appendNestingSelector = [] (auto& parserSelector) {
// https://drafts.csswg.org/css-nesting/#cssom
// Relative selector should be absolutized (only when not "nest-containing" for the descendant one),
// with the implied nesting selector inserted.

auto lastSelector = parserSelector->leftmostSimpleSelector()->selector();
ASSERT(lastSelector);

// Relation is Descendant by default.
auto relation = lastSelector->relation();
if (relation == CSSSelector::RelationType::Subselector)
relation = CSSSelector::RelationType::DescendantSpace;

auto nestingSelector = makeUnique<CSSParserSelector>();
nestingSelector->setMatch(CSSSelector::Match::NestingParent);
// We add the implicit parent selector at the beginning of the selector.
parserSelector->appendTagHistory(relation, WTFMove(nestingSelector));
};

auto selector = consumeComplexSelector(range);
if (selector) {
if (selector->hasExplicitNestingParent())
return selector;

appendNestingSelector(selector);
if (selector)
return selector;
}

selector = consumeRelativeNestedSelector(range);
if (selector) {
appendNestingSelector(selector);
if (selector)
return selector;
}

return nullptr;
}
Expand Down Expand Up @@ -1280,7 +1254,7 @@ CSSSelectorList CSSSelectorParser::resolveNestingParent(const CSSSelectorList& n
// FIXME: We should build a new CSSParserSelector from this selector and resolve it
const_cast<CSSSelector*>(selector)->resolveNestingParentSelectors(*parentResolvedSelectorList);
} else {
// It's top-level, the nesting parent selector should be replace by :scope
// It's top-level, the nesting parent selector should be replaced by :scope
const_cast<CSSSelector*>(selector)->replaceNestingParentByPseudoClassScope();
}
auto parserSelector = makeUnique<CSSParserSelector>(*selector);
Expand Down

0 comments on commit f658dff

Please sign in to comment.