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
  • Loading branch information
byung-woo authored and chromium-wpt-export-bot committed Aug 3, 2021
1 parent 380dd71 commit 6f5e733
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 143 deletions.
69 changes: 69 additions & 0 deletions css/selectors/has-argument-with-explicit-scope.tentative.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>:has pseudo class behavior with explicit ':scope' in its argument</title>
<link rel="author" title="Byungwoo Lee" href="mailto:[email protected]">
<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<main>
<div id=d01 class="a">
<div id=scope1 class="b">
<div id=d02 class="c">
<div id=d03 class="c">
<div id=d04 class="d"></div>
</div>
</div>
<div id=d05 class="e"></div>
</div>
</div>
<div id=d06>
<div id=scope2 class="b">
<div id=d07 class="c">
<div id=d08 class="c">
<div id=d09></div>
</div>
</div>
</div>
</div>
</div>

<script>
function formatElements(elements) {
return elements.map(e => e.id).sort().join();
}

// Test that |selector| returns the given elements in the given scope element
function test_selector_all(scope, selector, expected) {
test(function() {
let actual = Array.from(scope.querySelectorAll(selector));
assert_equals(formatElements(actual), formatElements(expected));
}, `${selector} matches expected elements on ${scope.id}`);
}

// Test that |selector1| and |selector2| returns same elements in the given scope element
function compare_selector_all(scope, selector1, selector2) {
test(function() {
let result1 = Array.from(scope.querySelectorAll(selector1));
let result2 = Array.from(scope.querySelectorAll(selector2));
assert_equals(formatElements(result1), formatElements(result2));
}, `${selector1} and ${selector2} returns same elements on ${scope.id}`);
}

// descendants of a scope element cannot have the scope element as its descendant
test_selector_all(scope1, ':has(:scope)', []);
test_selector_all(scope1, ':has(:scope .c)', []);
test_selector_all(scope1, ':has(.a :scope)', []);

// there can be more simple and efficient alternative for a ':scope' in ':has'
test_selector_all(scope1, '.a:has(:scope) .c', [d02, d03]);
compare_selector_all(scope1, '.a:has(:scope) .c', ':is(.a :scope .c)');
test_selector_all(scope2, '.a:has(:scope) .c', []);
compare_selector_all(scope2, '.a:has(:scope) .c', ':is(.a :scope .c)');
test_selector_all(scope1, '.c:has(:is(:scope .d))', [d02, d03]);
compare_selector_all(scope1, '.c:has(:is(:scope .d))', ':scope .c:has(.d)');
compare_selector_all(scope1, '.c:has(:is(:scope .d))', '.c:has(.d)');
test_selector_all(scope2, '.c:has(:is(:scope .d))', []);
compare_selector_all(scope2, '.c:has(:is(:scope .d))', ':scope .c:has(.d)');
compare_selector_all(scope2, '.c:has(:is(:scope .d))', '.c:has(.d)');
</script>
6 changes: 1 addition & 5 deletions css/selectors/has-basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,10 @@
test_selector_all(
':has(.sibling:has(.descendant) ~ .target) ~ .parent > .descendant',
[g, i, j]);
test_selector_all(':has(:scope .target)', [a, b, f, h]);
test_selector_all(':has(:scope > .parent)', [a]);
test_selector_all(':has(> .parent)', [a]);
test_selector_all(':has(> .target)', [b, f, h]);
test_selector_all(':has(:scope > .target)', [b, f, h]);
test_selector_all(':has(+ #h)', [f]);
test_selector_all(':has(:scope + #h)', [f]);
test_selector_all('.parent:has(~ #h)', [b, f]);
test_selector_all('.parent:has(:scope ~ #h)', [b, f]);
test_selector('.sibling:has(.descendant)', c);
test_closest(k, '.ancestor:has(.descendant)', h);
test_matches(h, ':has(.target ~ .sibling .descendant)', true);
Expand Down
164 changes: 39 additions & 125 deletions css/selectors/has-relative-argument.html
Original file line number Diff line number Diff line change
Expand Up @@ -138,139 +138,53 @@
}, `${selector} matches expected elements`);
}

test_selector_all('.x:has(:scope .a)', [d02, d06, d07, d09, d12]);
test_selector_all('.x:has(:scope .a > .b)', [d09]);
test_selector_all('.x:has(:scope .a .b)', [d09, d12]);
test_selector_all('.x:has(:scope .a + .b)', [d12]);
test_selector_all('.x:has(:scope .a ~ .b)', [d02, d12]);
test_selector_all(':has(.x:scope .a)', [d02, d06, d07, d09, d12]);
test_selector_all(':has(.x:scope .a > .b)', [d09]);
test_selector_all(':has(.x:scope .a .b)', [d09, d12]);
test_selector_all(':has(.x:scope .a + .b)', [d12]);
test_selector_all(':has(.x:scope .a ~ .b)', [d02, d12]);
test_selector_all(':has(:scope.x .a)', [d02, d06, d07, d09, d12]);
test_selector_all(':has(:scope.x .a > .b)', [d09]);
test_selector_all(':has(:scope.x .a .b)', [d09, d12]);
test_selector_all(':has(:scope.x .a + .b)', [d12]);
test_selector_all(':has(:scope.x .a ~ .b)', [d02, d12]);
test_selector_all('.x:has(.a)', [d02, d06, d07, d09, d12]);
test_selector_all('.x:has(.a > .b)', [d09]);
test_selector_all('.x:has(.a .b)', [d09, d12]);
test_selector_all('.x:has(.a + .b)', [d12]);
test_selector_all('.x:has(.a ~ .b)', [d02, d12]);

test_selector_all('.x:has(:scope > .a)', [d02, d07, d09, d12]);
test_selector_all('.x:has(:scope > .a > .b)', [d09]);
test_selector_all('.x:has(:scope > .a .b)', [d09, d12]);
test_selector_all('.x:has(:scope > .a + .b)', [d12]);
test_selector_all('.x:has(:scope > .a ~ .b)', [d02, d12]);
test_selector_all(':has(.x:scope > .a)', [d02, d07, d09, d12]);
test_selector_all(':has(.x:scope > .a > .b)', [d09]);
test_selector_all(':has(.x:scope > .a .b)', [d09, d12]);
test_selector_all(':has(.x:scope > .a + .b)', [d12]);
test_selector_all(':has(.x:scope > .a ~ .b)', [d02, d12]);
test_selector_all(':has(:scope.x > .a)', [d02, d07, d09, d12]);
test_selector_all(':has(:scope.x > .a > .b)', [d09]);
test_selector_all(':has(:scope.x > .a .b)', [d09, d12]);
test_selector_all(':has(:scope.x > .a + .b)', [d12]);
test_selector_all(':has(:scope.x > .a ~ .b)', [d02, d12]);
test_selector_all('.x:has(> .a)', [d02, d07, d09, d12]);
test_selector_all('.x:has(> .a > .b)', [d09]);
test_selector_all('.x:has(> .a .b)', [d09, d12]);
test_selector_all('.x:has(> .a + .b)', [d12]);
test_selector_all('.x:has(> .a ~ .b)', [d02, d12]);

test_selector_all('.x:has(:scope + .a)', [d19, d21, d24, d28, d32, d37, d40, d46]);
test_selector_all('.x:has(:scope + .a > .b)', [d21]);
test_selector_all('.x:has(:scope + .a .b)', [d21, d24]);
test_selector_all('.x:has(:scope + .a + .b)', [d28, d32, d37]);
test_selector_all('.x:has(:scope + .a ~ .b)', [d19, d21, d24, d28, d32, d37, d40]);
test_selector_all(':has(.x:scope + .a)', [d19, d21, d24, d28, d32, d37, d40, d46]);
test_selector_all(':has(.x:scope + .a > .b)', [d21]);
test_selector_all(':has(.x:scope + .a .b)', [d21, d24]);
test_selector_all(':has(.x:scope + .a + .b)', [d28, d32, d37]);
test_selector_all(':has(.x:scope + .a ~ .b)', [d19, d21, d24, d28, d32, d37, d40]);
test_selector_all(':has(:scope.x + .a)', [d19, d21, d24, d28, d32, d37, d40, d46]);
test_selector_all(':has(:scope.x + .a > .b)', [d21]);
test_selector_all(':has(:scope.x + .a .b)', [d21, d24]);
test_selector_all(':has(:scope.x + .a + .b)', [d28, d32, d37]);
test_selector_all(':has(:scope.x + .a ~ .b)', [d19, d21, d24, d28, d32, d37, d40]);
test_selector_all('.x:has(+ .a)', [d19, d21, d24, d28, d32, d37, d40, d46]);
test_selector_all('.x:has(+ .a > .b)', [d21]);
test_selector_all('.x:has(+ .a .b)', [d21, d24]);
test_selector_all('.x:has(+ .a + .b)', [d28, d32, d37]);
test_selector_all('.x:has(+ .a ~ .b)', [d19, d21, d24, d28, d32, d37, d40]);

test_selector_all('.x:has(:scope ~ .a)', [d18, d19, d21, d24, d28, d32, d37, d40, d46]);
test_selector_all('.x:has(:scope ~ .a > .b)', [d18, d19, d21]);
test_selector_all('.x:has(:scope ~ .a .b)', [d18, d19, d21, d24]);
test_selector_all('.x:has(:scope ~ .a + .b)', [d18, d19, d21, d24, d28, d32, d37]);
test_selector_all('.x:has(:scope ~ .a + .b > .c)', [d18, d19, d21, d24, d28]);
test_selector_all('.x:has(:scope ~ .a + .b .c)', [d18, d19, d21, d24, d28, d32]);
test_selector_all(':has(.x:scope ~ .a)', [d18, d19, d21, d24, d28, d32, d37, d40, d46]);
test_selector_all(':has(.x:scope ~ .a > .b)', [d18, d19, d21]);
test_selector_all(':has(.x:scope ~ .a .b)', [d18, d19, d21, d24]);
test_selector_all(':has(.x:scope ~ .a + .b)', [d18, d19, d21, d24, d28, d32, d37]);
test_selector_all(':has(.x:scope ~ .a + .b > .c)', [d18, d19, d21, d24, d28]);
test_selector_all(':has(.x:scope ~ .a + .b .c)', [d18, d19, d21, d24, d28, d32]);
test_selector_all(':has(:scope.x ~ .a)', [d18, d19, d21, d24, d28, d32, d37, d40, d46]);
test_selector_all(':has(:scope.x ~ .a > .b)', [d18, d19, d21]);
test_selector_all(':has(:scope.x ~ .a .b)', [d18, d19, d21, d24]);
test_selector_all(':has(:scope.x ~ .a + .b)', [d18, d19, d21, d24, d28, d32, d37]);
test_selector_all(':has(:scope.x ~ .a + .b > .c)', [d18, d19, d21, d24, d28]);
test_selector_all(':has(:scope.x ~ .a + .b .c)', [d18, d19, d21, d24, d28, d32]);
test_selector_all('.x:has(~ .a)', [d18, d19, d21, d24, d28, d32, d37, d40, d46]);
test_selector_all('.x:has(~ .a > .b)', [d18, d19, d21]);
test_selector_all('.x:has(~ .a .b)', [d18, d19, d21, d24]);
test_selector_all('.x:has(~ .a + .b)', [d18, d19, d21, d24, d28, d32, d37]);
test_selector_all('.x:has(~ .a + .b > .c)', [d18, d19, d21, d24, d28]);
test_selector_all('.x:has(~ .a + .b .c)', [d18, d19, d21, d24, d28, d32]);

test_selector_all('.x:has(.d .e)', [d48, d49, d50]);
test_selector_all('.x:has(.d .e) .f', [d54]);
test_selector_all('.x:has(:scope .d .e)', [d48, d49, d50]);
test_selector_all('.x:has(:scope .d .e) .f', [d54]);
test_selector_all('.x:has(:scope > .d)', [d49, d50]);
test_selector_all('.x:has(:scope > .d) .f', [d54]);
test_selector_all('.x:has(:scope ~ .d ~ .e)', [d48, d55, d56]);
test_selector_all('.x:has(:scope ~ .d ~ .e) ~ .f', [d60]);
test_selector_all('.x:has(:scope + .d ~ .e)', [d55, d56]);
test_selector_all('.x:has(:scope + .d ~ .e) ~ .f', [d60]);
test_selector_all(':has(.x:scope .d .e)', [d48, d49, d50]);
test_selector_all(':has(.x:scope .d .e) .f', [d54]);
test_selector_all(':has(.x:scope > .d)', [d49, d50]);
test_selector_all(':has(.x:scope > .d) .f', [d54]);
test_selector_all(':has(.x:scope ~ .d ~ .e)', [d48, d55, d56]);
test_selector_all(':has(.x:scope ~ .d ~ .e) ~ .f', [d60]);
test_selector_all(':has(.x:scope + .d ~ .e)', [d55, d56]);
test_selector_all(':has(.x:scope + .d ~ .e) ~ .f', [d60]);
test_selector_all(':has(:scope.x .d .e)', [d48, d49, d50]);
test_selector_all(':has(:scope.x .d .e) .f', [d54]);
test_selector_all(':has(:scope.x > .d)', [d49, d50]);
test_selector_all(':has(:scope.x > .d) .f', [d54]);
test_selector_all(':has(:scope.x ~ .d ~ .e)', [d48, d55, d56]);
test_selector_all(':has(:scope.x ~ .d ~ .e) ~ .f', [d60]);
test_selector_all(':has(:scope.x + .d ~ .e)', [d55, d56]);
test_selector_all(':has(:scope.x + .d ~ .e) ~ .f', [d60]);
test_selector_all('.x:has(> .d)', [d49, d50]);
test_selector_all('.x:has(> .d) .f', [d54]);
test_selector_all('.x:has(~ .d ~ .e)', [d48, d55, d56]);
test_selector_all('.x:has(~ .d ~ .e) ~ .f', [d60]);
test_selector_all('.x:has(+ .d ~ .e)', [d55, d56]);
test_selector_all('.x:has(+ .d ~ .e) ~ .f', [d60]);

test_selector_all('.y:has(:scope > .g .h)', [d63, d71])
test_selector_all('.y:has(:scope .g .h)', [d63, d68, d71])
test_selector_all('.y:has(:scope > .g .h) .i', [d67, d75])
test_selector_all('.y:has(:scope .g .h) .i', [d67, d75])
test_selector_all('.x:has(:scope + .y:has(:scope > .g .h) .i)', [d62, d70])
test_selector_all('.x:has(:scope + .y:has(:scope .g .h) .i)', [d62, d63, d70])
test_selector_all('.x:has(:scope + .y:has(:scope > .g .h) .i) ~ .j', [d77, d80])
test_selector_all('.x:has(:scope + .y:has(:scope .g .h) .i) ~ .j', [d77, d80])
test_selector_all('.x:has(:scope ~ .y:has(:scope > .g .h) .i)', [d61, d62, d69, d70])
test_selector_all('.x:has(:scope ~ .y:has(:scope .g .h) .i)', [d61, d62, d63, d69, d70])
test_selector_all(':has(.y:scope > .g .h)', [d63, d71])
test_selector_all(':has(.y:scope .g .h)', [d63, d68, d71])
test_selector_all(':has(.y:scope > .g .h) .i', [d67, d75])
test_selector_all(':has(.y:scope .g .h) .i', [d67, d75])
test_selector_all(':has(.x:scope + :has(.y:scope > .g .h) .i)', [d62, d70])
test_selector_all(':has(.x:scope + :has(.y:scope .g .h) .i)', [d62, d63, d70])
test_selector_all(':has(.x:scope + :has(.y:scope > .g .h) .i) ~ .j', [d77, d80])
test_selector_all(':has(.x:scope + :has(.y:scope .g .h) .i) ~ .j', [d77, d80])
test_selector_all(':has(.x:scope ~ :has(.y:scope > .g .h) .i)', [d61, d62, d69, d70])
test_selector_all(':has(.x:scope ~ :has(.y:scope .g .h) .i)', [d61, d62, d63, d69, d70])
test_selector_all(':has(:scope.y > .g .h)', [d63, d71])
test_selector_all(':has(:scope.y .g .h)', [d63, d68, d71])
test_selector_all(':has(:scope.y > .g .h) .i', [d67, d75])
test_selector_all(':has(:scope.y .g .h) .i', [d67, d75])
test_selector_all(':has(:scope.x + :has(:scope.y > .g .h) .i)', [d62, d70])
test_selector_all(':has(:scope.x + :has(:scope.y .g .h) .i)', [d62, d63, d70])
test_selector_all(':has(:scope.x + :has(:scope.y > .g .h) .i) ~ .j', [d77, d80])
test_selector_all(':has(:scope.x + :has(:scope.y .g .h) .i) ~ .j', [d77, d80])
test_selector_all(':has(:scope.x ~ :has(:scope.y > .g .h) .i)', [d61, d62, d69, d70])
test_selector_all(':has(:scope.x ~ :has(:scope.y .g .h) .i)', [d61, d62, d63, d69, d70])
test_selector_all('.y:has(> .g .h)', [d63, d71])
test_selector_all('.y:has(.g .h)', [d63, d68, d71])
test_selector_all('.y:has(> .g .h) .i', [d67, d75])
test_selector_all('.y:has(.g .h) .i', [d67, d75])
test_selector_all('.x:has(+ .y:has(> .g .h) .i)', [d62, d70])
test_selector_all('.x:has(+ .y:has(.g .h) .i)', [d62, d63, d70])
test_selector_all('.x:has(+ .y:has(> .g .h) .i) ~ .j', [d77, d80])
test_selector_all('.x:has(+ .y:has(.g .h) .i) ~ .j', [d77, d80])
test_selector_all('.x:has(~ .y:has(> .g .h) .i)', [d61, d62, d69, d70])
test_selector_all('.x:has(~ .y:has(.g .h) .i)', [d61, d62, d63, d69, d70])

test_selector_all('.x:has(.d :scope .e)', [d51, d52])
test_selector_all(':has(.d .x:scope .e)', [d51, d52])
test_selector_all(':has(.d :scope.x .e)', [d51, d52])
test_selector_all('.d .x:has(.e)', [d51, d52])

test_selector_all('.x:has(.d ~ :scope ~ .e)', [d57, d58])
test_selector_all(':has(.d ~ .x:scope ~ .e)', [d57, d58])
test_selector_all(':has(.d ~ :scope.x ~ .e)', [d57, d58])
test_selector_all('.d ~ .x:has(~ .e)', [d57, d58])

test_selector_all(':has(:scope .d :scope)', [])
test_selector_all(':has(:scope ~ .d ~ :scope)', [])
</script>
13 changes: 0 additions & 13 deletions css/selectors/parsing/parse-has.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,9 @@
test_valid_selector(':has(:hover)');
test_valid_selector('*:has(.a)', ['*:has(.a)', ':has(.a)']);
test_valid_selector('.a:has(.b)');
test_valid_selector('.a:has(:scope .b)');
test_valid_selector(':has(.a:scope .b)');
test_valid_selector(':has(.a .b:scope)');
test_valid_selector('.a:has(> .b)');
test_valid_selector('.a:has(:scope > .b)');
test_valid_selector(':has(.a:scope > .b)');
test_valid_selector(':has(> .a .b:scope)');
test_valid_selector('.a:has(~ .b)');
test_valid_selector('.a:has(:scope ~ .b)');
test_valid_selector(':has(.a:scope ~ .b)');
test_valid_selector(':has(~ .a .b:scope)');
test_valid_selector('.a:has(+ .b)');
test_valid_selector('.a:has(:scope + .b)');
test_valid_selector(':has(.a:scope + .b)');
test_valid_selector(':has(+ .a .b:scope)');
test_valid_selector('.a:has(:scope .b :scope)');
test_valid_selector('.a:has(.b) .c');
test_valid_selector('.a .b:has(.c)');
test_valid_selector('.a .b:has(.c .d)');
Expand Down

0 comments on commit 6f5e733

Please sign in to comment.