1010 */
1111
1212import { stringify as commas } from 'comma-separated-tokens'
13- import { ok as assert , unreachable } from 'devlop'
14- import { hasProperty } from 'hast-util-has-property'
13+ import { ok as assert } from 'devlop'
1514import { find } from 'property-information'
16- import { stringify as spaces } from 'space-separated-tokens'
17- import { zwitch } from 'zwitch'
18-
19- /** @type {(query: AstAttribute, element: Element, info: Info) => boolean } */
20- const handle = zwitch ( 'operator' , {
21- unknown : unknownOperator ,
22- // @ts -expect-error: `exists` is fine.
23- invalid : exists ,
24- handlers : {
25- '=' : exact ,
26- '$=' : ends ,
27- '*=' : contains ,
28- '^=' : begins ,
29- '|=' : exactOrPrefix ,
30- '~=' : spaceSeparatedList
31- }
32- } )
15+ import * as spaces from 'space-separated-tokens'
3316
3417/**
3518 * @param {AstRule } query
@@ -41,14 +24,12 @@ const handle = zwitch('operator', {
4124 * @returns {boolean }
4225 * Whether `element` matches `query`.
4326 */
44- export function attribute ( query , element , schema ) {
27+ export function attributes ( query , element , schema ) {
4528 let index = - 1
4629
4730 if ( query . attributes ) {
4831 while ( ++ index < query . attributes . length ) {
49- const attribute = query . attributes [ index ]
50-
51- if ( ! handle ( attribute , element , find ( schema , attribute . name ) ) ) {
32+ if ( ! attribute ( query . attributes [ index ] , element , schema ) ) {
5233 return false
5334 }
5435 }
@@ -58,225 +39,105 @@ export function attribute(query, element, schema) {
5839}
5940
6041/**
61- * Check whether an attribute has a substring as its start.
62- *
63- * `[attr^=value]`
64- *
6542 * @param {AstAttribute } query
6643 * Query.
6744 * @param {Element } element
6845 * Element.
69- * @param {Info } info
70- * Property info .
46+ * @param {Schema } schema
47+ * Schema of element .
7148 * @returns {boolean }
7249 * Whether `element` matches `query`.
7350 */
74- function begins ( query , element , info ) {
75- assert ( query . value , 'expected `value`' )
76- assert ( query . value . type === 'String' , 'expected plain string' )
77-
78- return Boolean (
79- hasProperty ( element , info . property ) &&
80- normalizeValue ( element . properties [ info . property ] , info ) . slice (
81- 0 ,
82- query . value . value . length
83- ) === query . value . value
84- )
85- }
51+ function attribute ( query , element , schema ) {
52+ const info = find ( schema , query . name )
53+ const propertyValue = element . properties [ info . property ]
54+ let value = normalizeValue ( propertyValue , info )
55+
56+ // Exists.
57+ if ( ! query . value ) {
58+ return value !== undefined
59+ }
8660
87- /**
88- * Check whether an attribute contains a substring.
89- *
90- * `[attr*=value]`
91- *
92- * @param {AstAttribute } query
93- * Query.
94- * @param {Element } element
95- * Element.
96- * @param {Info } info
97- * Property info.
98- * @returns {boolean }
99- * Whether `element` matches `query`.
100- */
101- function contains ( query , element , info ) {
102- assert ( query . value , 'expected `value`' )
10361 assert ( query . value . type === 'String' , 'expected plain string' )
62+ let key = query . value . value
10463
105- return Boolean (
106- hasProperty ( element , info . property ) &&
107- normalizeValue ( element . properties [ info . property ] , info ) . includes (
108- query . value . value
109- )
110- )
111- }
64+ // Case-sensitivity.
65+ if ( query . caseSensitivityModifier === 'i' ) {
66+ key = key . toLowerCase ( )
11267
113- /**
114- * Check whether an attribute has a substring as its end.
115- *
116- * `[attr$=value]`
117- *
118- * @param {AstAttribute } query
119- * Query.
120- * @param {Element } element
121- * Element.
122- * @param {Info } info
123- * Property info.
124- * @returns {boolean }
125- * Whether `element` matches `query`.
126- */
127- function ends ( query , element , info ) {
128- assert ( query . value , 'expected `value`' )
129- assert ( query . value . type === 'String' , 'expected plain string' )
68+ if ( value ) {
69+ value = value . toLowerCase ( )
70+ }
71+ }
13072
131- return Boolean (
132- hasProperty ( element , info . property ) &&
133- normalizeValue ( element . properties [ info . property ] , info ) . slice (
134- - query . value . value . length
135- ) === query . value . value
136- )
137- }
73+ if ( value !== undefined ) {
74+ switch ( query . operator ) {
75+ // Exact.
76+ case '=' : {
77+ return key === value
78+ }
13879
139- /**
140- * Check whether an attribute has an exact value.
141- *
142- * `[attr=value]`
143- *
144- * @param {AstAttribute } query
145- * Query.
146- * @param {Element } element
147- * Element.
148- * @param {Info } info
149- * Property info.
150- * @returns {boolean }
151- * Whether `element` matches `query`.
152- */
153- function exact ( query , element , info ) {
154- assert ( query . value , 'expected `value`' )
155- assert ( query . value . type === 'String' , 'expected plain string' )
80+ // Ends.
81+ case '$=' : {
82+ return key === value . slice ( - key . length )
83+ }
15684
157- return Boolean (
158- hasProperty ( element , info . property ) &&
159- normalizeValue ( element . properties [ info . property ] , info ) ===
160- query . value . value
161- )
162- }
85+ // Contains.
86+ case '*=' : {
87+ return value . includes ( key )
88+ }
16389
164- /**
165- * Check whether an attribute has a substring as either the exact value or a
166- * prefix.
167- *
168- * `[attr|=value]`
169- *
170- * @param {AstAttribute } query
171- * Query.
172- * @param {Element } element
173- * Element.
174- * @param {Info } info
175- * Property info.
176- * @returns {boolean }
177- * Whether `element` matches `query`.
178- */
179- function exactOrPrefix ( query , element , info ) {
180- assert ( query . value , 'expected `value`' )
181- assert ( query . value . type === 'String' , 'expected plain string' )
90+ // Begins.
91+ case '^=' : {
92+ return key === value . slice ( 0 , key . length )
93+ }
18294
183- const value = normalizeValue ( element . properties [ info . property ] , info )
95+ // Exact or prefix.
96+ case '|=' : {
97+ return (
98+ key === value ||
99+ ( key === value . slice ( 0 , key . length ) &&
100+ value . charAt ( key . length ) === '-' )
101+ )
102+ }
184103
185- return Boolean (
186- hasProperty ( element , info . property ) &&
187- ( value === query . value . value ||
188- ( value . slice ( 0 , query . value . value . length ) === query . value . value &&
189- value . charAt ( query . value . value . length ) === '-' ) )
190- )
191- }
104+ // Space-separated list.
105+ case '~=' : {
106+ return (
107+ // For all other values (including comma-separated lists), return whether this
108+ // is an exact match.
109+ key === value ||
110+ // If this is a space-separated list, and the query is contained in it, return
111+ // true.
112+ spaces . parse ( value ) . includes ( key )
113+ )
114+ }
115+ // Other values are not yet supported by CSS.
116+ // No default
117+ }
118+ }
192119
193- /**
194- * Check whether an attribute exists.
195- *
196- * `[attr]`
197- *
198- * @param {AstAttribute } _
199- * Query.
200- * @param {Element } element
201- * Element.
202- * @param {Info } info
203- * Property info.
204- * @returns {boolean }
205- * Whether `element` matches `query`.
206- */
207- function exists ( _ , element , info ) {
208- return hasProperty ( element , info . property )
120+ return false
209121}
210122
211123/**
212- * Stringify a hast value back to its HTML form.
213124 *
214125 * @param {Properties[keyof Properties] } value
215- * hast property value.
216126 * @param {Info } info
217- * Property info.
218- * @returns {string }
219- * Normalized value.
127+ * @returns {string | undefined }
220128 */
221129function normalizeValue ( value , info ) {
222- if ( typeof value === 'boolean' ) {
223- return info . attribute
224- }
225-
226- if ( Array . isArray ( value ) ) {
227- return ( info . commaSeparated ? commas : spaces ) ( value )
130+ if ( value === null || value === undefined ) {
131+ // Empty.
132+ } else if ( typeof value === 'boolean' ) {
133+ if ( value ) {
134+ return info . attribute
135+ }
136+ } else if ( Array . isArray ( value ) ) {
137+ if ( value . length > 0 ) {
138+ return ( info . commaSeparated ? commas : spaces . stringify ) ( value )
139+ }
140+ } else {
141+ return String ( value )
228142 }
229-
230- return String ( value )
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 + '`' )
282143}
0 commit comments