From 0c47af3f0819342bb2cffe0de61dd3dfb6b4c87b Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 7 Aug 2023 16:01:12 +0200 Subject: [PATCH] Add support for `a:has(> b)` --- lib/walk.js | 36 ++++++++++++++++++++++++++++++------ readme.md | 7 ++----- test/matches.js | 47 ++++++++++++++++++++++------------------------- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/lib/walk.js b/lib/walk.js index 096b91d..b21f779 100644 --- a/lib/walk.js +++ b/lib/walk.js @@ -44,7 +44,7 @@ const empty = [] */ export function walk(state, tree) { if (tree) { - one(state, [], tree, undefined, undefined) + one(state, [], tree, undefined, undefined, tree) } } @@ -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 | undefined} */ let fromSibling @@ -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) } @@ -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, @@ -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 @@ -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() diff --git a/readme.md b/readme.md index 2d2e0be..b3c5d70 100644 --- a/readme.md +++ b/readme.md @@ -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) @@ -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 @@ -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 diff --git a/test/matches.js b/test/matches.js index cbfeeb1..31df49f 100644 --- a/test/matches.js +++ b/test/matches.js @@ -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) {