rework attribute selector matching to not use regexes (#1710)

pull/3579/head
Conduitry 5 years ago committed by GitHub
parent 3c6bb880d7
commit 3c5ccf6ee5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,9 @@
# Svelte changelog # Svelte changelog
## Unreleased
* Fix edge cases in matching selectors against elements ([#1710](https://github.com/sveltejs/svelte/issues/1710))
## 3.12.1 ## 3.12.1
* Escape `@` symbols in props, again ([#3545](https://github.com/sveltejs/svelte/issues/3545)) * Escape `@` symbols in props, again ([#3545](https://github.com/sveltejs/svelte/issues/3545))

@ -138,8 +138,9 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta
while (i--) { while (i--) {
const selector = block.selectors[i]; const selector = block.selectors[i];
const name = typeof selector.name === 'string' && selector.name.replace(/\\(.)/g, '$1');
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { if (selector.type === 'PseudoClassSelector' && name === 'global') {
// TODO shouldn't see this here... maybe we should enforce that :global(...) // TODO shouldn't see this here... maybe we should enforce that :global(...)
// cannot be sandwiched between non-global selectors? // cannot be sandwiched between non-global selectors?
return false; return false;
@ -150,11 +151,11 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta
} }
if (selector.type === 'ClassSelector') { if (selector.type === 'ClassSelector') {
if (!attribute_matches(node, 'class', selector.name, '~=', false) && !class_matches(node, selector.name)) return false; if (!attribute_matches(node, 'class', name, '~=', false) && !node.classes.some(c => c.name === name)) return false;
} }
else if (selector.type === 'IdSelector') { else if (selector.type === 'IdSelector') {
if (!attribute_matches(node, 'id', selector.name, '=', false)) return false; if (!attribute_matches(node, 'id', name, '=', false)) return false;
} }
else if (selector.type === 'AttributeSelector') { else if (selector.type === 'AttributeSelector') {
@ -162,8 +163,7 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta
} }
else if (selector.type === 'TypeSelector') { else if (selector.type === 'TypeSelector') {
// remove toLowerCase() in v2, when uppercase elements will be forbidden if (node.name.toLowerCase() !== name.toLowerCase() && name !== '*') return false;
if (node.name.toLowerCase() !== selector.name.toLowerCase() && selector.name !== '*') return false;
} }
else { else {
@ -206,14 +206,21 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta
return true; return true;
} }
const operators = { function test_attribute(operator, expected_value, case_insensitive, value) {
'=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags), if (case_insensitive) {
'~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags), expected_value = expected_value.toLowerCase();
'|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags), value = value.toLowerCase();
'^=': (value: string, flags: string) => new RegExp(`^${value}`, flags), }
'$=': (value: string, flags: string) => new RegExp(`${value}$`, flags), switch (operator) {
'*=': (value: string, flags: string) => new RegExp(value, flags) case '=': return value === expected_value;
}; case '~=': return ` ${value} `.includes(` ${expected_value} `);
case '|=': return `${value}-`.startsWith(`${expected_value}-`);
case '^=': return value.startsWith(expected_value);
case '$=': return value.endsWith(expected_value);
case '*=': return value.includes(expected_value);
default: throw new Error(`this shouldn't happen`);
}
}
function attribute_matches(node: Node, name: string, expected_value: string, operator: string, case_insensitive: boolean) { function attribute_matches(node: Node, name: string, expected_value: string, operator: string, case_insensitive: boolean) {
const spread = node.attributes.find(attr => attr.type === 'Spread'); const spread = node.attributes.find(attr => attr.type === 'Spread');
@ -227,29 +234,22 @@ function attribute_matches(node: Node, name: string, expected_value: string, ope
if (attr.chunks.length > 1) return true; if (attr.chunks.length > 1) return true;
if (!expected_value) return true; if (!expected_value) return true;
const pattern = operators[operator](expected_value, case_insensitive ? 'i' : '');
const value = attr.chunks[0]; const value = attr.chunks[0];
if (!value) return false; if (!value) return false;
if (value.type === 'Text') return pattern.test(value.data); if (value.type === 'Text') return test_attribute(operator, expected_value, case_insensitive, value.data);
const possible_values = new Set(); const possible_values = new Set();
gather_possible_values(value.node, possible_values); gather_possible_values(value.node, possible_values);
if (possible_values.has(UNKNOWN)) return true; if (possible_values.has(UNKNOWN)) return true;
for (const x of Array.from(possible_values)) { // TypeScript for-of is slightly unlike JS for (const value of possible_values) {
if (pattern.test(x)) return true; if (test_attribute(operator, expected_value, case_insensitive, value)) return true;
} }
return false; return false;
} }
function class_matches(node, name: string) {
return node.classes.some((class_directive) => {
return new RegExp(`\\b${name}\\b`).test(class_directive.name);
});
}
function unquote(value: Node) { function unquote(value: Node) {
if (value.type === 'Identifier') return value.name; if (value.type === 'Identifier') return value.name;
const str = value.value; const str = value.value;

@ -0,0 +1 @@
.-foo.svelte-xyz{color:red}[title='['].svelte-xyz{color:blue}

@ -0,0 +1,12 @@
<div class='-foo'>foo</div>
<div title='['>bar</div>
<style>
.-foo {
color: red;
}
[title='['] {
color: blue;
}
</style>
Loading…
Cancel
Save