css global selector + css parser fixes

pull/9723/head
Simon Holthausen 3 years ago
parent 43a5e0cc52
commit 5548332268

@ -435,10 +435,6 @@ const errors = {
// code: 'illegal-variable-declaration', // code: 'illegal-variable-declaration',
// message: 'Cannot declare same variable name which is imported inside <script context="module">' // message: 'Cannot declare same variable name which is imported inside <script context="module">'
// }, // },
// css_invalid_global: {
// code: 'css-invalid-global',
// message: ':global(...) can be at the start or end of a selector sequence, but not in the middle'
// },
// css_invalid_global_selector: { // css_invalid_global_selector: {
// code: 'css-invalid-global-selector', // code: 'css-invalid-global-selector',
// message: ':global(...) must contain a single selector' // message: ':global(...) must contain a single selector'

@ -156,7 +156,13 @@ export class Parser {
/** @param {string} str */ /** @param {string} str */
match(str) { match(str) {
return this.template.slice(this.index, this.index + str.length) === str; const length = str.length;
if (length === 1) {
// more performant than slicing
return this.template[this.index] === str;
}
return this.template.slice(this.index, this.index + length) === str;
} }
/** /**

@ -1,7 +1,7 @@
import { error } from '../../../errors.js'; import { error } from '../../../errors.js';
const REGEX_MATCHER = /^[~^$*|]?=/; const REGEX_MATCHER = /^[~^$*|]?=/;
const REGEX_CLOSING_PAREN = /\)/; const REGEX_CLOSING_PAREN = /(?<!\\)\)/; // \) is a way of escaping a closing paren, so we need to exclude it
const REGEX_CLOSING_BRACKET = /[\s\]]/; const REGEX_CLOSING_BRACKET = /[\s\]]/;
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof
const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/; const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/;
@ -145,22 +145,23 @@ function read_rule(parser) {
/** /**
* @param {import('../index.js').Parser} parser * @param {import('../index.js').Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {import('#compiler').Css.SelectorList} * @returns {import('#compiler').Css.SelectorList}
*/ */
function read_selector_list(parser) { function read_selector_list(parser, inside_pseudo_class = false) {
/** @type {import('#compiler').Css.Selector[]} */ /** @type {import('#compiler').Css.Selector[]} */
const children = []; const children = [];
const start = parser.index; const start = parser.index;
while (parser.index < parser.template.length) { while (parser.index < parser.template.length) {
children.push(read_selector(parser)); children.push(read_selector(parser, inside_pseudo_class));
const end = parser.index; const end = parser.index;
parser.allow_whitespace(); parser.allow_whitespace();
if (parser.match('{')) { if (parser.match('{') || (inside_pseudo_class && parser.match(')'))) {
return { return {
type: 'SelectorList', type: 'SelectorList',
start, start,
@ -178,9 +179,10 @@ function read_selector_list(parser) {
/** /**
* @param {import('../index.js').Parser} parser * @param {import('../index.js').Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {import('#compiler').Css.Selector} * @returns {import('#compiler').Css.Selector}
*/ */
function read_selector(parser) { function read_selector(parser, inside_pseudo_class = false) {
const list_start = parser.index; const list_start = parser.index;
/** @type {Array<import('#compiler').Css.SimpleSelector | import('#compiler').Css.Combinator>} */ /** @type {Array<import('#compiler').Css.SimpleSelector | import('#compiler').Css.Combinator>} */
@ -190,9 +192,16 @@ function read_selector(parser) {
const start = parser.index; const start = parser.index;
if (parser.eat('*')) { if (parser.eat('*')) {
let name = '*';
if (parser.match('|')) {
// * is the namespace (which we ignore)
parser.index++;
name = read_identifier(parser);
}
children.push({ children.push({
type: 'TypeSelector', type: 'TypeSelector',
name: '*', name,
start, start,
end: parser.index end: parser.index
}); });
@ -220,11 +229,11 @@ function read_selector(parser) {
} else if (parser.eat(':')) { } else if (parser.eat(':')) {
const name = read_identifier(parser); const name = read_identifier(parser);
/** @type {string | null} */ /** @type {null | import('#compiler').Css.SelectorList} */
let args = null; let args = null;
if (parser.eat('(')) { if (parser.eat('(')) {
args = parser.read_until(REGEX_CLOSING_PAREN); args = read_selector_list(parser, true);
parser.eat(')', true); parser.eat(')', true);
} }
@ -284,9 +293,15 @@ function read_selector(parser) {
end: parser.index end: parser.index
}); });
} else { } else {
let name = read_identifier(parser);
if (parser.match('|')) {
// we ignore the namespace when trying to find matching element classes
parser.index++;
name = read_identifier(parser);
}
children.push({ children.push({
type: 'TypeSelector', type: 'TypeSelector',
name: read_identifier(parser), name,
start, start,
end: parser.index end: parser.index
}); });
@ -295,7 +310,7 @@ function read_selector(parser) {
const index = parser.index; const index = parser.index;
parser.allow_whitespace(); parser.allow_whitespace();
if (parser.match('{') || parser.match(',')) { if (parser.match('{') || parser.match(',') || (inside_pseudo_class && parser.match(')'))) {
parser.index = index; parser.index = index;
return { return {

@ -17,7 +17,6 @@ const whitelist_attribute_selector = new Map([
['details', new Set(['open'])], ['details', new Set(['open'])],
['dialog', new Set(['open'])] ['dialog', new Set(['open'])]
]); ]);
const regex_is_single_css_selector = /[^\\],(?!([^([]+[^\\]|[^([\\])[)\]])/;
export default class Selector { export default class Selector {
/** @type {import('#compiler').Css.Selector} */ /** @type {import('#compiler').Css.Selector} */
@ -157,15 +156,14 @@ export default class Selector {
if ( if (
selector.type === 'PseudoClassSelector' && selector.type === 'PseudoClassSelector' &&
selector.name === 'global' && selector.name === 'global' &&
selector.args !== null selector.args !== null &&
selector.args.children.length > 1
) { ) {
if (regex_is_single_css_selector.test(selector.args)) {
error(selector, 'invalid-css-global-selector'); error(selector, 'invalid-css-global-selector');
} }
} }
} }
} }
}
/** @param {import('../../types.js').ComponentAnalysis} analysis */ /** @param {import('../../types.js').ComponentAnalysis} analysis */
validate_invalid_combinator_without_selector(analysis) { validate_invalid_combinator_without_selector(analysis) {
@ -179,11 +177,14 @@ export default class Selector {
validate_global_compound_selector() { validate_global_compound_selector() {
for (const block of this.blocks) { for (const block of this.blocks) {
for (const selector of block.selectors) { for (let i = 0; i < block.selectors.length; i++) {
const selector = block.selectors[i];
if ( if (
selector.type === 'PseudoClassSelector' && selector.type === 'PseudoClassSelector' &&
selector.name === 'global' && selector.name === 'global' &&
block.selectors.length !== 1 block.selectors.length !== 1 &&
(i === block.selectors.length - 1 ||
block.selectors.slice(i + 1).some((s) => s.type !== 'PseudoElementSelector'))
) { ) {
error(selector, 'invalid-css-global-selector-list'); error(selector, 'invalid-css-global-selector-list');
} }

@ -59,7 +59,7 @@ export interface PseudoElementSelector extends BaseNode {
export interface PseudoClassSelector extends BaseNode { export interface PseudoClassSelector extends BaseNode {
type: 'PseudoClassSelector'; type: 'PseudoClassSelector';
name: string; name: string;
args: string | null; args: SelectorList | null;
} }
export interface Percentage extends BaseNode { export interface Percentage extends BaseNode {

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,6 +1,6 @@
[ [
{ {
"code": "css-invalid-global", "code": "invalid-css-global-placement",
"message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", "message": ":global(...) can be at the start or end of a selector sequence, but not in the middle",
"start": { "start": {
"line": 2, "line": 2,

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,6 +1,6 @@
[ [
{ {
"code": "css-invalid-global", "code": "invalid-css-global-placement",
"message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", "message": ":global(...) can be at the start or end of a selector sequence, but not in the middle",
"start": { "start": {
"line": 5, "line": 5,

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,7 +1,7 @@
[ [
{ {
"code": "css-invalid-global-selector-position", "code": "invalid-css-global-selector-list",
"message": ":global(...) not at the start of a selector sequence should not contain type or universal selectors", "message": ":global(...) cannot be used to modify a selector, or be modified by another selector",
"start": { "start": {
"line": 2, "line": 2,
"column": 5 "column": 5

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,7 +1,7 @@
[ [
{ {
"code": "css-invalid-global-selector-position", "code": "invalid-css-global-selector-list",
"message": ":global(...) not at the start of a selector sequence should not contain type or universal selectors", "message": ":global(...) cannot be used to modify a selector, or be modified by another selector",
"start": { "start": {
"line": 2, "line": 2,
"column": 5 "column": 5

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,6 +1,6 @@
[ [
{ {
"code": "css-invalid-global", "code": "invalid-css-global-placement",
"message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", "message": ":global(...) can be at the start or end of a selector sequence, but not in the middle",
"start": { "start": {
"line": 2, "line": 2,

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,7 +1,7 @@
[ [
{ {
"code": "css-invalid-global-selector", "code": "invalid-css-global-selector",
"message": ":global(...) must contain a single selector", "message": ":global(...) must contain exactly one selector",
"start": { "start": {
"line": 11, "line": 11,
"column": 5 "column": 5

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,7 +1,7 @@
[ [
{ {
"code": "css-invalid-global-selector", "code": "invalid-css-global-selector",
"message": ":global(...) must contain a single selector", "message": ":global(...) must contain exactly one selector",
"start": { "start": {
"line": 5, "line": 5,
"column": 5 "column": 5

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,7 +1,7 @@
[ [
{ {
"code": "css-invalid-global-selector", "code": "invalid-css-global-selector",
"message": ":global(...) must contain a single selector", "message": ":global(...) must contain exactly one selector",
"start": { "start": {
"line": 2, "line": 2,
"column": 5 "column": 5

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,7 +1,7 @@
[ [
{ {
"code": "css-invalid-global-selector", "code": "invalid-css-global-selector",
"message": ":global(...) must contain a single selector", "message": ":global(...) must contain exactly one selector",
"start": { "start": {
"line": 5, "line": 5,
"column": 1 "column": 1

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,7 +1,7 @@
[ [
{ {
"code": "css-invalid-global-selector", "code": "invalid-css-global-selector",
"message": ":global(...) must contain a single selector", "message": ":global(...) must contain exactly one selector",
"start": { "start": {
"line": 5, "line": 5,
"column": 1 "column": 1

@ -1,8 +1,8 @@
<style> <style>
:global(.h1\,h2\,h3).foo { :global(.h1\,h2\,h3)::foo {
color: red; color: red;
} }
:global(h1, h2, h3).foo { :global(h1, h2, h3)::foo {
color: red; color: red;
} }
</style> </style>

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,7 +1,7 @@
[ [
{ {
"code": "css-invalid-global-selector", "code": "invalid-css-global-selector",
"message": ":global(...) must contain a single selector", "message": ":global(...) must contain exactly one selector",
"start": { "start": {
"line": 11, "line": 11,
"column": 5 "column": 5

@ -74,6 +74,10 @@ Content inside component tags becomes a [snippet prop](/docs/snippets) called `c
Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed. Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed.
### Stricter CSS `:global` selector validation
Previously, a selector like `.foo :global(bar).baz` was valid. In Svelte 5, this is a validation error instead. The reason is that in this selector the resulting CSS would be equivalent to one without `:global` - in other words, `:global` is ignored in this case.
### CSS hash position no longer deterministic ### CSS hash position no longer deterministic
Previously Svelte would always insert the CSS hash last. This is no longer guaranteed in Svelte 5. This is only breaking if you [have very weird css selectors](https://stackoverflow.com/questions/15670631/does-the-order-of-classes-listed-on-an-item-affect-the-css). Previously Svelte would always insert the CSS hash last. This is no longer guaranteed in Svelte 5. This is only breaking if you [have very weird css selectors](https://stackoverflow.com/questions/15670631/does-the-order-of-classes-listed-on-an-item-affect-the-css).

Loading…
Cancel
Save