Skip to content

Commit 3c5ccf6

Browse files
authored
rework attribute selector matching to not use regexes (#1710)
1 parent 3c6bb88 commit 3c5ccf6

File tree

4 files changed

+40
-23
lines changed

4 files changed

+40
-23
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Svelte changelog
22

3+
## Unreleased
4+
5+
* Fix edge cases in matching selectors against elements ([#1710](https://github.com/sveltejs/svelte/issues/1710))
6+
37
## 3.12.1
48

59
* Escape `@` symbols in props, again ([#3545](https://github.com/sveltejs/svelte/issues/3545))

src/compiler/compile/css/Selector.ts

+23-23
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,9 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta
138138

139139
while (i--) {
140140
const selector = block.selectors[i];
141+
const name = typeof selector.name === 'string' && selector.name.replace(/\\(.)/g, '$1');
141142

142-
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
143+
if (selector.type === 'PseudoClassSelector' && name === 'global') {
143144
// TODO shouldn't see this here... maybe we should enforce that :global(...)
144145
// cannot be sandwiched between non-global selectors?
145146
return false;
@@ -150,20 +151,19 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta
150151
}
151152

152153
if (selector.type === 'ClassSelector') {
153-
if (!attribute_matches(node, 'class', selector.name, '~=', false) && !class_matches(node, selector.name)) return false;
154+
if (!attribute_matches(node, 'class', name, '~=', false) && !node.classes.some(c => c.name === name)) return false;
154155
}
155156

156157
else if (selector.type === 'IdSelector') {
157-
if (!attribute_matches(node, 'id', selector.name, '=', false)) return false;
158+
if (!attribute_matches(node, 'id', name, '=', false)) return false;
158159
}
159160

160161
else if (selector.type === 'AttributeSelector') {
161162
if (!attribute_matches(node, selector.name.name, selector.value && unquote(selector.value), selector.matcher, selector.flags)) return false;
162163
}
163164

164165
else if (selector.type === 'TypeSelector') {
165-
// remove toLowerCase() in v2, when uppercase elements will be forbidden
166-
if (node.name.toLowerCase() !== selector.name.toLowerCase() && selector.name !== '*') return false;
166+
if (node.name.toLowerCase() !== name.toLowerCase() && name !== '*') return false;
167167
}
168168

169169
else {
@@ -206,14 +206,21 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta
206206
return true;
207207
}
208208

209-
const operators = {
210-
'=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags),
211-
'~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags),
212-
'|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags),
213-
'^=': (value: string, flags: string) => new RegExp(`^${value}`, flags),
214-
'$=': (value: string, flags: string) => new RegExp(`${value}$`, flags),
215-
'*=': (value: string, flags: string) => new RegExp(value, flags)
216-
};
209+
function test_attribute(operator, expected_value, case_insensitive, value) {
210+
if (case_insensitive) {
211+
expected_value = expected_value.toLowerCase();
212+
value = value.toLowerCase();
213+
}
214+
switch (operator) {
215+
case '=': return value === expected_value;
216+
case '~=': return ` ${value} `.includes(` ${expected_value} `);
217+
case '|=': return `${value}-`.startsWith(`${expected_value}-`);
218+
case '^=': return value.startsWith(expected_value);
219+
case '$=': return value.endsWith(expected_value);
220+
case '*=': return value.includes(expected_value);
221+
default: throw new Error(`this shouldn't happen`);
222+
}
223+
}
217224

218225
function attribute_matches(node: Node, name: string, expected_value: string, operator: string, case_insensitive: boolean) {
219226
const spread = node.attributes.find(attr => attr.type === 'Spread');
@@ -227,29 +234,22 @@ function attribute_matches(node: Node, name: string, expected_value: string, ope
227234
if (attr.chunks.length > 1) return true;
228235
if (!expected_value) return true;
229236

230-
const pattern = operators[operator](expected_value, case_insensitive ? 'i' : '');
231237
const value = attr.chunks[0];
232238

233239
if (!value) return false;
234-
if (value.type === 'Text') return pattern.test(value.data);
240+
if (value.type === 'Text') return test_attribute(operator, expected_value, case_insensitive, value.data);
235241

236242
const possible_values = new Set();
237243
gather_possible_values(value.node, possible_values);
238244
if (possible_values.has(UNKNOWN)) return true;
239245

240-
for (const x of Array.from(possible_values)) { // TypeScript for-of is slightly unlike JS
241-
if (pattern.test(x)) return true;
246+
for (const value of possible_values) {
247+
if (test_attribute(operator, expected_value, case_insensitive, value)) return true;
242248
}
243249

244250
return false;
245251
}
246252

247-
function class_matches(node, name: string) {
248-
return node.classes.some((class_directive) => {
249-
return new RegExp(`\\b${name}\\b`).test(class_directive.name);
250-
});
251-
}
252-
253253
function unquote(value: Node) {
254254
if (value.type === 'Identifier') return value.name;
255255
const str = value.value;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.-foo.svelte-xyz{color:red}[title='['].svelte-xyz{color:blue}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<div class='-foo'>foo</div>
2+
3+
<div title='['>bar</div>
4+
5+
<style>
6+
.-foo {
7+
color: red;
8+
}
9+
[title='['] {
10+
color: blue;
11+
}
12+
</style>

0 commit comments

Comments
 (0)