Skip to content

Commit 3a5f97a

Browse files
committed
Add support for a:has(> b)
1 parent 414d1f7 commit 3a5f97a

File tree

3 files changed

+36
-24
lines changed

3 files changed

+36
-24
lines changed

lib/walk.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const empty = []
3737
*/
3838
export function walk(state, tree) {
3939
if (tree) {
40-
one(state, [], tree, undefined, undefined)
40+
one(state, [], tree, undefined, undefined, tree)
4141
}
4242
}
4343

@@ -49,9 +49,10 @@ export function walk(state, tree) {
4949
* @param {Node} node
5050
* @param {number | undefined} index
5151
* @param {Parent | undefined} parentNode
52+
* @param {Node} tree
5253
* @returns {Nest}
5354
*/
54-
function one(state, currentRules, node, index, parentNode) {
55+
function one(state, currentRules, node, index, parentNode, tree) {
5556
/** @type {Nest} */
5657
let nestResult = {
5758
directChild: undefined,
@@ -60,10 +61,23 @@ function one(state, currentRules, node, index, parentNode) {
6061
generalSibling: undefined
6162
}
6263

64+
let rootRules = state.rootQuery.rules
65+
66+
// Remove direct child rules if this is the root.
67+
// This only happens for a `:has()` rule, which can be like
68+
// `a:has(> b)`.
69+
if (parentNode && parentNode !== tree) {
70+
rootRules = state.rootQuery.rules.filter(
71+
(d) =>
72+
d.combinator === undefined ||
73+
(d.combinator === '>' && parentNode === tree)
74+
)
75+
}
76+
6377
nestResult = applySelectors(
6478
state,
6579
// Try the root rules for this node too.
66-
combine(currentRules, state.rootQuery.rules),
80+
combine(currentRules, rootRules),
6781
node,
6882
index,
6983
parentNode
@@ -72,7 +86,7 @@ function one(state, currentRules, node, index, parentNode) {
7286
// If this is a parent, and we want to delve into them, and we haven’t found
7387
// our single result yet.
7488
if (parent(node) && !state.shallow && !(state.one && state.found)) {
75-
all(state, nestResult, node)
89+
all(state, nestResult, node, tree)
7690
}
7791

7892
return nestResult
@@ -84,9 +98,10 @@ function one(state, currentRules, node, index, parentNode) {
8498
* @param {SelectState} state
8599
* @param {Nest} nest
86100
* @param {Parent} node
101+
* @param {Node} tree
87102
* @returns {undefined}
88103
*/
89-
function all(state, nest, node) {
104+
function all(state, nest, node, tree) {
90105
const fromParent = combine(nest.descendant, nest.directChild)
91106
/** @type {Array<AstRule> | undefined} */
92107
let fromSibling
@@ -121,7 +136,7 @@ function all(state, nest, node) {
121136

122137
// Only apply if this is a parent.
123138
const forSibling = combine(fromParent, fromSibling)
124-
const nest = one(state, forSibling, node.children[index], index, node)
139+
const nest = one(state, forSibling, node.children[index], index, node, tree)
125140
fromSibling = combine(nest.generalSibling, nest.adjacentSibling)
126141

127142
// We found one thing, and one is enough.
@@ -208,7 +223,7 @@ function applySelectors(state, rules, node, index, parent) {
208223
else if (rule.combinator === '~') {
209224
add(nestResult, 'generalSibling', rule)
210225
}
211-
// Drop top-level nesting (`undefined`), direct child (`>`), adjacent sibling (`+`).
226+
// Drop direct child (`>`), adjacent sibling (`+`).
212227
}
213228

214229
return nestResult

readme.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,7 @@ Yields:
248248
* [x] `[attr~=value]` (attribute contains, checks if `value` is in the array,
249249
if there’s an array on the tree, otherwise same as attribute equality)
250250
* [x] `:is()` (functional pseudo-class)
251-
* [x] `:has()` (functional pseudo-class)
252-
Relative selectors (`:has(> img)`) are not supported, but `:scope` is
251+
* [x] `:has()` (functional pseudo-class; also supports `a:has(> b)`)
253252
* [x] `:not()` (functional pseudo-class)
254253
* [x] `:blank` (pseudo-class, blank and empty are the same: a parent without
255254
children, or a node without value)
@@ -271,8 +270,7 @@ Yields:
271270
###### Notes
272271

273272
* \* — not supported in `matches`
274-
275-
`:any()` and `:matches()` are renamed to `:is()` in CSS.
273+
* `:any()` and `:matches()` are renamed to `:is()` in CSS
276274

277275
## Types
278276

test/matches.js

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -831,19 +831,18 @@ test('select.matches()', async function (t) {
831831
assert.ok(matches('a:has( b ,\t p )', u('a', [u('b')])))
832832
})
833833

834-
// Note: These should not be uncommented, but that’s not supported by the CSS parser.
835-
// assert.ok(
836-
// matches('a:has(> b)', u('a', [u('b')])),
837-
// 'should yield `true` for relative direct child selector'
838-
// )
839-
// assert.ok(
840-
// !matches('a:has(> c)', u('a', [u('b', [u('c')])])),
841-
// 'should yield `false` for relative direct child selectors'
842-
// )
843-
// assert.ok(
844-
// matches('a:has(> c, > b)', u('a', [u('b', [u('b')])])),
845-
// 'should support a list of relative selectors'
846-
// )
834+
assert.ok(
835+
matches('a:has(> b)', u('a', [u('b')])),
836+
'should yield `true` for relative direct child selector'
837+
)
838+
assert.ok(
839+
!matches('a:has(> c)', u('a', [u('b', [u('c')])])),
840+
'should yield `false` for relative direct child selectors'
841+
)
842+
assert.ok(
843+
matches('a:has(> c, > b)', u('a', [u('b', [u('b')])])),
844+
'should support a list of relative selectors'
845+
)
847846
})
848847

849848
const emptyBlankPseudos = [':empty', ':blank']

0 commit comments

Comments
 (0)