1
1
/**
2
- * @typedef {import('css-selector-parser').AstRule } AstRule
3
2
* @typedef {import('css-selector-parser').AstAttribute } AstAttribute
3
+ * @typedef {import('css-selector-parser').AstRule } AstRule
4
+ *
4
5
* @typedef {import('hast').Element } Element
5
6
* @typedef {import('hast').Properties } Properties
6
- * @typedef { import('property-information').Schema } Schema
7
+ *
7
8
* @typedef {import('property-information').Info } Info
9
+ * @typedef {import('property-information').Schema } Schema
8
10
*/
9
11
10
12
import { stringify as commas } from 'comma-separated-tokens'
11
- import { ok as assert } from 'devlop'
13
+ import { ok as assert , unreachable } from 'devlop'
12
14
import { hasProperty } from 'hast-util-has-property'
13
15
import { find } from 'property-information'
14
16
import { stringify as spaces } from 'space-separated-tokens'
@@ -17,23 +19,27 @@ import {zwitch} from 'zwitch'
17
19
/** @type {(query: AstAttribute, element: Element, info: Info) => boolean } */
18
20
const handle = zwitch ( 'operator' , {
19
21
unknown : unknownOperator ,
20
- // @ts -expect-error: hush .
22
+ // @ts -expect-error: `exists` is fine .
21
23
invalid : exists ,
22
24
handlers : {
23
25
'=' : exact ,
24
- '~=' : spaceSeparatedList ,
25
- '|=' : exactOrPrefix ,
26
- '^=' : begins ,
27
26
'$=' : ends ,
28
- '*=' : contains
27
+ '*=' : contains ,
28
+ '^=' : begins ,
29
+ '|=' : exactOrPrefix ,
30
+ '~=' : spaceSeparatedList
29
31
}
30
32
} )
31
33
32
34
/**
33
35
* @param {AstRule } query
36
+ * Query.
34
37
* @param {Element } element
38
+ * Element.
35
39
* @param {Schema } schema
40
+ * Schema of element.
36
41
* @returns {boolean }
42
+ * Whether `element` matches `query`.
37
43
*/
38
44
export function attribute ( query , element , schema ) {
39
45
let index = - 1
@@ -52,187 +58,165 @@ export function attribute(query, element, schema) {
52
58
}
53
59
54
60
/**
55
- * Check whether an attribute exists.
56
- *
57
- * `[attr]`
58
- *
59
- * @param {AstAttribute } _
60
- * @param {Element } element
61
- * @param {Info } info
62
- * @returns {boolean }
63
- */
64
- function exists ( _ , element , info ) {
65
- return hasProperty ( element , info . property )
66
- }
67
-
68
- /**
69
- * Check whether an attribute has an exact value.
61
+ * Check whether an attribute has a substring as its start.
70
62
*
71
- * `[attr=value]`
63
+ * `[attr^ =value]`
72
64
*
73
65
* @param {AstAttribute } query
66
+ * Query.
74
67
* @param {Element } element
68
+ * Element.
75
69
* @param {Info } info
70
+ * Property info.
76
71
* @returns {boolean }
72
+ * Whether `element` matches `query`.
77
73
*/
78
- function exact ( query , element , info ) {
74
+ function begins ( query , element , info ) {
79
75
assert ( query . value , 'expected `value`' )
80
76
assert ( query . value . type === 'String' , 'expected plain string' )
81
77
82
78
return Boolean (
83
79
hasProperty ( element , info . property ) &&
84
- element . properties &&
85
- normalizeValue ( element . properties [ info . property ] , info ) ===
86
- query . value . value
80
+ normalizeValue ( element . properties [ info . property ] , info ) . slice (
81
+ 0 ,
82
+ query . value . value . length
83
+ ) === query . value . value
87
84
)
88
85
}
89
86
90
87
/**
91
- * Check whether an attribute, interpreted as a space-separated list, contains
92
- * a value.
88
+ * Check whether an attribute contains a substring.
93
89
*
94
- * `[attr~ =value]`
90
+ * `[attr* =value]`
95
91
*
96
92
* @param {AstAttribute } query
93
+ * Query.
97
94
* @param {Element } element
95
+ * Element.
98
96
* @param {Info } info
97
+ * Property info.
99
98
* @returns {boolean }
99
+ * Whether `element` matches `query`.
100
100
*/
101
- function spaceSeparatedList ( query , element , info ) {
101
+ function contains ( query , element , info ) {
102
102
assert ( query . value , 'expected `value`' )
103
103
assert ( query . value . type === 'String' , 'expected plain string' )
104
104
105
- const value = element . properties && element . properties [ info . property ]
106
-
107
- return (
108
- // If this is a space-separated list, and the query is contained in it, return
109
- // true.
110
- ( ! info . commaSeparated &&
111
- value &&
112
- typeof value === 'object' &&
113
- value . includes ( query . value . value ) ) ||
114
- // For all other values (including comma-separated lists), return whether this
115
- // is an exact match.
116
- ( hasProperty ( element , info . property ) &&
117
- normalizeValue ( value , info ) === query . value . value )
105
+ return Boolean (
106
+ hasProperty ( element , info . property ) &&
107
+ normalizeValue ( element . properties [ info . property ] , info ) . includes (
108
+ query . value . value
109
+ )
118
110
)
119
111
}
120
112
121
113
/**
122
- * Check whether an attribute has a substring as either the exact value or a
123
- * prefix.
114
+ * Check whether an attribute has a substring as its end.
124
115
*
125
- * `[attr| =value]`
116
+ * `[attr$ =value]`
126
117
*
127
118
* @param {AstAttribute } query
119
+ * Query.
128
120
* @param {Element } element
121
+ * Element.
129
122
* @param {Info } info
123
+ * Property info.
130
124
* @returns {boolean }
125
+ * Whether `element` matches `query`.
131
126
*/
132
- function exactOrPrefix ( query , element , info ) {
127
+ function ends ( query , element , info ) {
133
128
assert ( query . value , 'expected `value`' )
134
129
assert ( query . value . type === 'String' , 'expected plain string' )
135
130
136
- const value = normalizeValue (
137
- element . properties && element . properties [ info . property ] ,
138
- info
139
- )
140
-
141
131
return Boolean (
142
132
hasProperty ( element , info . property ) &&
143
- ( value === query . value . value ||
144
- ( value . slice ( 0 , query . value . value . length ) === query . value . value &&
145
- value . charAt ( query . value . value . length ) === '-' ) )
133
+ normalizeValue ( element . properties [ info . property ] , info ) . slice (
134
+ - query . value . value . length
135
+ ) === query . value . value
146
136
)
147
137
}
148
138
149
139
/**
150
- * Check whether an attribute has a substring as its start .
140
+ * Check whether an attribute has an exact value .
151
141
*
152
- * `[attr^ =value]`
142
+ * `[attr=value]`
153
143
*
154
144
* @param {AstAttribute } query
145
+ * Query.
155
146
* @param {Element } element
147
+ * Element.
156
148
* @param {Info } info
149
+ * Property info.
157
150
* @returns {boolean }
151
+ * Whether `element` matches `query`.
158
152
*/
159
- function begins ( query , element , info ) {
153
+ function exact ( query , element , info ) {
160
154
assert ( query . value , 'expected `value`' )
161
155
assert ( query . value . type === 'String' , 'expected plain string' )
162
156
163
157
return Boolean (
164
158
hasProperty ( element , info . property ) &&
165
- element . properties &&
166
- normalizeValue ( element . properties [ info . property ] , info ) . slice (
167
- 0 ,
168
- query . value . value . length
169
- ) === query . value . value
159
+ normalizeValue ( element . properties [ info . property ] , info ) ===
160
+ query . value . value
170
161
)
171
162
}
172
163
173
164
/**
174
- * Check whether an attribute has a substring as its end.
165
+ * Check whether an attribute has a substring as either the exact value or a
166
+ * prefix.
175
167
*
176
- * `[attr$ =value]`
168
+ * `[attr| =value]`
177
169
*
178
170
* @param {AstAttribute } query
171
+ * Query.
179
172
* @param {Element } element
173
+ * Element.
180
174
* @param {Info } info
175
+ * Property info.
181
176
* @returns {boolean }
177
+ * Whether `element` matches `query`.
182
178
*/
183
- function ends ( query , element , info ) {
179
+ function exactOrPrefix ( query , element , info ) {
184
180
assert ( query . value , 'expected `value`' )
185
181
assert ( query . value . type === 'String' , 'expected plain string' )
186
182
183
+ const value = normalizeValue ( element . properties [ info . property ] , info )
184
+
187
185
return Boolean (
188
186
hasProperty ( element , info . property ) &&
189
- element . properties &&
190
- normalizeValue ( element . properties [ info . property ] , info ) . slice (
191
- - query . value . value . length
192
- ) === query . value . value
187
+ ( value === query . value . value ||
188
+ ( value . slice ( 0 , query . value . value . length ) === query . value . value &&
189
+ value . charAt ( query . value . value . length ) === '-' ) )
193
190
)
194
191
}
195
192
196
193
/**
197
- * Check whether an attribute contains a substring .
194
+ * Check whether an attribute exists .
198
195
*
199
- * `[attr*=value ]`
196
+ * `[attr]`
200
197
*
201
- * @param {AstAttribute } query
198
+ * @param {AstAttribute } _
199
+ * Query.
202
200
* @param {Element } element
201
+ * Element.
203
202
* @param {Info } info
203
+ * Property info.
204
204
* @returns {boolean }
205
+ * Whether `element` matches `query`.
205
206
*/
206
- function contains ( query , element , info ) {
207
- assert ( query . value , 'expected `value`' )
208
- assert ( query . value . type === 'String' , 'expected plain string' )
209
-
210
- return Boolean (
211
- hasProperty ( element , info . property ) &&
212
- element . properties &&
213
- normalizeValue ( element . properties [ info . property ] , info ) . includes (
214
- query . value . value
215
- )
216
- )
217
- }
218
-
219
- // Shouldn’t be called, Parser throws an error instead.
220
- /**
221
- * @param {unknown } query
222
- * @returns {never }
223
- */
224
- /* c8 ignore next 4 */
225
- function unknownOperator ( query ) {
226
- // @ts -expect-error: `operator` guaranteed.
227
- throw new Error ( 'Unknown operator `' + query . operator + '`' )
207
+ function exists ( _ , element , info ) {
208
+ return hasProperty ( element , info . property )
228
209
}
229
210
230
211
/**
231
212
* Stringify a hast value back to its HTML form.
232
213
*
233
214
* @param {Properties[keyof Properties] } value
215
+ * hast property value.
234
216
* @param {Info } info
217
+ * Property info.
235
218
* @returns {string }
219
+ * Normalized value.
236
220
*/
237
221
function normalizeValue ( value , info ) {
238
222
if ( typeof value === 'boolean' ) {
@@ -245,3 +229,54 @@ function normalizeValue(value, info) {
245
229
246
230
return String ( value )
247
231
}
232
+
233
+ /**
234
+ * Check whether an attribute, interpreted as a space-separated list, contains
235
+ * a value.
236
+ *
237
+ * `[attr~=value]`
238
+ *
239
+ * @param {AstAttribute } query
240
+ * Query.
241
+ * @param {Element } element
242
+ * Element.
243
+ * @param {Info } info
244
+ * Property info.
245
+ * @returns {boolean }
246
+ * Whether `element` matches `query`.
247
+ */
248
+ function spaceSeparatedList ( query , element , info ) {
249
+ assert ( query . value , 'expected `value`' )
250
+ assert ( query . value . type === 'String' , 'expected plain string' )
251
+
252
+ const value = element . properties [ info . property ]
253
+
254
+ return (
255
+ // If this is a space-separated list, and the query is contained in it, return
256
+ // true.
257
+ ( ! info . commaSeparated &&
258
+ value &&
259
+ typeof value === 'object' &&
260
+ value . includes ( query . value . value ) ) ||
261
+ // For all other values (including comma-separated lists), return whether this
262
+ // is an exact match.
263
+ ( hasProperty ( element , info . property ) &&
264
+ normalizeValue ( value , info ) === query . value . value )
265
+ )
266
+ }
267
+
268
+ // Shouldn’t be called, Parser throws an error instead.
269
+ /**
270
+ * @param {unknown } query_
271
+ * Query.
272
+ * @returns {never }
273
+ * Nothing.
274
+ * @throws {Error }
275
+ * Error.
276
+ */
277
+ /* c8 ignore next 5 */
278
+ function unknownOperator ( query_ ) {
279
+ // Runtime guarantees `operator` exists.
280
+ const query = /** @type {AstAttribute } */ ( query_ )
281
+ unreachable ( 'Unknown operator `' + query . operator + '`' )
282
+ }
0 commit comments