|
4 | 4 | * @typedef {import('./types.js').Node} Node
|
5 | 5 | */
|
6 | 6 |
|
7 |
| -import {unreachable} from 'devlop' |
8 |
| -import {zwitch} from 'zwitch' |
| 7 | +import {ok as assert} from 'devlop' |
9 | 8 | import {indexable} from './util.js'
|
10 | 9 |
|
11 |
| -/** @type {(query: AstAttribute, node: Node) => boolean} */ |
12 |
| -const handle = zwitch('operator', { |
13 |
| - unknown: unknownOperator, |
14 |
| - // @ts-expect-error: hush. |
15 |
| - invalid: exists, |
16 |
| - handlers: { |
17 |
| - '=': exact, |
18 |
| - '^=': begins, |
19 |
| - '$=': ends, |
20 |
| - '*=': containsString, |
21 |
| - '~=': containsArray |
22 |
| - } |
23 |
| -}) |
24 |
| - |
25 | 10 | /**
|
26 | 11 | * @param {AstRule} query
|
| 12 | + * Query. |
27 | 13 | * @param {Node} node
|
| 14 | + * Node. |
28 | 15 | * @returns {boolean}
|
| 16 | + * Whether `node` matches `query`. |
29 | 17 | */
|
30 |
| -export function attribute(query, node) { |
| 18 | +export function attributes(query, node) { |
31 | 19 | let index = -1
|
32 | 20 |
|
33 | 21 | if (query.attributes) {
|
34 | 22 | while (++index < query.attributes.length) {
|
35 |
| - if (!handle(query.attributes[index], node)) return false |
| 23 | + if (!attribute(query.attributes[index], node)) { |
| 24 | + return false |
| 25 | + } |
36 | 26 | }
|
37 | 27 | }
|
38 | 28 |
|
39 | 29 | return true
|
40 | 30 | }
|
41 | 31 |
|
42 | 32 | /**
|
43 |
| - * Check whether an attribute exists. |
44 |
| - * |
45 |
| - * `[attr]` |
46 |
| - * |
47 | 33 | * @param {AstAttribute} query
|
| 34 | + * Query. |
48 | 35 | * @param {Node} node
|
| 36 | + * Node. |
49 | 37 | * @returns {boolean}
|
| 38 | + * Whether `node` matches `query`. |
50 | 39 | */
|
51 |
| -function exists(query, node) { |
52 |
| - indexable(node) |
53 |
| - return node[query.name] !== null && node[query.name] !== undefined |
54 |
| -} |
55 | 40 |
|
56 |
| -/** |
57 |
| - * Check whether an attribute has an exact value. |
58 |
| - * |
59 |
| - * `[attr=value]` |
60 |
| - * |
61 |
| - * @param {AstAttribute} query |
62 |
| - * @param {Node} node |
63 |
| - * @returns {boolean} |
64 |
| - */ |
65 |
| -function exact(query, node) { |
66 |
| - const queryValue = attributeValue(query) |
67 |
| - indexable(node) |
68 |
| - return exists(query, node) && String(node[query.name]) === queryValue |
69 |
| -} |
70 |
| - |
71 |
| -/** |
72 |
| - * Check whether an attribute, as a list, contains a value. |
73 |
| - * |
74 |
| - * When the attribute value is not a list, checks that the serialized value |
75 |
| - * is the queried one. |
76 |
| - * |
77 |
| - * `[attr~=value]` |
78 |
| - * |
79 |
| - * @param {AstAttribute} query |
80 |
| - * @param {Node} node |
81 |
| - * @returns {boolean} |
82 |
| - */ |
83 |
| -function containsArray(query, node) { |
| 41 | +function attribute(query, node) { |
84 | 42 | indexable(node)
|
85 | 43 | const value = node[query.name]
|
86 | 44 |
|
87 |
| - if (value === null || value === undefined) return false |
88 |
| - |
89 |
| - const queryValue = attributeValue(query) |
90 |
| - |
91 |
| - // If this is an array, and the query is contained in it, return `true`. |
92 |
| - // Coverage comment in place because TS turns `Array.isArray(unknown)` |
93 |
| - // into `Array<any>` instead of `Array<unknown>`. |
94 |
| - // type-coverage:ignore-next-line |
95 |
| - if (Array.isArray(value) && value.includes(queryValue)) { |
96 |
| - return true |
| 45 | + // Exists. |
| 46 | + if (!query.value) { |
| 47 | + return value !== null && value !== undefined |
97 | 48 | }
|
98 | 49 |
|
99 |
| - // For all other values, return whether this is an exact match. |
100 |
| - return String(value) === queryValue |
101 |
| -} |
| 50 | + assert(query.value.type === 'String', 'expected plain string') |
| 51 | + let key = query.value.value |
| 52 | + let normal = value === null || value === undefined ? undefined : String(value) |
102 | 53 |
|
103 |
| -/** |
104 |
| - * Check whether an attribute has a substring as its start. |
105 |
| - * |
106 |
| - * `[attr^=value]` |
107 |
| - * |
108 |
| - * @param {AstAttribute} query |
109 |
| - * @param {Node} node |
110 |
| - * @returns {boolean} |
111 |
| - */ |
112 |
| -function begins(query, node) { |
113 |
| - indexable(node) |
114 |
| - const value = node[query.name] |
115 |
| - const queryValue = attributeValue(query) |
| 54 | + // Case-sensitivity. |
| 55 | + if (query.caseSensitivityModifier === 'i') { |
| 56 | + key = key.toLowerCase() |
116 | 57 |
|
117 |
| - return Boolean( |
118 |
| - query.value && |
119 |
| - typeof value === 'string' && |
120 |
| - value.slice(0, queryValue.length) === queryValue |
121 |
| - ) |
122 |
| -} |
123 |
| - |
124 |
| -/** |
125 |
| - * Check whether an attribute has a substring as its end. |
126 |
| - * |
127 |
| - * `[attr$=value]` |
128 |
| - * |
129 |
| - * @param {AstAttribute} query |
130 |
| - * @param {Node} node |
131 |
| - * @returns {boolean} |
132 |
| - */ |
133 |
| -function ends(query, node) { |
134 |
| - indexable(node) |
135 |
| - const value = node[query.name] |
136 |
| - const queryValue = attributeValue(query) |
137 |
| - |
138 |
| - return Boolean( |
139 |
| - query.value && |
140 |
| - typeof value === 'string' && |
141 |
| - value.slice(-queryValue.length) === queryValue |
142 |
| - ) |
143 |
| -} |
144 |
| - |
145 |
| -/** |
146 |
| - * Check whether an attribute contains a substring. |
147 |
| - * |
148 |
| - * `[attr*=value]` |
149 |
| - * |
150 |
| - * @param {AstAttribute} query |
151 |
| - * @param {Node} node |
152 |
| - * @returns {boolean} |
153 |
| - */ |
154 |
| -function containsString(query, node) { |
155 |
| - indexable(node) |
156 |
| - const value = node[query.name] |
157 |
| - const queryValue = attributeValue(query) |
158 |
| - |
159 |
| - return Boolean( |
160 |
| - typeof value === 'string' && queryValue && value.includes(queryValue) |
161 |
| - ) |
162 |
| -} |
163 |
| - |
164 |
| -// Shouldn’t be called, parser throws an error instead. |
165 |
| -/** |
166 |
| - * @param {unknown} query |
167 |
| - * @returns {never} |
168 |
| - */ |
169 |
| -/* c8 ignore next 4 */ |
170 |
| -function unknownOperator(query) { |
171 |
| - // @ts-expect-error: `operator` guaranteed. |
172 |
| - throw new Error('Unknown operator `' + query.operator + '`') |
173 |
| -} |
174 |
| - |
175 |
| -/** |
176 |
| - * @param {AstAttribute} query |
177 |
| - * @returns {string} |
178 |
| - */ |
179 |
| -function attributeValue(query) { |
180 |
| - const queryValue = query.value |
| 58 | + if (normal) { |
| 59 | + normal = normal.toLowerCase() |
| 60 | + } |
| 61 | + } |
181 | 62 |
|
182 |
| - /* c8 ignore next 4 -- never happens with our config */ |
183 |
| - if (!queryValue) unreachable('Attribute values should be defined') |
184 |
| - if (queryValue.type === 'Substitution') { |
185 |
| - unreachable('Substitutions are not enabled') |
| 63 | + if (value !== undefined) { |
| 64 | + switch (query.operator) { |
| 65 | + // Exact. |
| 66 | + case '=': { |
| 67 | + return typeof normal === 'string' && key === normal |
| 68 | + } |
| 69 | + |
| 70 | + // Ends. |
| 71 | + case '$=': { |
| 72 | + return typeof value === 'string' && value.slice(-key.length) === key |
| 73 | + } |
| 74 | + |
| 75 | + // Contains. |
| 76 | + case '*=': { |
| 77 | + return typeof value === 'string' && value.includes(key) |
| 78 | + } |
| 79 | + |
| 80 | + // Begins. |
| 81 | + case '^=': { |
| 82 | + return typeof value === 'string' && key === value.slice(0, key.length) |
| 83 | + } |
| 84 | + |
| 85 | + // Space-separated list. |
| 86 | + case '~=': { |
| 87 | + // type-coverage:ignore-next-line -- some bug with TS. |
| 88 | + return (Array.isArray(value) && value.includes(key)) || normal === key |
| 89 | + } |
| 90 | + // Other values are not yet supported by CSS. |
| 91 | + // No default |
| 92 | + } |
186 | 93 | }
|
187 | 94 |
|
188 |
| - return queryValue.value |
| 95 | + return false |
189 | 96 | }
|
0 commit comments