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 Jun 30, 2023
1 parent 414d1f7 commit 3a5f97a
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 24 deletions.
29 changes: 22 additions & 7 deletions lib/walk.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const empty = []
*/
export function walk(state, tree) {
if (tree) {
one(state, [], tree, undefined, undefined)
one(state, [], tree, undefined, undefined, tree)
}
}

Expand All @@ -49,9 +49,10 @@ export function walk(state, tree) {
* @param {Node} node
* @param {number | undefined} index
* @param {Parent | undefined} parentNode
* @param {Node} tree
* @returns {Nest}
*/
function one(state, currentRules, node, index, parentNode) {
function one(state, currentRules, node, index, parentNode, tree) {
/** @type {Nest} */
let nestResult = {
directChild: undefined,
Expand All @@ -60,10 +61,23 @@ function one(state, currentRules, node, index, parentNode) {
generalSibling: undefined
}

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 (parentNode && parentNode !== tree) {
rootRules = state.rootQuery.rules.filter(
(d) =>
d.combinator === undefined ||
(d.combinator === '>' && parentNode === tree)
)
}

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

return nestResult
Expand All @@ -84,9 +98,10 @@ function one(state, currentRules, node, index, parentNode) {
* @param {SelectState} state
* @param {Nest} nest
* @param {Parent} node
* @param {Node} tree
* @returns {undefined}
*/
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 @@ -121,7 +136,7 @@ function all(state, nest, node) {

// Only apply if this is a parent.
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)

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

return nestResult
Expand Down
6 changes: 2 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,7 @@ Yields:
* [x] `[attr~=value]` (attribute contains, checks if `value` is in the array,
if there’s an array on the tree, otherwise same as attribute equality)
* [x] `:is()` (functional pseudo-class)
* [x] `:has()` (functional pseudo-class)
Relative selectors (`:has(> img)`) are not supported, but `:scope` is
* [x] `:has()` (functional pseudo-class; also supports `a:has(> b)`)
* [x] `:not()` (functional pseudo-class)
* [x] `:blank` (pseudo-class, blank and empty are the same: a parent without
children, or a node without value)
Expand All @@ -271,8 +270,7 @@ Yields:
###### Notes

* \* — not supported in `matches`

`:any()` and `:matches()` are renamed to `:is()` in CSS.
* `:any()` and `:matches()` are renamed to `:is()` in CSS

## Types

Expand Down
25 changes: 12 additions & 13 deletions test/matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -831,19 +831,18 @@ test('select.matches()', async function (t) {
assert.ok(matches('a:has( b ,\t p )', u('a', [u('b')])))
})

// Note: These should not be uncommented, but that’s not supported by the CSS parser.
// assert.ok(
// matches('a:has(> b)', u('a', [u('b')])),
// 'should yield `true` for relative direct child selector'
// )
// assert.ok(
// !matches('a:has(> c)', u('a', [u('b', [u('c')])])),
// 'should yield `false` for relative direct child selectors'
// )
// assert.ok(
// matches('a:has(> c, > b)', u('a', [u('b', [u('b')])])),
// 'should support a list of relative selectors'
// )
assert.ok(
matches('a:has(> b)', u('a', [u('b')])),
'should yield `true` for relative direct child selector'
)
assert.ok(
!matches('a:has(> c)', u('a', [u('b', [u('c')])])),
'should yield `false` for relative direct child selectors'
)
assert.ok(
matches('a:has(> c, > b)', u('a', [u('b', [u('b')])])),
'should support a list of relative selectors'
)
})

const emptyBlankPseudos = [':empty', ':blank']
Expand Down

0 comments on commit 3a5f97a

Please sign in to comment.