|
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