diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ea328f9c..51e1e49cba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Svelte changelog +## Unreleased + +* Fix edge cases in matching selectors against elements ([#1710](https://github.com/sveltejs/svelte/issues/1710)) + ## 3.12.1 * Escape `@` symbols in props, again ([#3545](https://github.com/sveltejs/svelte/issues/3545)) diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts index 0d4ea82bb5..7601ee58b5 100644 --- a/src/compiler/compile/css/Selector.ts +++ b/src/compiler/compile/css/Selector.ts @@ -138,8 +138,9 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta while (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(...) // cannot be sandwiched between non-global selectors? return false; @@ -150,11 +151,11 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta } 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') { - if (!attribute_matches(node, 'id', selector.name, '=', false)) return false; + if (!attribute_matches(node, 'id', name, '=', false)) return false; } else if (selector.type === 'AttributeSelector') { @@ -162,8 +163,7 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta } else if (selector.type === 'TypeSelector') { - // remove toLowerCase() in v2, when uppercase elements will be forbidden - if (node.name.toLowerCase() !== selector.name.toLowerCase() && selector.name !== '*') return false; + if (node.name.toLowerCase() !== name.toLowerCase() && name !== '*') return false; } else { @@ -206,14 +206,21 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta return true; } -const operators = { - '=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags), - '~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags), - '|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags), - '^=': (value: string, flags: string) => new RegExp(`^${value}`, flags), - '$=': (value: string, flags: string) => new RegExp(`${value}$`, flags), - '*=': (value: string, flags: string) => new RegExp(value, flags) -}; +function test_attribute(operator, expected_value, case_insensitive, value) { + if (case_insensitive) { + expected_value = expected_value.toLowerCase(); + value = value.toLowerCase(); + } + switch (operator) { + 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) { 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 (!expected_value) return true; - const pattern = operators[operator](expected_value, case_insensitive ? 'i' : ''); const value = attr.chunks[0]; 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(); gather_possible_values(value.node, possible_values); if (possible_values.has(UNKNOWN)) return true; - for (const x of Array.from(possible_values)) { // TypeScript for-of is slightly unlike JS - if (pattern.test(x)) return true; + for (const value of possible_values) { + if (test_attribute(operator, expected_value, case_insensitive, value)) return true; } 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) { if (value.type === 'Identifier') return value.name; const str = value.value; diff --git a/test/css/samples/weird-selectors/expected.css b/test/css/samples/weird-selectors/expected.css new file mode 100644 index 0000000000..d4ead16751 --- /dev/null +++ b/test/css/samples/weird-selectors/expected.css @@ -0,0 +1 @@ +.-foo.svelte-xyz{color:red}[title='['].svelte-xyz{color:blue} \ No newline at end of file diff --git a/test/css/samples/weird-selectors/input.svelte b/test/css/samples/weird-selectors/input.svelte new file mode 100644 index 0000000000..65db029bbd --- /dev/null +++ b/test/css/samples/weird-selectors/input.svelte @@ -0,0 +1,12 @@ +