Skip to content

Commit

Permalink
Support relative selector to update the :has tests
Browse files Browse the repository at this point in the history
Support the relative selector grammar starting with combinator.
- https://www.w3.org/TR/selectors-4/#typedef-relative-selector

To simplify matching operation, some relation types are added.

- kRelativeDescendant       : Leftmost descendant combinator
- kRelativeChild            : Leftmost child combinator
- kRelativeDirectAdjacent   : Leftmost next-sibling combinator
- kRelativeIndirectAdjacent : Leftmost subsequent-sibling combinator

The ':scope' dependency in <relative-selector> definition creates
too much confusion especially with ':has' as the CSSWG issue
describes.
- w3c/csswg-drafts#6399

1. ':scope' behavior in ':has' argument is different with usual
   ':scope' behavior.
2. Explicit ':scope' in a ':has' argument can create performance
   issues or increase complexity when the ':scope' is not leftmost
   or compounded with other simple selectors.
3. Absolutizing a relative selector with ':scope' doesn't make sense
   when the ':has' argument already has explicit ':scope'
   (e.g. ':has(~ .a :scope .b)' -> ':has(:scope ~ .a :scope .b)'

To skip those complexity and ambiguity, this CL removed some logic
related with the 'explicit :scope in :has argument', and added
TODO comment to handle it later separately.

As suggested in the CSSWG issue, this CL always absolutize the
<relative-selector> with a dummy pseudo class.
- kPseudoRelativeLeftmost

The added pseudo class represents any elements that is at the
relative position that matches with the leftmost combinator
of the relative selector.

This CL also includes tentative tests for some cases involving the
':scope' inside ':has' to show the result of the suggestion.
By removing the ':scope' dependency from the relative selector,
most of the ':scope' inside ':has' will be meaningless. (It will
not match or can be changed more simple/efficient expression)

Change-Id: I1e0ccf0c190d04b9636d86cb15e1bbb175b7cc30
Bug: 669058
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2972189
Reviewed-by: Rune Lillesveen <futhark@chromium.org>
Commit-Queue: Byungwoo Lee <blee@igalia.com>
Cr-Commit-Position: refs/heads/master@{#908421}
NOKEYCHECK=True
GitOrigin-RevId: 4913bff09fee113fddaeef2aaeed95a527a1201a
  • Loading branch information
byung-woo authored and copybara-github committed Aug 4, 2021
1 parent 4ee16a3 commit 6e703b7
Show file tree
Hide file tree
Showing 18 changed files with 388 additions and 498 deletions.
18 changes: 18 additions & 0 deletions blink/renderer/core/css/css_selector.cc
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ inline unsigned CSSSelector::SpecificityForOneSelector() const {
FALLTHROUGH;
case kPseudoIs:
return MaximumSpecificity(SelectorList());
case kPseudoHas:
return MaximumSpecificity(SelectorList());
case kPseudoRelativeLeftmost:
return 0;
// FIXME: PseudoAny should base the specificity on the sub-selectors.
// See http://lists.w3.org/Archives/Public/www-style/2010Sep/0530.html
case kPseudoAny:
Expand Down Expand Up @@ -342,6 +346,7 @@ PseudoId CSSSelector::GetPseudoId(PseudoType type) {
case kPseudoXrOverlay:
case kPseudoModal:
case kPseudoHas:
case kPseudoRelativeLeftmost:
return kPseudoIdNone;
}

Expand Down Expand Up @@ -369,6 +374,7 @@ const static NameToPseudoStruct kPseudoTypeWithoutArgumentsMap[] = {
{"-internal-modal", CSSSelector::kPseudoModal},
{"-internal-multi-select-focus", CSSSelector::kPseudoMultiSelectFocus},
{"-internal-popup-open", CSSSelector::kPseudoPopupOpen},
{"-internal-relative-leftmost", CSSSelector::kPseudoRelativeLeftmost},
{"-internal-shadow-host-has-appearance",
CSSSelector::kPseudoHostHasAppearance},
{"-internal-spatial-navigation-focus",
Expand Down Expand Up @@ -727,6 +733,7 @@ void CSSSelector::UpdatePseudoType(const AtomicString& value,
case kPseudoPastCue:
case kPseudoReadOnly:
case kPseudoReadWrite:
case kPseudoRelativeLeftmost:
case kPseudoRequired:
case kPseudoRoot:
case kPseudoScope:
Expand Down Expand Up @@ -870,6 +877,9 @@ const CSSSelector* CSSSelector::SerializeCompound(
case kPseudoIs:
case kPseudoWhere:
break;
case kPseudoRelativeLeftmost:
NOTREACHED();
return nullptr;
default:
break;
}
Expand Down Expand Up @@ -993,6 +1003,14 @@ String CSSSelector::SelectorText() const {
case kShadowSlot:
result = builder.ToString() + result;
break;
case kRelativeDescendant:
return builder.ToString() + result;
case kRelativeChild:
return "> " + builder.ToString() + result;
case kRelativeDirectAdjacent:
return "+ " + builder.ToString() + result;
case kRelativeIndirectAdjacent:
return "~ " + builder.ToString() + result;
}
}
NOTREACHED();
Expand Down
14 changes: 14 additions & 0 deletions blink/renderer/core/css/css_selector.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ class CORE_EXPORT CSSSelector {
// matching a ::part in shadow-including descendant tree for #host in
// "#host::part(button)".
kShadowPart,

// leftmost "Space" combinator of relative selector
kRelativeDescendant,
// leftmost > combinator of relative selector
kRelativeChild,
// leftmost + combinator of relative selector
kRelativeDirectAdjacent,
// leftmost ~ combinator of relative selector
kRelativeIndirectAdjacent
};

enum PseudoType {
Expand Down Expand Up @@ -277,6 +286,11 @@ class CORE_EXPORT CSSSelector {
kPseudoSpellingError,
kPseudoGrammarError,
kPseudoHas,
// TODO(blee@igalia.com) Need to clarify the :scope dependency in relative
// selector definition.
// - spec : https://www.w3.org/TR/selectors-4/#relative
// - csswg issue : https://github.com/w3c/csswg-drafts/issues/6399
kPseudoRelativeLeftmost,
};

enum class AttributeMatchType {
Expand Down
18 changes: 18 additions & 0 deletions blink/renderer/core/css/css_selector_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,24 @@ TEST(CSSSelector, Specificity_Not) {
Specificity(":is(.c + .c + .c, .b + .c:not(span), .b + .c + .e)"));
}

TEST(CSSSelector, Specificity_Has) {
EXPECT_EQ(Specificity(":has(div)"), Specificity("div"));
EXPECT_EQ(Specificity(":has(div)"), Specificity("* div"));
EXPECT_EQ(Specificity(":has(~ div)"), Specificity("* ~ div"));
EXPECT_EQ(Specificity(":has(> .a)"), Specificity("* > .a"));
EXPECT_EQ(Specificity(":has(+ div.a)"), Specificity("* + div.a"));
EXPECT_EQ(Specificity(".a :has(.b, div.c)"), Specificity(".a div.c"));
EXPECT_EQ(Specificity(".a :has(.c#d, .e)"), Specificity(".a .c#d"));
EXPECT_EQ(Specificity(":has(.e+.f, .g>.b, .h)"), Specificity(".e+.f"));
EXPECT_EQ(Specificity(".a :has(.e+.f, .g>.b, .h#i)"), Specificity(".a .h#i"));
EXPECT_EQ(Specificity(".a+:has(.b+span.f, :has(.c>.e, .g))"),
Specificity(".a+.b+span.f"));
EXPECT_EQ(Specificity("div > :has(div:where(span:where(.b ~ .c)))"),
Specificity("div > div"));
EXPECT_EQ(Specificity(":has(.c + .c + .c, .b + .c:not(span), .b + .c + .e)"),
Specificity(".c + .c + .c"));
}

TEST(CSSSelector, HasLinkOrVisited) {
EXPECT_FALSE(HasLinkOrVisited("tag"));
EXPECT_FALSE(HasLinkOrVisited("visited"));
Expand Down
119 changes: 44 additions & 75 deletions blink/renderer/core/css/has_argument_match_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace { // anonymous namespace for file-local method and constant

using blink::CSSSelector;
using blink::Element;
using blink::To;
using blink::Traversal;
Expand All @@ -34,98 +35,82 @@ inline Element* LastDescendantOf(const Element& element,
return last_descendant;
}

inline const CSSSelector* GetCurrentRelationAndNextCompound(
const CSSSelector* compound_selector,
CSSSelector::RelationType& relation) {
DCHECK(compound_selector);
for (; compound_selector;
compound_selector = compound_selector->TagHistory()) {
relation = compound_selector->Relation();
if (relation != CSSSelector::kSubSelector)
return compound_selector->TagHistory();
}
return nullptr;
}

} // namespace

namespace blink {

HasArgumentMatchContext::HasArgumentMatchContext(const CSSSelector* selector) {
const CSSSelector* leftmost_compound = selector;
const CSSSelector* leftmost_compound_containing_scope = nullptr;

while (leftmost_compound) {
const CSSSelector* simple_selector = leftmost_compound;
CSSSelector::RelationType relation;
while (simple_selector) {
if (leftmost_compound_containing_scope)
contains_compounded_scope_selector_ = true;
if (simple_selector->GetPseudoType() == CSSSelector::kPseudoScope) {
if (leftmost_compound_containing_scope &&
leftmost_compound_containing_scope != leftmost_compound) {
// Selectors that contains multiple :scope pseudo classes separated
// by combinators will never match.
// (e.g. :has(:scope > .a > :scope))
SetNeverMatch();
return;
}
if (simple_selector != leftmost_compound)
contains_compounded_scope_selector_ = true;
leftmost_compound_containing_scope = leftmost_compound;
}
relation = simple_selector->Relation();
if (relation != CSSSelector::kSubSelector)
break;
simple_selector = simple_selector->TagHistory();
}

if (!simple_selector)
break;

if (leftmost_compound_containing_scope) {
// Skip to update the context if it already found the :scope
leftmost_compound = simple_selector->TagHistory();
DCHECK(leftmost_compound);
continue;
}

HasArgumentMatchContext::HasArgumentMatchContext(const CSSSelector* selector)
: leftmost_relation_(CSSSelector::kSubSelector),
adjacent_traversal_distance_(0),
descendant_traversal_depth_(0) {
CSSSelector::RelationType relation = CSSSelector::kSubSelector;
// The explicit ':scope' in ':has' argument selector is not considered
// for getting the depth and adjacent distance.
// TODO(blee@igalia.com) Need to clarify the :scope dependency in relative
// selector definition.
// - spec : https://www.w3.org/TR/selectors-4/#relative
// - csswg issue : https://github.com/w3c/csswg-drafts/issues/6399
for (selector = GetCurrentRelationAndNextCompound(selector, relation);
selector;
selector = GetCurrentRelationAndNextCompound(selector, relation)) {
switch (relation) {
case CSSSelector::kRelativeDescendant:
leftmost_relation_ = relation;
FALLTHROUGH;
case CSSSelector::kDescendant:
descendant_traversal_depth_ = kInfiniteDepth;
adjacent_traversal_distance_ = 0;
leftmost_relation_ = relation;
break;

case CSSSelector::kRelativeChild:
leftmost_relation_ = relation;
FALLTHROUGH;
case CSSSelector::kChild:
if (descendant_traversal_depth_ != kInfiniteDepth) {
descendant_traversal_depth_++;
adjacent_traversal_distance_ = 0;
}
leftmost_relation_ = relation;
break;

case CSSSelector::kRelativeDirectAdjacent:
leftmost_relation_ = relation;
FALLTHROUGH;
case CSSSelector::kDirectAdjacent:
if (adjacent_traversal_distance_ != kInfiniteAdjacentDistance)
adjacent_traversal_distance_++;
leftmost_relation_ = relation;
break;

case CSSSelector::kRelativeIndirectAdjacent:
leftmost_relation_ = relation;
FALLTHROUGH;
case CSSSelector::kIndirectAdjacent:
adjacent_traversal_distance_ = kInfiniteAdjacentDistance;
leftmost_relation_ = relation;
break;

case CSSSelector::kUAShadow:
case CSSSelector::kShadowSlot:
case CSSSelector::kShadowPart:
// TODO(blee@igalia.com) Need to check how to handle the shadow tree
// (e.g. ':has(::slotted(img))', ':has(component::part(my-part))')
SetNeverMatch();
return;
default:
NOTREACHED();
break;
}

leftmost_compound = simple_selector->TagHistory();
DCHECK(leftmost_compound);
}

if (!leftmost_compound_containing_scope) {
// Always set descendant relative selector because the relative selector
// spec is not supported yet.
leftmost_relation_ = CSSSelector::kDescendant;
descendant_traversal_depth_ = kInfiniteDepth;
adjacent_traversal_distance_ = 0;
return;
}

contains_no_leftmost_scope_selector_ =
leftmost_compound_containing_scope != leftmost_compound;
}

CSSSelector::RelationType HasArgumentMatchContext::GetLeftMostRelation() const {
Expand All @@ -140,22 +125,6 @@ bool HasArgumentMatchContext::GetAdjacentDistanceFixed() const {
return adjacent_traversal_distance_ != kInfiniteAdjacentDistance;
}

bool HasArgumentMatchContext::WillNeverMatch() const {
return leftmost_relation_ == CSSSelector::kSubSelector;
}

bool HasArgumentMatchContext::ContainsCompoundedScopeSelector() const {
return contains_compounded_scope_selector_;
}

bool HasArgumentMatchContext::ContainsNoLeftmostScopeSelector() const {
return contains_no_leftmost_scope_selector_;
}

void HasArgumentMatchContext::SetNeverMatch() {
leftmost_relation_ = CSSSelector::kSubSelector;
}

HasArgumentSubtreeIterator::HasArgumentSubtreeIterator(
Element& scope_element,
HasArgumentMatchContext& context)
Expand Down
20 changes: 3 additions & 17 deletions blink/renderer/core/css/has_argument_match_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,8 @@ class HasArgumentMatchContext {
CSSSelector::RelationType GetLeftMostRelation() const;
bool GetDepthFixed() const;
bool GetAdjacentDistanceFixed() const;
bool WillNeverMatch() const;
bool ContainsCompoundedScopeSelector() const;
bool ContainsNoLeftmostScopeSelector() const;

private:
void SetNeverMatch();

// Indicate the :has argument relative type and subtree traversal scope.
// If 'adjacent_traversal_distance_' is greater than 0, then it means that
// it is enough to traverse the adjacent subtree at that distance.
Expand Down Expand Up @@ -144,18 +139,9 @@ class HasArgumentMatchContext {
// - Traverse the depth m elements of the distance n sibling subtree of
// the :has scope element. (elements at depth m of the descendant subtree
// of the sibling element at distance n)
CSSSelector::RelationType leftmost_relation_{CSSSelector::kSubSelector};
int adjacent_traversal_distance_{0};
int descendant_traversal_depth_{0};

// Indicate that the argument selector has a ':scope' in a compound which
// contains other simple selectors.
// (e.g. ':has(.a:scope .b)', ':has(.a .b:scope .c')
bool contains_compounded_scope_selector_{false};

// Indicate that the argument selector has a ':scope' which is not leftmost
// (e.g. ':has(.a :scope .b)', ':has(.a .b:scope .c)')
bool contains_no_leftmost_scope_selector_{false};
CSSSelector::RelationType leftmost_relation_;
int adjacent_traversal_distance_;
int descendant_traversal_depth_;

friend class HasArgumentSubtreeIterator;
};
Expand Down
Loading

0 comments on commit 6e703b7

Please sign in to comment.