11/**
2- * @typedef {import('css-selector-parser').AstRule } AstRule
32 * @typedef {import('css-selector-parser').AstAttribute } AstAttribute
3+ * @typedef {import('css-selector-parser').AstRule } AstRule
4+ *
45 * @typedef {import('hast').Element } Element
56 * @typedef {import('hast').Properties } Properties
6- * @typedef { import('property-information').Schema } Schema
7+ *
78 * @typedef {import('property-information').Info } Info
9+ * @typedef {import('property-information').Schema } Schema
810 */
911
1012import { stringify as commas } from 'comma-separated-tokens'
11- import { ok as assert } from 'devlop'
13+ import { ok as assert , unreachable } from 'devlop'
1214import { hasProperty } from 'hast-util-has-property'
1315import { find } from 'property-information'
1416import { stringify as spaces } from 'space-separated-tokens'
@@ -17,23 +19,27 @@ import {zwitch} from 'zwitch'
1719/** @type {(query: AstAttribute, element: Element, info: Info) => boolean } */
1820const handle = zwitch ( 'operator' , {
1921 unknown : unknownOperator ,
20- // @ts -expect-error: hush .
22+ // @ts -expect-error: `exists` is fine .
2123 invalid : exists ,
2224 handlers : {
2325 '=' : exact ,
24- '~=' : spaceSeparatedList ,
25- '|=' : exactOrPrefix ,
26- '^=' : begins ,
2726 '$=' : ends ,
28- '*=' : contains
27+ '*=' : contains ,
28+ '^=' : begins ,
29+ '|=' : exactOrPrefix ,
30+ '~=' : spaceSeparatedList
2931 }
3032} )
3133
3234/**
3335 * @param {AstRule } query
36+ * Query.
3437 * @param {Element } element
38+ * Element.
3539 * @param {Schema } schema
40+ * Schema of element.
3641 * @returns {boolean }
42+ * Whether `element` matches `query`.
3743 */
3844export function attribute ( query , element , schema ) {
3945 let index = - 1
@@ -52,187 +58,165 @@ export function attribute(query, element, schema) {
5258}
5359
5460/**
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.
7062 *
71- * `[attr=value]`
63+ * `[attr^ =value]`
7264 *
7365 * @param {AstAttribute } query
66+ * Query.
7467 * @param {Element } element
68+ * Element.
7569 * @param {Info } info
70+ * Property info.
7671 * @returns {boolean }
72+ * Whether `element` matches `query`.
7773 */
78- function exact ( query , element , info ) {
74+ function begins ( query , element , info ) {
7975 assert ( query . value , 'expected `value`' )
8076 assert ( query . value . type === 'String' , 'expected plain string' )
8177
8278 return Boolean (
8379 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
8784 )
8885}
8986
9087/**
91- * Check whether an attribute, interpreted as a space-separated list, contains
92- * a value.
88+ * Check whether an attribute contains a substring.
9389 *
94- * `[attr~ =value]`
90+ * `[attr* =value]`
9591 *
9692 * @param {AstAttribute } query
93+ * Query.
9794 * @param {Element } element
95+ * Element.
9896 * @param {Info } info
97+ * Property info.
9998 * @returns {boolean }
99+ * Whether `element` matches `query`.
100100 */
101- function spaceSeparatedList ( query , element , info ) {
101+ function contains ( query , element , info ) {
102102 assert ( query . value , 'expected `value`' )
103103 assert ( query . value . type === 'String' , 'expected plain string' )
104104
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+ )
118110 )
119111}
120112
121113/**
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.
124115 *
125- * `[attr| =value]`
116+ * `[attr$ =value]`
126117 *
127118 * @param {AstAttribute } query
119+ * Query.
128120 * @param {Element } element
121+ * Element.
129122 * @param {Info } info
123+ * Property info.
130124 * @returns {boolean }
125+ * Whether `element` matches `query`.
131126 */
132- function exactOrPrefix ( query , element , info ) {
127+ function ends ( query , element , info ) {
133128 assert ( query . value , 'expected `value`' )
134129 assert ( query . value . type === 'String' , 'expected plain string' )
135130
136- const value = normalizeValue (
137- element . properties && element . properties [ info . property ] ,
138- info
139- )
140-
141131 return Boolean (
142132 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
146136 )
147137}
148138
149139/**
150- * Check whether an attribute has a substring as its start .
140+ * Check whether an attribute has an exact value .
151141 *
152- * `[attr^ =value]`
142+ * `[attr=value]`
153143 *
154144 * @param {AstAttribute } query
145+ * Query.
155146 * @param {Element } element
147+ * Element.
156148 * @param {Info } info
149+ * Property info.
157150 * @returns {boolean }
151+ * Whether `element` matches `query`.
158152 */
159- function begins ( query , element , info ) {
153+ function exact ( query , element , info ) {
160154 assert ( query . value , 'expected `value`' )
161155 assert ( query . value . type === 'String' , 'expected plain string' )
162156
163157 return Boolean (
164158 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
170161 )
171162}
172163
173164/**
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.
175167 *
176- * `[attr$ =value]`
168+ * `[attr| =value]`
177169 *
178170 * @param {AstAttribute } query
171+ * Query.
179172 * @param {Element } element
173+ * Element.
180174 * @param {Info } info
175+ * Property info.
181176 * @returns {boolean }
177+ * Whether `element` matches `query`.
182178 */
183- function ends ( query , element , info ) {
179+ function exactOrPrefix ( query , element , info ) {
184180 assert ( query . value , 'expected `value`' )
185181 assert ( query . value . type === 'String' , 'expected plain string' )
186182
183+ const value = normalizeValue ( element . properties [ info . property ] , info )
184+
187185 return Boolean (
188186 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 ) === '-' ) )
193190 )
194191}
195192
196193/**
197- * Check whether an attribute contains a substring .
194+ * Check whether an attribute exists .
198195 *
199- * `[attr*=value ]`
196+ * `[attr]`
200197 *
201- * @param {AstAttribute } query
198+ * @param {AstAttribute } _
199+ * Query.
202200 * @param {Element } element
201+ * Element.
203202 * @param {Info } info
203+ * Property info.
204204 * @returns {boolean }
205+ * Whether `element` matches `query`.
205206 */
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 )
228209}
229210
230211/**
231212 * Stringify a hast value back to its HTML form.
232213 *
233214 * @param {Properties[keyof Properties] } value
215+ * hast property value.
234216 * @param {Info } info
217+ * Property info.
235218 * @returns {string }
219+ * Normalized value.
236220 */
237221function normalizeValue ( value , info ) {
238222 if ( typeof value === 'boolean' ) {
@@ -245,3 +229,54 @@ function normalizeValue(value, info) {
245229
246230 return String ( value )
247231}
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