Skip to content

Commit b7b5597

Browse files
committed
Add support for :has() selector
1 parent 943a117 commit b7b5597

File tree

9 files changed

+284
-75
lines changed

9 files changed

+284
-75
lines changed

index.js

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
'use strict'
22

3-
var Parser = require('css-selector-parser').CssSelectorParser
4-
var attributes = require('./lib/attribute')
5-
var pseudos = require('./lib/pseudo')
6-
var any = require('./lib/any')
7-
var nesting = require('./lib/nest')
8-
var compile = require('./lib/compile')
9-
10-
var parser = new Parser()
11-
12-
parser.registerAttrEqualityMods.apply(parser, attributes.support)
13-
parser.registerSelectorPseudos.apply(parser, pseudos.selectorPseudoSupport)
14-
parser.registerNestingOperators.apply(parser, nesting.support)
15-
163
exports.matches = matches
174
exports.selectAll = selectAll
185
exports.select = select
196

7+
var any = require('./lib/any')
8+
var parse = require('./lib/parse')
9+
2010
function matches(selector, node, space) {
2111
return Boolean(
2212
any(parse(selector), node, {space: space, one: true, shallow: true})[0]
@@ -30,11 +20,3 @@ function select(selector, node, space) {
3020
function selectAll(selector, node, space) {
3121
return any(parse(selector), node, {space: space})
3222
}
33-
34-
function parse(selector) {
35-
if (typeof selector !== 'string') {
36-
throw new Error('Expected `string` as selector, not `' + selector + '`')
37-
}
38-
39-
return compile(parser.parse(selector))
40-
}

lib/any.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function rule(query, tree, state) {
4747
language: undefined,
4848
direction: 'ltr',
4949
editableOrEditingHost: false,
50+
scopeElements: tree.type === 'root' ? tree.children : [tree],
5051
iterator: match,
5152
one: state.one,
5253
shallow: state.shallow

lib/attribute.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ var commaSeparated = require('comma-separated-tokens').stringify
1111
var handle = zwitch('operator')
1212
var handlers = handle.handlers
1313

14-
match.support = ['~', '|', '^', '$', '*']
15-
1614
handle.unknown = unknownOperator
1715
handle.invalid = exists
1816
handlers['='] = exact

lib/enter-state.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
/* eslint-disable complexity */
44

5+
module.exports = enter
6+
57
var svg = require('property-information/svg')
68
var direction = require('direction')
79
var visit = require('unist-util-visit')
@@ -15,9 +17,7 @@ var valueTypes = ['text', 'search', 'tel', 'url', 'email']
1517
var validDirections = [ltr, rtl, auto]
1618
var ignoreElements = ['bdi', 'script', 'style', 'textare']
1719

18-
module.exports = config
19-
20-
function config(state, node) {
20+
function enter(state, node) {
2121
var schema = state.schema
2222
var space = schema.space
2323
var language = state.language

lib/nest.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
'use strict'
22

3+
module.exports = match
4+
35
var zwitch = require('zwitch')
46
var enter = require('./enter-state')
57

6-
module.exports = zwitch('nestingOperator')
7-
88
var own = {}.hasOwnProperty
99
var slice = [].slice
1010

11-
var handle = module.exports
11+
var handle = zwitch('nestingOperator')
1212
var handlers = handle.handlers
1313

14-
handle.support = ['>', '+', '~']
15-
1614
handle.unknown = unknownNesting
1715
handle.invalid = topScan /* `undefined` is the top query selector. */
1816
handlers.null = descendant /* `null` is the descendant combinator. */
1917
handlers['>'] = child
2018
handlers['+'] = adjacentSibling
2119
handlers['~'] = generalSibling
2220

21+
function match(query, node, index, parent, state) {
22+
return handle(query, node, index, parent, state)
23+
}
24+
2325
/* istanbul ignore next - Shouldn’t be invoked, parser gives correct data. */
2426
function unknownNesting(query) {
2527
throw new Error('Unexpected nesting `' + query.nestingOperator + '`')

lib/compile.js renamed to lib/parse.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
'use strict'
22

3+
module.exports = parse
4+
5+
var Parser = require('css-selector-parser').CssSelectorParser
36
var zwitch = require('zwitch')
47
var nthCheck = require('nth-check')
58

6-
module.exports = zwitch('type')
9+
var nth = ['nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type']
710

8-
var compile = module.exports
11+
var parser = new Parser()
12+
var compile = zwitch('type')
913
var handlers = compile.handlers
1014

11-
var nth = ['nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type']
15+
parser.registerAttrEqualityMods('~', '|', '^', '$', '*')
16+
parser.registerSelectorPseudos('any', 'matches', 'not', 'has')
17+
parser.registerNestingOperators('>', '+', '~')
1218

1319
handlers.selectors = selectors
1420
handlers.ruleSet = ruleSet
1521
handlers.rule = rule
1622

23+
function parse(selector) {
24+
if (typeof selector !== 'string') {
25+
throw new Error('Expected `string` as selector, not `' + selector + '`')
26+
}
27+
28+
return compile(parser.parse(selector))
29+
}
30+
1731
function selectors(query) {
1832
var selectors = query.selectors
1933
var length = selectors.length

lib/pseudo.js

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
'use strict'
22

3+
/* eslint-disable complexity */
4+
5+
module.exports = match
6+
37
var commaSeparated = require('comma-separated-tokens').parse
48
var filter = require('bcp-47-match').extendedFilter
59
var zwitch = require('zwitch')
@@ -8,10 +12,6 @@ var is = require('hast-util-is-element')
812
var has = require('hast-util-has-property')
913
var whitespace = require('hast-util-whitespace')
1014

11-
module.exports = match
12-
13-
match.selectorPseudoSupport = ['any', 'matches', 'not']
14-
1515
match.needsIndex = [
1616
'first-child',
1717
'first-of-type',
@@ -56,6 +56,7 @@ handlers.empty = empty
5656
handlers.enabled = not(disabled)
5757
handlers['first-child'] = firstChild
5858
handlers['first-of-type'] = firstOfType
59+
handlers.has = hasSelector
5960
handlers.lang = lang
6061
handlers['last-child'] = lastChild
6162
handlers['last-of-type'] = lastOfType
@@ -96,7 +97,7 @@ function matches(query, node, index, parent, state) {
9697
state.shallow = true
9798
state.one = true
9899

99-
result = Boolean(anything(query.value, node, state)[0])
100+
result = anything(query.value, node, state)[0] === node
100101

101102
state.shallow = shallow
102103
state.one = one
@@ -155,8 +156,8 @@ function root(query, node, index, parent, state) {
155156
)
156157
}
157158

158-
function scope(query, node, index, parent) {
159-
return (!parent || parent.type === 'root') && is(node)
159+
function scope(query, node, index, parent, state) {
160+
return is(node) && state.scopeElements.indexOf(node) !== -1
160161
}
161162

162163
function empty(query, node) {
@@ -267,3 +268,55 @@ function assertDeep(state, query) {
267268
throw new Error('Cannot use `:' + query.name + '` without parent')
268269
}
269270
}
271+
272+
function hasSelector(query, node, index, parent, state) {
273+
var shallow = state.shallow
274+
var one = state.one
275+
var scopeElements = state.scopeElements
276+
var value = appendScope(query.value)
277+
var result
278+
279+
state.shallow = false
280+
state.one = true
281+
state.scopeElements = [node]
282+
283+
result = anything(value, node, state)[0]
284+
285+
state.shallow = shallow
286+
state.one = one
287+
state.scopeElements = scopeElements
288+
289+
return result
290+
}
291+
292+
function appendScope(selector) {
293+
var selectors
294+
var length
295+
var index
296+
var rule
297+
298+
if (selector.type === 'ruleSet') {
299+
selector = {type: 'selectors', selectors: [selector]}
300+
}
301+
302+
selectors = selector.selectors
303+
length = selectors.length
304+
index = -1
305+
306+
while (++index < length) {
307+
rule = selectors[index].rule
308+
rule.nestingOperator = null
309+
310+
if (
311+
!rule.pseudos ||
312+
rule.pseudos.length !== 1 ||
313+
rule.pseudos[0].name !== 'scope'
314+
) {
315+
rule = {type: 'rule', rule: rule, pseudos: [{name: 'scope'}]}
316+
}
317+
318+
selectors[index] = rule
319+
}
320+
321+
return selector
322+
}

readme.md

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -153,23 +153,28 @@ Yields:
153153

154154
## Support
155155

156-
* [x] `*` (universal selector, namespaces not supported)
156+
* [x] `*` (universal selector)
157157
* [x] `,` (multiple selector)
158158
* [x] `p` (type selector)
159159
* [x] `.class` (class selector)
160160
* [x] `#id` (id selector)
161+
* [x] `article p` (combinator: descendant selector)
162+
* [x] `article > p` (combinator: child selector)
163+
* [x] `h1 + p` (combinator: adjacent sibling selector)
164+
* [x] `h1 ~ p` (combinator: general sibling selector)
161165
* [x] `[attr]` (attribute existence)
162166
* [x] `[attr=value]` (attribute equality)
163167
* [x] `[attr~=value]` (attribute contains in space-separated list)
164168
* [x] `[attr|=value]` (attribute equality or prefix)
165169
* [x] `[attr^=value]` (attribute begins with)
166170
* [x] `[attr$=value]` (attribute ends with)
167171
* [x] `[attr*=value]` (attribute contains)
168-
* [x] `:any()` (pseudo-class, use `:matches` instead)
169-
* [x] `:dir()` (pseudo-class)
170-
* [x] `:lang()` (pseudo-class)
171-
* [x] `:matches()` (pseudo-class)
172-
* [x] `:not()` (pseudo-class)
172+
* [x] `:any()` (functional pseudo-class, use `:matches` instead)
173+
* [x] `:dir()` (functional pseudo-class)
174+
* [x] `:has()` (functional pseudo-class)
175+
* [x] `:lang()` (functional pseudo-class)
176+
* [x] `:matches()` (functional pseudo-class)
177+
* [x] `:not()` (functional pseudo-class)
173178
* [x] `:any-link` (pseudo-class)
174179
* [x] `:blank` (pseudo-class)
175180
* [x] `:checked` (pseudo-class)
@@ -182,47 +187,60 @@ Yields:
182187
* [x] `:required` (pseudo-class)
183188
* [x] `:root` (pseudo-class)
184189
* [x] `:scope` (pseudo-class):
185-
* [x] `article p` (combinator: descendant selector)
186-
* [x] `article > p` (combinator: child selector)
187-
* [x] `h1 + p` (combinator: adjacent sibling selector)
188-
* [x] `h1 ~ p` (combinator: general sibling selector)
189190
* [x] \* `:first-child` (pseudo-class)
190191
* [x] \* `:first-of-type` (pseudo-class)
191192
* [x] \* `:last-child` (pseudo-class)
192193
* [x] \* `:last-of-type` (pseudo-class)
193194
* [x] \* `:only-child` (pseudo-class)
194195
* [x] \* `:only-of-type` (pseudo-class)
195-
* [x] \* `:nth-child()` (pseudo-class)
196-
* [x] \* `:nth-last-child()` (pseudo-class)
197-
* [x] \* `:nth-last-of-type()` (pseudo-class)
198-
* [x] \* `:nth-of-type()` (pseudo-class)
196+
* [x] \* `:nth-child()` (functional pseudo-class)
197+
* [x] \* `:nth-last-child()` (functional pseudo-class)
198+
* [x] \* `:nth-last-of-type()` (functional pseudo-class)
199+
* [x] \* `:nth-of-type()` (functional pseudo-class)
199200

200201
## Unsupported
201202

202-
* [ ] `>>` (explicit descendant combinator)
203-
* [ ] `||` (column combinator)
203+
* [ ]`||` (column combinator)
204+
* [ ]`ns|E` (namespace type selector)
205+
* [ ]`*|E` (any namespace type selector)
206+
* [ ]`|E` (no namespace type selector)
207+
* [ ]`[ns|attr]` (namespace attribute)
208+
* [ ]`[*|attr]` (any namespace attribute)
209+
* [ ]`[|attr]` (no namespace attribute)
204210
* [ ]`[attr=value i]` (attribute case-insensitive)
211+
* [ ]`:has()` (functional pseudo class)
212+
* [ ]`:nth-child(n of S)` (scoped to parent S)
213+
* [ ]`:nth-last-child(n of S)` (scoped to parent S)
205214
* [ ]`:active` (pseudo-class)
206215
* [ ]`:current` (pseudo-class)
216+
* [ ]`:current()` (functional pseudo-class)
207217
* [ ]`:default` (pseudo-class)
208218
* [ ]`:defined` (pseudo-class)
209-
* [ ]`:fullscreen` (pseudo-class)
219+
* [ ]`:drop` (pseudo-class)
220+
* [ ]`:drop()` (functional pseudo-class)
210221
* [ ]`:focus` (pseudo-class)
222+
* [ ]`:focus-visible` (pseudo-class)
223+
* [ ]`:focus-within` (pseudo-class)
224+
* [ ]`:fullscreen` (pseudo-class)
211225
* [ ]`:future` (pseudo-class)
212-
* [ ] § `:has()` (pseudo-class)
226+
* [ ]`:host()` (functional pseudo-class)
227+
* [ ]`:host-context()` (functional pseudo-class)
213228
* [ ]`:hover` (pseudo-class)
214-
* [ ]`:indeterminate` (pseudo-class)
215229
* [ ] § `:in-range` (pseudo-class)
230+
* [ ]`:indeterminate` (pseudo-class)
216231
* [ ] § `:invalid` (pseudo-class)
217232
* [ ]`:link` (pseudo-class)
218233
* [ ]`:local-link` (pseudo-class)
219-
* [ ]`nth-column()` (pseudo-class)
220-
* [ ]`nth-last-column()` (pseudo-class)
234+
* [ ]`:nth-column()` (functional pseudo-class)
235+
* [ ]`:nth-last-column()` (functional pseudo-class)
221236
* [ ] § `:out-of-range` (pseudo-class)
222237
* [ ]`:past` (pseudo-class)
223238
* [ ]`:paused` (pseudo-class)
224239
* [ ]`:placeholder-shown` (pseudo-class)
225240
* [ ]`:playing` (pseudo-class)
241+
* [ ]`:something()` (functional pseudo-class)
242+
* [ ]`:target` (pseudo-class)
243+
* [ ]`:target-within` (pseudo-class)
226244
* [ ]`:user-error` (pseudo-class)
227245
* [ ]`:user-invalid` (pseudo-class)
228246
* [ ] § `:valid` (pseudo-class)
@@ -235,6 +253,7 @@ Yields:
235253
* † — Needs a user, browser, interactivity, or scripting to make sense
236254
* ‡ — Not supported by the underlying algorithm
237255
* § — Not very interested in writing / including the code for this
256+
* ‖ — Too new, the spec is still changing
238257

239258
## Contribute
240259

0 commit comments

Comments
 (0)