Skip to content

Commit 5605e8c

Browse files
authored
feat: implement support for :is(...) and :where(...) (#10490)
* failing test for :is(...) * simplify * trim * factor out truncate function * pass stylesheet around * recurse into :is and :where * fix types * fix types * almost there * gah so close * tada * changeset * simplify * feat: nested CSS support (#10491) * parse nested CSS * tests * track parent rules * some progress * switch it up * pruning * works * changeset * lint * error early on invalid nesting selector * tidy * note to self * fix some specificity stuff * failing test * note to self * fix: correctly scope CSS selectors with descendant combinators (#10502) * fix traversal, but break some other stuff * man this is fucken hard * fixes * getting closer * be conservative for now * tidy * invert * invert * simplify * switch * for now * progress * i think it works? * fix * tidy up * revert some stuff * remove some junk * handle weird cases * update * tweak * shrink * changeset --------- Co-authored-by: Rich Harris <[email protected]> --------- Co-authored-by: Rich Harris <[email protected]> * Update playgrounds/sandbox/run.js * changeset --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 40ac2ca commit 5605e8c

File tree

40 files changed

+731
-287
lines changed

40 files changed

+731
-287
lines changed

.changeset/beige-mirrors-listen.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: correctly scope CSS selectors with descendant combinators

.changeset/big-eggs-flash.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: implement support for `:is(...)` and `:where(...)`

.changeset/fluffy-dolls-share.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: implement nested CSS support

.changeset/thick-shirts-deliver.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
breaking: encapsulate/remove selectors inside `:is(...)` and `:where(...)`

packages/svelte/src/compiler/errors.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ const css = {
107107
'invalid-css-global-selector-list': () =>
108108
`:global(...) must not contain type or universal selectors when used in a compound selector`,
109109
'invalid-css-selector': () => `Invalid selector`,
110-
'invalid-css-identifier': () => 'Expected a valid CSS identifier'
110+
'invalid-css-identifier': () => 'Expected a valid CSS identifier',
111+
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`
111112
};
112113

113114
/** @satisfies {Errors} */

packages/svelte/src/compiler/phases/1-parse/read/style.js

+40-33
Original file line numberDiff line numberDiff line change
@@ -83,36 +83,10 @@ function read_at_rule(parser) {
8383
let block = null;
8484

8585
if (parser.match('{')) {
86-
// if the parser could easily distinguish between rules and declarations, this wouldn't be necessary.
87-
// but this approach is much simpler. in future, when we support CSS nesting, the parser _will_ need
88-
// to be able to distinguish between them, but since we'll also need other changes to support that
89-
// this remains a TODO
90-
const contains_declarations = [
91-
'color-profile',
92-
'counter-style',
93-
'font-face',
94-
'font-palette-values',
95-
'page',
96-
'property'
97-
].includes(name);
98-
99-
if (contains_declarations) {
100-
block = read_block(parser);
101-
} else {
102-
const start = parser.index;
103-
104-
parser.eat('{', true);
105-
const children = read_body(parser, '}');
106-
parser.eat('}', true);
107-
108-
block = {
109-
type: 'Block',
110-
start,
111-
end: parser.index,
112-
children
113-
};
114-
}
86+
// e.g. `@media (...) {...}`
87+
block = read_block(parser);
11588
} else {
89+
// e.g. `@import '...'`
11690
parser.eat(';', true);
11791
}
11892

@@ -138,7 +112,11 @@ function read_rule(parser) {
138112
prelude: read_selector_list(parser),
139113
block: read_block(parser),
140114
start,
141-
end: parser.index
115+
end: parser.index,
116+
metadata: {
117+
parent_rule: null,
118+
has_local_selectors: false
119+
}
142120
};
143121
}
144122

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

219-
if (parser.eat('*')) {
197+
if (parser.eat('&')) {
198+
relative_selector.selectors.push({
199+
type: 'NestingSelector',
200+
name: '&',
201+
start,
202+
end: parser.index
203+
});
204+
} else if (parser.eat('*')) {
220205
let name = '*';
221206

222207
if (parser.eat('|')) {
@@ -356,6 +341,7 @@ function read_selector(parser, inside_pseudo_class = false) {
356341
end: index,
357342
children,
358343
metadata: {
344+
rule: null,
359345
used: false
360346
}
361347
};
@@ -432,7 +418,7 @@ function read_block(parser) {
432418

433419
parser.eat('{', true);
434420

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

438424
while (parser.index < parser.template.length) {
@@ -441,7 +427,7 @@ function read_block(parser) {
441427
if (parser.match('}')) {
442428
break;
443429
} else {
444-
children.push(read_declaration(parser));
430+
children.push(read_block_item(parser));
445431
}
446432
}
447433

@@ -455,6 +441,27 @@ function read_block(parser) {
455441
};
456442
}
457443

444+
/**
445+
* Reads a declaration, rule or at-rule
446+
*
447+
* @param {import('../index.js').Parser} parser
448+
* @returns {import('#compiler').Css.Declaration | import('#compiler').Css.Rule | import('#compiler').Css.Atrule}
449+
*/
450+
function read_block_item(parser) {
451+
if (parser.match('@')) {
452+
return read_at_rule(parser);
453+
}
454+
455+
// read ahead to understand whether we're dealing with a declaration or a nested rule.
456+
// this involves some duplicated work, but avoids a try-catch that would disguise errors
457+
const start = parser.index;
458+
read_value(parser);
459+
const char = parser.template[parser.index];
460+
parser.index = start;
461+
462+
return char === '{' ? read_rule(parser) : read_declaration(parser);
463+
}
464+
458465
/**
459466
* @param {import('../index.js').Parser} parser
460467
* @returns {import('#compiler').Css.Declaration}

packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js

+38-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import { walk } from 'zimmerframe';
12
import { error } from '../../../errors.js';
23
import { is_keyframes_node } from '../../css.js';
34
import { merge } from '../../visitors.js';
45

56
/**
67
* @typedef {import('zimmerframe').Visitors<
78
* import('#compiler').Css.Node,
8-
* NonNullable<import('../../types.js').ComponentAnalysis['css']>
9+
* {
10+
* keyframes: string[];
11+
* rule: import('#compiler').Css.Rule | null;
12+
* }
913
* >} Visitors
1014
*/
1115

@@ -24,7 +28,7 @@ function is_global(relative_selector) {
2428
}
2529

2630
/** @type {Visitors} */
27-
const analysis = {
31+
const analysis_visitors = {
2832
Atrule(node, context) {
2933
if (is_keyframes_node(node)) {
3034
if (!node.prelude.startsWith('-global-')) {
@@ -35,6 +39,8 @@ const analysis = {
3539
ComplexSelector(node, context) {
3640
context.next(); // analyse relevant selectors first
3741

42+
node.metadata.rule = context.state.rule;
43+
3844
node.metadata.used = node.children.every(
3945
({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root
4046
);
@@ -59,11 +65,25 @@ const analysis = {
5965
);
6066

6167
context.next();
68+
},
69+
Rule(node, context) {
70+
node.metadata.parent_rule = context.state.rule;
71+
72+
context.next({
73+
...context.state,
74+
rule: node
75+
});
76+
77+
node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
78+
return selector.children.some(
79+
({ metadata }) => !metadata.is_global && !metadata.is_host && !metadata.is_root
80+
);
81+
});
6282
}
6383
};
6484

6585
/** @type {Visitors} */
66-
const validation = {
86+
const validation_visitors = {
6787
ComplexSelector(node, context) {
6888
// ensure `:global(...)` is not used in the middle of a selector
6989
{
@@ -118,7 +138,21 @@ const validation = {
118138
}
119139
}
120140
}
141+
},
142+
NestingSelector(node, context) {
143+
const rule = /** @type {import('#compiler').Css.Rule} */ (context.state.rule);
144+
if (!rule.metadata.parent_rule) {
145+
error(node, 'invalid-nesting-selector');
146+
}
121147
}
122148
};
123149

124-
export const css_visitors = merge(analysis, validation);
150+
const css_visitors = merge(analysis_visitors, validation_visitors);
151+
152+
/**
153+
* @param {import('#compiler').Css.StyleSheet} stylesheet
154+
* @param {import('../../types.js').ComponentAnalysis} analysis
155+
*/
156+
export function analyze_css(stylesheet, analysis) {
157+
walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
158+
}

0 commit comments

Comments
 (0)