Skip to content

Commit 0c47af3

Browse files
committed
Add support for a:has(> b)
1 parent 2b1144c commit 0c47af3

File tree

3 files changed

+54
-36
lines changed

3 files changed

+54
-36
lines changed

lib/walk.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const empty = []
4444
*/
4545
export function walk(state, tree) {
4646
if (tree) {
47-
one(state, [], tree, undefined, undefined)
47+
one(state, [], tree, undefined, undefined, tree)
4848
}
4949
}
5050

@@ -76,10 +76,12 @@ function add(nest, field, rule) {
7676
* Nesting.
7777
* @param {Parents} node
7878
* Parent.
79+
* @param {Nodes} tree
80+
* Tree.
7981
* @returns {undefined}
8082
* Nothing.
8183
*/
82-
function all(state, nest, node) {
84+
function all(state, nest, node, tree) {
8385
const fromParent = combine(nest.descendant, nest.directChild)
8486
/** @type {Array<AstRule> | undefined} */
8587
let fromSibling
@@ -118,7 +120,14 @@ function all(state, nest, node) {
118120
// for parents so that we delve into custom nodes too.
119121
if ('children' in child) {
120122
const forSibling = combine(fromParent, fromSibling)
121-
const nest = one(state, forSibling, node.children[index], index, node)
123+
const nest = one(
124+
state,
125+
forSibling,
126+
node.children[index],
127+
index,
128+
node,
129+
tree
130+
)
122131
fromSibling = combine(nest.generalSibling, nest.adjacentSibling)
123132
}
124133

@@ -268,10 +277,12 @@ function count(counts, node) {
268277
* Index of `node` in `parent`.
269278
* @param {Parents | undefined} parent
270279
* Parent of `node`.
280+
* @param {Nodes} tree
281+
* Tree.
271282
* @returns {Nest}
272283
* Nesting.
273284
*/
274-
function one(state, currentRules, node, index, parent) {
285+
function one(state, currentRules, node, index, parent, tree) {
275286
/** @type {Nest} */
276287
let nestResult = {
277288
adjacentSibling: undefined,
@@ -283,10 +294,23 @@ function one(state, currentRules, node, index, parent) {
283294
const exit = enterState(state, node)
284295

285296
if (node.type === 'element') {
297+
let rootRules = state.rootQuery.rules
298+
299+
// Remove direct child rules if this is the root.
300+
// This only happens for a `:has()` rule, which can be like
301+
// `a:has(> b)`.
302+
if (parent && parent !== tree) {
303+
rootRules = state.rootQuery.rules.filter(
304+
(d) =>
305+
d.combinator === undefined ||
306+
(d.combinator === '>' && parent === tree)
307+
)
308+
}
309+
286310
nestResult = applySelectors(
287311
state,
288312
// Try the root rules for this element too.
289-
combine(currentRules, state.rootQuery.rules),
313+
combine(currentRules, rootRules),
290314
node,
291315
index,
292316
parent
@@ -296,7 +320,7 @@ function one(state, currentRules, node, index, parent) {
296320
// If this is a parent, and we want to delve into them, and we haven’t found
297321
// our single result yet.
298322
if ('children' in node && !state.shallow && !(state.one && state.found)) {
299-
all(state, nestResult, node)
323+
all(state, nestResult, node, tree)
300324
}
301325

302326
exit()

readme.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ type Space = 'html' | 'svg'
276276
* [x] `[attr$=value]` (attribute ends with)
277277
* [x] `[attr*=value]` (attribute contains)
278278
* [x] `:dir()` (functional pseudo-class)
279-
* [x] `:has()` (functional pseudo-class)
279+
* [x] `:has()` (functional pseudo-class; also supports `a:has(> b)`)
280280
* [x] `:is()` (functional pseudo-class)
281281
* [x] `:lang()` (functional pseudo-class)
282282
* [x] `:not()` (functional pseudo-class)
@@ -313,8 +313,6 @@ type Space = 'html' | 'svg'
313313
* [ ] ‡ `[*|attr]` (any namespace attribute)
314314
* [ ] ‡ `[|attr]` (no namespace attribute)
315315
* [ ] ‡ `[attr=value i]` (attribute case-insensitive)
316-
* [ ] ‡ `:has()` (functional pseudo-class, note: relative selectors such as
317-
`:has(> img)` are not supported, but scope is: `:has(:scope > img)`)
318316
* [ ] ‖ `:nth-child(n of S)` (functional pseudo-class, note: scoping to
319317
parents is not supported)
320318
* [ ] ‖ `:nth-last-child(n of S)` (functional pseudo-class, note: scoping to
@@ -362,8 +360,7 @@ type Space = 'html' | 'svg'
362360
* ‡ — not supported by the underlying algorithm
363361
* § — not very interested in writing / including the code for this
364362
* ‖ — too new, the spec is still changing
365-
366-
`:any()` and `:matches()` are renamed to `:is()` in CSS.
363+
* `:any()` and `:matches()` are renamed to `:is()` in CSS.
367364
368365
## Types
369366

test/matches.js

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,31 +1346,28 @@ test('select.matches()', async function (t) {
13461346
assert.ok(matches('a:has( img ,\t p )', h('a', h('img'))))
13471347
})
13481348

1349-
// To do: add `:has(>)`.
1350-
// Note: These should be uncommented, but that’s not supported by the CSS
1351-
// parser:
1352-
// await t.test(
1353-
// 'should match for relative direct child selector',
1354-
// async function () {
1355-
// assert.ok(matches('a:has(> img)', h('a', h('img'))))
1356-
// }
1357-
// )
1358-
1359-
// await t.test(
1360-
// 'should not match for relative direct child selectors',
1361-
// async function () {
1362-
// assert.ok(!matches('a:has(> img)', h('a', h('span', h('img')))))
1363-
// }
1364-
// )
1365-
1366-
// await t.test(
1367-
// 'should support a list of relative selectors',
1368-
// async function () {
1369-
// assert.ok(
1370-
// matches('a:has(> img, > span)', h('a', h('span', h('span'))))
1371-
// )
1372-
// }
1373-
// )
1349+
await t.test(
1350+
'should match for relative direct child selector',
1351+
async function () {
1352+
assert.ok(matches('a:has(> img)', h('a', h('img'))))
1353+
}
1354+
)
1355+
1356+
await t.test(
1357+
'should not match for relative direct child selectors',
1358+
async function () {
1359+
assert.ok(!matches('a:has(> img)', h('a', h('span', h('img')))))
1360+
}
1361+
)
1362+
1363+
await t.test(
1364+
'should support a list of relative selectors',
1365+
async function () {
1366+
assert.ok(
1367+
matches('a:has(> img, > span)', h('a', h('span', h('span'))))
1368+
)
1369+
}
1370+
)
13741371
})
13751372

13761373
await t.test(':any-link', async function (t) {

0 commit comments

Comments
 (0)