Skip to content

Commit 56ae734

Browse files
committed
Add support for case-sensitivity modifiers
1 parent 0c47af3 commit 56ae734

File tree

4 files changed

+139
-226
lines changed

4 files changed

+139
-226
lines changed

lib/attribute.js

Lines changed: 78 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,9 @@
1010
*/
1111

1212
import {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'
1514
import {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
*/
221129
function 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
}

lib/test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @typedef {import('./index.js').State} State
88
*/
99

10-
import {attribute} from './attribute.js'
10+
import {attributes} from './attribute.js'
1111
import {className} from './class-name.js'
1212
import {id} from './id.js'
1313
import {name} from './name.js'
@@ -38,7 +38,7 @@ export function test(query, element, index, parent, state) {
3838
(!query.tag || name(query, element)) &&
3939
(!query.classNames || className(query, element)) &&
4040
(!query.ids || id(query, element)) &&
41-
(!query.attributes || attribute(query, element, state.schema)) &&
41+
(!query.attributes || attributes(query, element, state.schema)) &&
4242
(!query.pseudoClasses || pseudo(query, element, index, parent, state))
4343
)
4444
}

0 commit comments

Comments
 (0)