Skip to content

Commit daf8770

Browse files
committed
Update to match CSS selectors 4
This commit updates `css-selector-parser`, which is completely different. You’ll get some different errors if you do weird things. Otherwise, this changes: * change to throw on empty selector * change to on invalid attribute selectors such as `[a=b,c]`, use `[a="b,c"]` instead * remove `any`, `matches`: use `:is()` instead
1 parent 11e66c0 commit daf8770

File tree

16 files changed

+388
-376
lines changed

16 files changed

+388
-376
lines changed

lib/attribute.js

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
/**
2-
* @typedef {import('./types.js').Rule} Rule
3-
* @typedef {import('./types.js').RuleAttr} RuleAttr
4-
* @typedef {import('./types.js').Element} Element
5-
* @typedef {import('./types.js').Schema} Schema
6-
* @typedef {import('./types.js').Info} Info
7-
* @typedef {import('./types.js').PropertyValue} PropertyValue
2+
* @typedef {import('css-selector-parser').AstRule} AstRule
3+
* @typedef {import('css-selector-parser').AstAttribute} AstAttribute
4+
* @typedef {import('hast').Element} Element
5+
* @typedef {import('hast').Properties} Properties
6+
* @typedef {import('property-information').Schema} Schema
7+
* @typedef {import('property-information').Info} Info
88
*/
99

1010
import {stringify as commas} from 'comma-separated-tokens'
11+
import {ok as assert} from 'devlop'
1112
import {hasProperty} from 'hast-util-has-property'
1213
import {find} from 'property-information'
1314
import {stringify as spaces} from 'space-separated-tokens'
1415
import {zwitch} from 'zwitch'
1516

16-
/** @type {(query: RuleAttr, element: Element, info: Info) => boolean} */
17+
/** @type {(query: AstAttribute, element: Element, info: Info) => boolean} */
1718
const handle = zwitch('operator', {
1819
unknown: unknownOperator,
1920
// @ts-expect-error: hush.
@@ -29,18 +30,21 @@ const handle = zwitch('operator', {
2930
})
3031

3132
/**
32-
* @param {Rule} query
33+
* @param {AstRule} query
3334
* @param {Element} element
3435
* @param {Schema} schema
3536
* @returns {boolean}
3637
*/
3738
export function attribute(query, element, schema) {
38-
const attrs = query.attrs
3939
let index = -1
4040

41-
while (++index < attrs.length) {
42-
if (!handle(attrs[index], element, find(schema, attrs[index].name))) {
43-
return false
41+
if (query.attributes) {
42+
while (++index < query.attributes.length) {
43+
const attribute = query.attributes[index]
44+
45+
if (!handle(attribute, element, find(schema, attribute.name))) {
46+
return false
47+
}
4448
}
4549
}
4650

@@ -52,7 +56,7 @@ export function attribute(query, element, schema) {
5256
*
5357
* `[attr]`
5458
*
55-
* @param {RuleAttr} _
59+
* @param {AstAttribute} _
5660
* @param {Element} element
5761
* @param {Info} info
5862
* @returns {boolean}
@@ -66,16 +70,20 @@ function exists(_, element, info) {
6670
*
6771
* `[attr=value]`
6872
*
69-
* @param {RuleAttr} query
73+
* @param {AstAttribute} query
7074
* @param {Element} element
7175
* @param {Info} info
7276
* @returns {boolean}
7377
*/
7478
function exact(query, element, info) {
79+
assert(query.value, 'expected `value`')
80+
assert(query.value.type === 'String', 'expected plain string')
81+
7582
return Boolean(
7683
hasProperty(element, info.property) &&
7784
element.properties &&
78-
normalizeValue(element.properties[info.property], info) === query.value
85+
normalizeValue(element.properties[info.property], info) ===
86+
query.value.value
7987
)
8088
}
8189

@@ -85,12 +93,15 @@ function exact(query, element, info) {
8593
*
8694
* `[attr~=value]`
8795
*
88-
* @param {RuleAttr} query
96+
* @param {AstAttribute} query
8997
* @param {Element} element
9098
* @param {Info} info
9199
* @returns {boolean}
92100
*/
93101
function spaceSeparatedList(query, element, info) {
102+
assert(query.value, 'expected `value`')
103+
assert(query.value.type === 'String', 'expected plain string')
104+
94105
const value = element.properties && element.properties[info.property]
95106

96107
return (
@@ -99,12 +110,11 @@ function spaceSeparatedList(query, element, info) {
99110
(!info.commaSeparated &&
100111
value &&
101112
typeof value === 'object' &&
102-
query.value &&
103-
value.includes(query.value)) ||
113+
value.includes(query.value.value)) ||
104114
// For all other values (including comma-separated lists), return whether this
105115
// is an exact match.
106116
(hasProperty(element, info.property) &&
107-
normalizeValue(value, info) === query.value)
117+
normalizeValue(value, info) === query.value.value)
108118
)
109119
}
110120

@@ -114,23 +124,25 @@ function spaceSeparatedList(query, element, info) {
114124
*
115125
* `[attr|=value]`
116126
*
117-
* @param {RuleAttr} query
127+
* @param {AstAttribute} query
118128
* @param {Element} element
119129
* @param {Info} info
120130
* @returns {boolean}
121131
*/
122132
function exactOrPrefix(query, element, info) {
133+
assert(query.value, 'expected `value`')
134+
assert(query.value.type === 'String', 'expected plain string')
135+
123136
const value = normalizeValue(
124137
element.properties && element.properties[info.property],
125138
info
126139
)
127140

128141
return Boolean(
129142
hasProperty(element, info.property) &&
130-
query.value &&
131-
(value === query.value ||
132-
(value.slice(0, query.value.length) === query.value &&
133-
value.charAt(query.value.length) === '-'))
143+
(value === query.value.value ||
144+
(value.slice(0, query.value.value.length) === query.value.value &&
145+
value.charAt(query.value.value.length) === '-'))
134146
)
135147
}
136148

@@ -139,20 +151,22 @@ function exactOrPrefix(query, element, info) {
139151
*
140152
* `[attr^=value]`
141153
*
142-
* @param {RuleAttr} query
154+
* @param {AstAttribute} query
143155
* @param {Element} element
144156
* @param {Info} info
145157
* @returns {boolean}
146158
*/
147159
function begins(query, element, info) {
160+
assert(query.value, 'expected `value`')
161+
assert(query.value.type === 'String', 'expected plain string')
162+
148163
return Boolean(
149164
hasProperty(element, info.property) &&
150165
element.properties &&
151-
query.value &&
152166
normalizeValue(element.properties[info.property], info).slice(
153167
0,
154-
query.value.length
155-
) === query.value
168+
query.value.value.length
169+
) === query.value.value
156170
)
157171
}
158172

@@ -161,19 +175,21 @@ function begins(query, element, info) {
161175
*
162176
* `[attr$=value]`
163177
*
164-
* @param {RuleAttr} query
178+
* @param {AstAttribute} query
165179
* @param {Element} element
166180
* @param {Info} info
167181
* @returns {boolean}
168182
*/
169183
function ends(query, element, info) {
184+
assert(query.value, 'expected `value`')
185+
assert(query.value.type === 'String', 'expected plain string')
186+
170187
return Boolean(
171188
hasProperty(element, info.property) &&
172189
element.properties &&
173-
query.value &&
174190
normalizeValue(element.properties[info.property], info).slice(
175-
-query.value.length
176-
) === query.value
191+
-query.value.value.length
192+
) === query.value.value
177193
)
178194
}
179195

@@ -182,18 +198,20 @@ function ends(query, element, info) {
182198
*
183199
* `[attr*=value]`
184200
*
185-
* @param {RuleAttr} query
201+
* @param {AstAttribute} query
186202
* @param {Element} element
187203
* @param {Info} info
188204
* @returns {boolean}
189205
*/
190206
function contains(query, element, info) {
207+
assert(query.value, 'expected `value`')
208+
assert(query.value.type === 'String', 'expected plain string')
209+
191210
return Boolean(
192211
hasProperty(element, info.property) &&
193212
element.properties &&
194-
query.value &&
195213
normalizeValue(element.properties[info.property], info).includes(
196-
query.value
214+
query.value.value
197215
)
198216
)
199217
}
@@ -212,7 +230,7 @@ function unknownOperator(query) {
212230
/**
213231
* Stringify a hast value back to its HTML form.
214232
*
215-
* @param {PropertyValue} value
233+
* @param {Properties[keyof Properties]} value
216234
* @param {Info} info
217235
* @returns {string}
218236
*/

lib/class-name.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
/**
2-
* @typedef {import('./types.js').Rule} Rule
3-
* @typedef {import('./types.js').Element} Element
2+
* @typedef {import('css-selector-parser').AstRule} AstRule
3+
* @typedef {import('hast').Element} Element
44
*/
55

6+
// Make VS Code see references to the above types.
7+
''
8+
69
/**
710
* Check whether an element has all class names.
811
*
9-
* @param {Rule} query
12+
* @param {AstRule} query
1013
* @param {Element} element
1114
* @returns {boolean}
1215
*/
1316
export function className(query, element) {
14-
/** @type {readonly string[]} */
17+
/** @type {Readonly<Array<string>>} */
1518
// @ts-expect-error Assume array.
1619
const value = element.properties.className || []
1720
let index = -1

lib/enter-state.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/**
22
* @typedef {import('./types.js').SelectState} SelectState
3-
* @typedef {import('./types.js').Node} Node
4-
* @typedef {import('./types.js').ElementChild} ElementChild
3+
* @typedef {import('hast').Nodes} Nodes
4+
* @typedef {import('hast').ElementContent} ElementContent
55
* @typedef {import('./types.js').Direction} Direction
6-
* @typedef {import('unist-util-visit').Visitor<ElementChild>} Visitor
6+
* @typedef {import('unist-util-visit').Visitor<ElementContent>} Visitor
77
*/
88

99
import {direction} from 'direction'
@@ -20,7 +20,7 @@ import {visit, EXIT, SKIP} from 'unist-util-visit'
2020
* Current state.
2121
*
2222
* Will be mutated: `exit` undos the changes.
23-
* @param {Node} node
23+
* @param {Nodes} node
2424
* Node to enter.
2525
* @returns {() => void}
2626
* Call to exit.
@@ -139,7 +139,7 @@ function dirBidi(value) {
139139
}
140140

141141
/**
142-
* @param {ElementChild} node
142+
* @param {ElementContent} node
143143
* @returns {Direction | undefined}
144144
*/
145145
function dirProperty(node) {

lib/id.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
/**
2-
* @typedef {import('./types.js').Rule} Rule
2+
* @typedef {import('css-selector-parser').AstRule} AstRule
33
* @typedef {import('./types.js').Element} Element
44
*/
55

6+
import {ok as assert} from 'devlop'
7+
68
/**
79
* Check whether an element has an ID.
810
*
9-
* @param {Rule} query
11+
* @param {AstRule} query
1012
* @param {Element} element
1113
* @returns {boolean}
1214
*/
1315
export function id(query, element) {
14-
return Boolean(element.properties && element.properties.id === query.id)
16+
assert(query.ids, 'expected `ids`')
17+
const id = query.ids[query.ids.length - 1]
18+
return Boolean(element.properties && element.properties.id === id)
1519
}

lib/index.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/**
2-
* @typedef {import('./types.js').Element} Element
3-
* @typedef {import('./types.js').Node} Node
2+
* @typedef {import('hast').Element} Element
3+
* @typedef {import('hast').Nodes} Nodes
44
* @typedef {import('./types.js').Space} Space
55
* @typedef {import('./types.js').SelectState} SelectState
66
*/
77

88
import {html, svg} from 'property-information'
9-
import {queryToSelectors, walk} from './walk.js'
109
import {parse} from './parse.js'
10+
import {walk} from './walk.js'
1111

1212
/**
1313
* Check that the given `node` matches `selector`.
@@ -19,7 +19,7 @@ import {parse} from './parse.js'
1919
*
2020
* @param {string} selector
2121
* CSS selector, such as (`h1`, `a, b`).
22-
* @param {Node | null | undefined} [node]
22+
* @param {Nodes | null | undefined} [node]
2323
* Node that might match `selector`, should be an element.
2424
* @param {Space | null | undefined} [space='html']
2525
* Name of namespace (`'svg'` or `'html'`).
@@ -40,7 +40,7 @@ export function matches(selector, node, space) {
4040
*
4141
* @param {string} selector
4242
* CSS selector, such as (`h1`, `a, b`).
43-
* @param {Node | null | undefined} [tree]
43+
* @param {Nodes | null | undefined} [tree]
4444
* Tree to search.
4545
* @param {Space | null | undefined} [space='html']
4646
* Name of namespace (`'svg'` or `'html'`).
@@ -63,7 +63,7 @@ export function select(selector, tree, space) {
6363
*
6464
* @param {string} selector
6565
* CSS selector, such as (`h1`, `a, b`).
66-
* @param {Node | null | undefined} [tree]
66+
* @param {Nodes | null | undefined} [tree]
6767
* Tree to search.
6868
* @param {Space | null | undefined} [space='html']
6969
* Name of namespace (`'svg'` or `'html'`).
@@ -80,7 +80,7 @@ export function selectAll(selector, tree, space) {
8080
/**
8181
* @param {string} selector
8282
* Tree to search.
83-
* @param {Node | null | undefined} [tree]
83+
* @param {Nodes | null | undefined} [tree]
8484
* Tree to search.
8585
* @param {Space | null | undefined} [space='html']
8686
* Name of namespace (`'svg'` or `'html'`).
@@ -89,7 +89,7 @@ export function selectAll(selector, tree, space) {
8989
function createState(selector, tree, space) {
9090
return {
9191
// State of the query.
92-
rootQuery: queryToSelectors(parse(selector)),
92+
rootQuery: parse(selector),
9393
results: [],
9494
// @ts-expect-error assume elements.
9595
scopeElements: tree ? (tree.type === 'root' ? tree.children : [tree]) : [],

lib/name.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
/**
2-
* @typedef {import('./types.js').Rule} Rule
2+
* @typedef {import('css-selector-parser').AstRule} AstRule
33
* @typedef {import('./types.js').Element} Element
44
*/
55

6+
import {ok as assert} from 'devlop'
7+
68
/**
79
* Check whether an element has a tag name.
810
*
9-
* @param {Rule} query
11+
* @param {AstRule} query
1012
* @param {Element} element
1113
* @returns {boolean}
1214
*/
1315
export function name(query, element) {
14-
return query.tagName === '*' || query.tagName === element.tagName
16+
assert(query.tag, 'expected `tag`')
17+
return query.tag.type === 'WildcardTag' || query.tag.name === element.tagName
1518
}

0 commit comments

Comments
 (0)