Skip to content

Commit

Permalink
Add support for a:has(> b)
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Aug 7, 2023
1 parent 2b1144c commit 0c47af3
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 36 deletions.
36 changes: 30 additions & 6 deletions lib/walk.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const empty = []
*/
export function walk(state, tree) {
if (tree) {
one(state, [], tree, undefined, undefined)
one(state, [], tree, undefined, undefined, tree)
}
}

Expand Down Expand Up @@ -76,10 +76,12 @@ function add(nest, field, rule) {
* Nesting.
* @param {Parents} node
* Parent.
* @param {Nodes} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
function all(state, nest, node) {
function all(state, nest, node, tree) {
const fromParent = combine(nest.descendant, nest.directChild)
/** @type {Array<AstRule> | undefined} */
let fromSibling
Expand Down Expand Up @@ -118,7 +120,14 @@ function all(state, nest, node) {
// for parents so that we delve into custom nodes too.
if ('children' in child) {
const forSibling = combine(fromParent, fromSibling)
const nest = one(state, forSibling, node.children[index], index, node)
const nest = one(
state,
forSibling,
node.children[index],
index,
node,
tree
)
fromSibling = combine(nest.generalSibling, nest.adjacentSibling)
}

Expand Down Expand Up @@ -268,10 +277,12 @@ function count(counts, node) {
* Index of `node` in `parent`.
* @param {Parents | undefined} parent
* Parent of `node`.
* @param {Nodes} tree
* Tree.
* @returns {Nest}
* Nesting.
*/
function one(state, currentRules, node, index, parent) {
function one(state, currentRules, node, index, parent, tree) {
/** @type {Nest} */
let nestResult = {
adjacentSibling: undefined,
Expand All @@ -283,10 +294,23 @@ function one(state, currentRules, node, index, parent) {
const exit = enterState(state, node)

if (node.type === 'element') {
let rootRules = state.rootQuery.rules

// Remove direct child rules if this is the root.
// This only happens for a `:has()` rule, which can be like
// `a:has(> b)`.
if (parent && parent !== tree) {
rootRules = state.rootQuery.rules.filter(
(d) =>
d.combinator === undefined ||
(d.combinator === '>' && parent === tree)
)
}

nestResult = applySelectors(
state,
// Try the root rules for this element too.
combine(currentRules, state.rootQuery.rules),
combine(currentRules, rootRules),
node,
index,
parent
Expand All @@ -296,7 +320,7 @@ function one(state, currentRules, node, index, parent) {
// If this is a parent, and we want to delve into them, and we haven’t found
// our single result yet.
if ('children' in node && !state.shallow && !(state.one && state.found)) {
all(state, nestResult, node)
all(state, nestResult, node, tree)
}

exit()
Expand Down
7 changes: 2 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ type Space = 'html' | 'svg'
* [x] `[attr$=value]` (attribute ends with)
* [x] `[attr*=value]` (attribute contains)
* [x] `:dir()` (functional pseudo-class)
* [x] `:has()` (functional pseudo-class)
* [x] `:has()` (functional pseudo-class; also supports `a:has(> b)`)
* [x] `:is()` (functional pseudo-class)
* [x] `:lang()` (functional pseudo-class)
* [x] `:not()` (functional pseudo-class)
Expand Down Expand Up @@ -313,8 +313,6 @@ type Space = 'html' | 'svg'
* [ ] ‡ `[*|attr]` (any namespace attribute)
* [ ] ‡ `[|attr]` (no namespace attribute)
* [ ] ‡ `[attr=value i]` (attribute case-insensitive)
* [ ] ‡ `:has()` (functional pseudo-class, note: relative selectors such as
`:has(> img)` are not supported, but scope is: `:has(:scope > img)`)
* [ ] ‖ `:nth-child(n of S)` (functional pseudo-class, note: scoping to
parents is not supported)
* [ ] ‖ `:nth-last-child(n of S)` (functional pseudo-class, note: scoping to
Expand Down Expand Up @@ -362,8 +360,7 @@ type Space = 'html' | 'svg'
* ‡ — not supported by the underlying algorithm
* § — not very interested in writing / including the code for this
* ‖ — too new, the spec is still changing
`:any()` and `:matches()` are renamed to `:is()` in CSS.
* `:any()` and `:matches()` are renamed to `:is()` in CSS.
## Types
Expand Down
47 changes: 22 additions & 25 deletions test/matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -1346,31 +1346,28 @@ test('select.matches()', async function (t) {
assert.ok(matches('a:has( img ,\t p )', h('a', h('img'))))
})

// To do: add `:has(>)`.
// Note: These should be uncommented, but that’s not supported by the CSS
// parser:
// await t.test(
// 'should match for relative direct child selector',
// async function () {
// assert.ok(matches('a:has(> img)', h('a', h('img'))))
// }
// )

// await t.test(
// 'should not match for relative direct child selectors',
// async function () {
// assert.ok(!matches('a:has(> img)', h('a', h('span', h('img')))))
// }
// )

// await t.test(
// 'should support a list of relative selectors',
// async function () {
// assert.ok(
// matches('a:has(> img, > span)', h('a', h('span', h('span'))))
// )
// }
// )
await t.test(
'should match for relative direct child selector',
async function () {
assert.ok(matches('a:has(> img)', h('a', h('img'))))
}
)

await t.test(
'should not match for relative direct child selectors',
async function () {
assert.ok(!matches('a:has(> img)', h('a', h('span', h('img')))))
}
)

await t.test(
'should support a list of relative selectors',
async function () {
assert.ok(
matches('a:has(> img, > span)', h('a', h('span', h('span'))))
)
}
)
})

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

0 comments on commit 0c47af3

Please sign in to comment.