Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/beige-mirrors-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: correctly scope CSS selectors with descendant combinators
5 changes: 5 additions & 0 deletions .changeset/big-eggs-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

feat: implement support for `:is(...)` and `:where(...)`
5 changes: 5 additions & 0 deletions .changeset/fluffy-dolls-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: implement nested CSS support
5 changes: 5 additions & 0 deletions .changeset/thick-shirts-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

breaking: encapsulate/remove selectors inside `:is(...)` and `:where(...)`
3 changes: 2 additions & 1 deletion packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ const css = {
'invalid-css-global-selector-list': () =>
`:global(...) must not contain type or universal selectors when used in a compound selector`,
'invalid-css-selector': () => `Invalid selector`,
'invalid-css-identifier': () => 'Expected a valid CSS identifier'
'invalid-css-identifier': () => 'Expected a valid CSS identifier',
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`
};

/** @satisfies {Errors} */
Expand Down
73 changes: 40 additions & 33 deletions packages/svelte/src/compiler/phases/1-parse/read/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,36 +83,10 @@ function read_at_rule(parser) {
let block = null;

if (parser.match('{')) {
// if the parser could easily distinguish between rules and declarations, this wouldn't be necessary.
// but this approach is much simpler. in future, when we support CSS nesting, the parser _will_ need
// to be able to distinguish between them, but since we'll also need other changes to support that
// this remains a TODO
const contains_declarations = [
'color-profile',
'counter-style',
'font-face',
'font-palette-values',
'page',
'property'
].includes(name);

if (contains_declarations) {
block = read_block(parser);
} else {
const start = parser.index;

parser.eat('{', true);
const children = read_body(parser, '}');
parser.eat('}', true);

block = {
type: 'Block',
start,
end: parser.index,
children
};
}
// e.g. `@media (...) {...}`
block = read_block(parser);
} else {
// e.g. `@import '...'`
parser.eat(';', true);
}

Expand All @@ -138,7 +112,11 @@ function read_rule(parser) {
prelude: read_selector_list(parser),
block: read_block(parser),
start,
end: parser.index
end: parser.index,
metadata: {
parent_rule: null,
has_local_selectors: false
}
};
}

Expand Down Expand Up @@ -216,7 +194,14 @@ function read_selector(parser, inside_pseudo_class = false) {
while (parser.index < parser.template.length) {
let start = parser.index;

if (parser.eat('*')) {
if (parser.eat('&')) {
relative_selector.selectors.push({
type: 'NestingSelector',
name: '&',
start,
end: parser.index
});
} else if (parser.eat('*')) {
let name = '*';

if (parser.eat('|')) {
Expand Down Expand Up @@ -356,6 +341,7 @@ function read_selector(parser, inside_pseudo_class = false) {
end: index,
children,
metadata: {
rule: null,
used: false
}
};
Expand Down Expand Up @@ -432,7 +418,7 @@ function read_block(parser) {

parser.eat('{', true);

/** @type {Array<import('#compiler').Css.Declaration | import('#compiler').Css.Rule>} */
/** @type {Array<import('#compiler').Css.Declaration | import('#compiler').Css.Rule | import('#compiler').Css.Atrule>} */
const children = [];

while (parser.index < parser.template.length) {
Expand All @@ -441,7 +427,7 @@ function read_block(parser) {
if (parser.match('}')) {
break;
} else {
children.push(read_declaration(parser));
children.push(read_block_item(parser));
}
}

Expand All @@ -455,6 +441,27 @@ function read_block(parser) {
};
}

/**
* Reads a declaration, rule or at-rule
*
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Declaration | import('#compiler').Css.Rule | import('#compiler').Css.Atrule}
*/
function read_block_item(parser) {
if (parser.match('@')) {
return read_at_rule(parser);
}

// read ahead to understand whether we're dealing with a declaration or a nested rule.
// this involves some duplicated work, but avoids a try-catch that would disguise errors
const start = parser.index;
read_value(parser);
const char = parser.template[parser.index];
parser.index = start;

return char === '{' ? read_rule(parser) : read_declaration(parser);
}

/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Declaration}
Expand Down
42 changes: 38 additions & 4 deletions packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { walk } from 'zimmerframe';
import { error } from '../../../errors.js';
import { is_keyframes_node } from '../../css.js';
import { merge } from '../../visitors.js';

/**
* @typedef {import('zimmerframe').Visitors<
* import('#compiler').Css.Node,
* NonNullable<import('../../types.js').ComponentAnalysis['css']>
* {
* keyframes: string[];
* rule: import('#compiler').Css.Rule | null;
* }
* >} Visitors
*/

Expand All @@ -24,7 +28,7 @@ function is_global(relative_selector) {
}

/** @type {Visitors} */
const analysis = {
const analysis_visitors = {
Atrule(node, context) {
if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-')) {
Expand All @@ -35,6 +39,8 @@ const analysis = {
ComplexSelector(node, context) {
context.next(); // analyse relevant selectors first

node.metadata.rule = context.state.rule;

node.metadata.used = node.children.every(
({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root
);
Expand All @@ -59,11 +65,25 @@ const analysis = {
);

context.next();
},
Rule(node, context) {
node.metadata.parent_rule = context.state.rule;

context.next({
...context.state,
rule: node
});

node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
return selector.children.some(
({ metadata }) => !metadata.is_global && !metadata.is_host && !metadata.is_root
);
});
}
};

/** @type {Visitors} */
const validation = {
const validation_visitors = {
ComplexSelector(node, context) {
// ensure `:global(...)` is not used in the middle of a selector
{
Expand Down Expand Up @@ -118,7 +138,21 @@ const validation = {
}
}
}
},
NestingSelector(node, context) {
const rule = /** @type {import('#compiler').Css.Rule} */ (context.state.rule);
if (!rule.metadata.parent_rule) {
error(node, 'invalid-nesting-selector');
}
}
};

export const css_visitors = merge(analysis, validation);
const css_visitors = merge(analysis_visitors, validation_visitors);

/**
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('../../types.js').ComponentAnalysis} analysis
*/
export function analyze_css(stylesheet, analysis) {
walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
}
Loading