Skip to content

Commit 967b881

Browse files
authored
scope CSS sibling combinators (#5427)
1 parent a1651ca commit 967b881

File tree

105 files changed

+2280
-24
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+2280
-24
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
* Add `|nonpassive` event modifier, explicitly passing `passive: false` ([#2068](https://github.com/sveltejs/svelte/issues/2068))
6+
* Scope CSS selectors with `~` and `+` combinators ([#3104](https://github.com/sveltejs/svelte/issues/3104))
67
* Fix keyed `{#each}` not reacting to key changing ([#5444](https://github.com/sveltejs/svelte/issues/5444))
78
* Fix destructuring into store values ([#5449](https://github.com/sveltejs/svelte/issues/5449))
89
* Fix erroneous `missing-declaration` warning with `use:obj.method` ([#5451](https://github.com/sveltejs/svelte/issues/5451))

src/compiler/compile/Component.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import add_to_set from './utils/add_to_set';
2929
import check_graph_for_cycles from './utils/check_graph_for_cycles';
3030
import { print, x, b } from 'code-red';
3131
import { is_reserved_keyword } from './utils/reserved_keywords';
32+
import Element from './nodes/Element';
3233

3334
interface ComponentOptions {
3435
namespace?: string;
@@ -85,6 +86,7 @@ export default class Component {
8586
file: string;
8687
locate: (c: number) => { line: number; column: number };
8788

89+
elements: Element[] = [];
8890
stylesheet: Stylesheet;
8991

9092
aliases: Map<string, Identifier> = new Map();
@@ -171,8 +173,8 @@ export default class Component {
171173

172174
this.walk_instance_js_post_template();
173175

176+
this.elements.forEach(element => this.stylesheet.apply(element));
174177
if (!compile_options.customElement) this.stylesheet.reify();
175-
176178
this.stylesheet.warn_on_unused_selectors(this);
177179
}
178180

@@ -221,6 +223,10 @@ export default class Component {
221223
return this.aliases.get(name);
222224
}
223225

226+
apply_stylesheet(element: Element) {
227+
this.elements.push(element);
228+
}
229+
224230
global(name: string) {
225231
const alias = this.alias(name);
226232
this.globals.set(name, alias);

src/compiler/compile/css/Selector.ts

+179-8
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@ import { gather_possible_values, UNKNOWN } from './gather_possible_values';
44
import { CssNode } from './interfaces';
55
import Component from '../Component';
66
import Element from '../nodes/Element';
7+
import { INode } from '../nodes/interfaces';
8+
import EachBlock from '../nodes/EachBlock';
9+
import IfBlock from '../nodes/IfBlock';
10+
import AwaitBlock from '../nodes/AwaitBlock';
711

812
enum BlockAppliesToNode {
913
NotPossible,
1014
Possible,
1115
UnknownSelectorType
1216
}
17+
enum NodeExist {
18+
Probably = 1,
19+
Definitely = 2,
20+
}
1321

1422
const whitelist_attribute_selector = new Map([
1523
['details', new Set(['open'])]
@@ -39,10 +47,10 @@ export default class Selector {
3947
this.used = this.local_blocks.length === 0;
4048
}
4149

42-
apply(node: Element, stack: Element[]) {
50+
apply(node: Element) {
4351
const to_encapsulate: any[] = [];
4452

45-
apply_selector(this.local_blocks.slice(), node, stack.slice(), to_encapsulate);
53+
apply_selector(this.local_blocks.slice(), node, to_encapsulate);
4654

4755
if (to_encapsulate.length > 0) {
4856
to_encapsulate.forEach(({ node, block }) => {
@@ -149,7 +157,7 @@ export default class Selector {
149157
}
150158
}
151159

152-
function apply_selector(blocks: Block[], node: Element, stack: Element[], to_encapsulate: any[]): boolean {
160+
function apply_selector(blocks: Block[], node: Element, to_encapsulate: any[]): boolean {
153161
const block = blocks.pop();
154162
if (!block) return false;
155163

@@ -162,7 +170,7 @@ function apply_selector(blocks: Block[], node: Element, stack: Element[], to_enc
162170
return false;
163171

164172
case BlockAppliesToNode.UnknownSelectorType:
165-
// bail. TODO figure out what these could be
173+
// bail. TODO figure out what these could be
166174
to_encapsulate.push({ node, block });
167175
return true;
168176
}
@@ -174,9 +182,10 @@ function apply_selector(blocks: Block[], node: Element, stack: Element[], to_enc
174182
continue;
175183
}
176184

177-
for (const stack_node of stack) {
178-
if (block_might_apply_to_node(ancestor_block, stack_node) !== BlockAppliesToNode.NotPossible) {
179-
to_encapsulate.push({ node: stack_node, block: ancestor_block });
185+
let parent = node;
186+
while (parent = get_element_parent(parent)) {
187+
if (block_might_apply_to_node(ancestor_block, parent) !== BlockAppliesToNode.NotPossible) {
188+
to_encapsulate.push({ node: parent, block: ancestor_block });
180189
}
181190
}
182191

@@ -193,12 +202,22 @@ function apply_selector(blocks: Block[], node: Element, stack: Element[], to_enc
193202

194203
return false;
195204
} else if (block.combinator.name === '>') {
196-
if (apply_selector(blocks, stack.pop(), stack, to_encapsulate)) {
205+
if (apply_selector(blocks, get_element_parent(node), to_encapsulate)) {
197206
to_encapsulate.push({ node, block });
198207
return true;
199208
}
200209

201210
return false;
211+
} else if (block.combinator.name === '+' || block.combinator.name === '~') {
212+
const siblings = get_possible_element_siblings(node, block.combinator.name === '+');
213+
let has_match = false;
214+
for (const possible_sibling of siblings.keys()) {
215+
if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) {
216+
to_encapsulate.push({ node, block });
217+
has_match = true;
218+
}
219+
}
220+
return has_match;
202221
}
203222

204223
// TODO other combinators
@@ -376,6 +395,158 @@ function unquote(value: CssNode) {
376395
return str;
377396
}
378397

398+
function get_element_parent(node: Element): Element | null {
399+
let parent: INode = node;
400+
while ((parent = parent.parent) && parent.type !== 'Element');
401+
return parent as Element | null;
402+
}
403+
404+
function get_possible_element_siblings(node: INode, adjacent_only: boolean): Map<Element, NodeExist> {
405+
const result: Map<Element, NodeExist> = new Map();
406+
let prev: INode = node;
407+
while (prev = prev.prev) {
408+
if (prev.type === 'Element') {
409+
if (!prev.attributes.find(attr => attr.name.toLowerCase() === 'slot')) {
410+
result.set(prev, NodeExist.Definitely);
411+
}
412+
413+
if (adjacent_only) {
414+
break;
415+
}
416+
} else if (prev.type === 'EachBlock' || prev.type === 'IfBlock' || prev.type === 'AwaitBlock') {
417+
const possible_last_child = get_possible_last_child(prev, adjacent_only);
418+
419+
add_to_map(possible_last_child, result);
420+
if (adjacent_only && has_definite_elements(possible_last_child)) {
421+
return result;
422+
}
423+
}
424+
}
425+
426+
if (!prev || !adjacent_only) {
427+
let parent: INode = node;
428+
let skip_each_for_last_child = node.type === 'ElseBlock';
429+
while ((parent = parent.parent) && (parent.type === 'EachBlock' || parent.type === 'IfBlock' || parent.type === 'ElseBlock' || parent.type === 'AwaitBlock')) {
430+
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
431+
add_to_map(possible_siblings, result);
432+
433+
if (parent.type === 'EachBlock') {
434+
// first child of each block can select the last child of each block as previous sibling
435+
if (skip_each_for_last_child) {
436+
skip_each_for_last_child = false;
437+
} else {
438+
add_to_map(get_possible_last_child(parent, adjacent_only), result);
439+
}
440+
} else if (parent.type === 'ElseBlock') {
441+
skip_each_for_last_child = true;
442+
parent = parent.parent;
443+
}
444+
445+
if (adjacent_only && has_definite_elements(possible_siblings)) {
446+
break;
447+
}
448+
}
449+
}
450+
451+
return result;
452+
}
453+
454+
function get_possible_last_child(block: EachBlock | IfBlock | AwaitBlock, adjacent_only: boolean): Map<Element, NodeExist> {
455+
const result: Map<Element, NodeExist> = new Map();
456+
457+
if (block.type === 'EachBlock') {
458+
const each_result: Map<Element, NodeExist> = loop_child(block.children, adjacent_only);
459+
const else_result: Map<Element, NodeExist> = block.else ? loop_child(block.else.children, adjacent_only) : new Map();
460+
461+
const not_exhaustive = !has_definite_elements(else_result);
462+
463+
if (not_exhaustive) {
464+
mark_as_probably(each_result);
465+
mark_as_probably(else_result);
466+
}
467+
add_to_map(each_result, result);
468+
add_to_map(else_result, result);
469+
} else if (block.type === 'IfBlock') {
470+
const if_result: Map<Element, NodeExist> = loop_child(block.children, adjacent_only);
471+
const else_result: Map<Element, NodeExist> = block.else ? loop_child(block.else.children, adjacent_only) : new Map();
472+
473+
const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result);
474+
475+
if (not_exhaustive) {
476+
mark_as_probably(if_result);
477+
mark_as_probably(else_result);
478+
}
479+
480+
add_to_map(if_result, result);
481+
add_to_map(else_result, result);
482+
} else if (block.type === 'AwaitBlock') {
483+
const pending_result: Map<Element, NodeExist> = block.pending ? loop_child(block.pending.children, adjacent_only) : new Map();
484+
const then_result: Map<Element, NodeExist> = block.then ? loop_child(block.then.children, adjacent_only) : new Map();
485+
const catch_result: Map<Element, NodeExist> = block.catch ? loop_child(block.catch.children, adjacent_only) : new Map();
486+
487+
const not_exhaustive = !has_definite_elements(pending_result) || !has_definite_elements(then_result) || !has_definite_elements(catch_result);
488+
489+
if (not_exhaustive) {
490+
mark_as_probably(pending_result);
491+
mark_as_probably(then_result);
492+
mark_as_probably(catch_result);
493+
}
494+
495+
add_to_map(pending_result, result);
496+
add_to_map(then_result, result);
497+
add_to_map(catch_result, result);
498+
}
499+
500+
return result;
501+
}
502+
503+
function has_definite_elements(result: Map<Element, NodeExist>): boolean {
504+
if (result.size === 0) return false;
505+
for (const exist of result.values()) {
506+
if (exist === NodeExist.Definitely) {
507+
return true;
508+
}
509+
}
510+
return false;
511+
}
512+
513+
function add_to_map(from: Map<Element, NodeExist>, to: Map<Element, NodeExist>) {
514+
from.forEach((exist, element) => {
515+
to.set(element, higher_existance(exist, to.get(element)));
516+
});
517+
}
518+
519+
function higher_existance(exist1: NodeExist | null, exist2: NodeExist | null): NodeExist {
520+
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
521+
return exist1 > exist2 ? exist1 : exist2;
522+
}
523+
524+
function mark_as_probably(result: Map<Element, NodeExist>) {
525+
for (const key of result.keys()) {
526+
result.set(key, NodeExist.Probably);
527+
}
528+
}
529+
530+
function loop_child(children: INode[], adjacent_only: boolean) {
531+
const result: Map<Element, NodeExist> = new Map();
532+
for (let i = children.length - 1; i >= 0; i--) {
533+
const child = children[i];
534+
if (child.type === 'Element') {
535+
result.set(child, NodeExist.Definitely);
536+
if (adjacent_only) {
537+
break;
538+
}
539+
} else if (child.type === 'EachBlock' || child.type === 'IfBlock' || child.type === 'AwaitBlock') {
540+
const child_result = get_possible_last_child(child, adjacent_only);
541+
add_to_map(child_result, result);
542+
if (adjacent_only && has_definite_elements(child_result)) {
543+
break;
544+
}
545+
}
546+
}
547+
return result;
548+
}
549+
379550
class Block {
380551
global: boolean;
381552
combinator: CssNode;

src/compiler/compile/css/Stylesheet.ts

+6-12
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import MagicString from 'magic-string';
22
import { walk } from 'estree-walker';
33
import Selector from './Selector';
44
import Element from '../nodes/Element';
5-
import { Ast, TemplateNode } from '../../interfaces';
5+
import { Ast } from '../../interfaces';
66
import Component from '../Component';
77
import { CssNode } from './interfaces';
88
import hash from "../utils/hash";
@@ -51,8 +51,8 @@ class Rule {
5151
this.declarations = node.block.children.map((node: CssNode) => new Declaration(node));
5252
}
5353

54-
apply(node: Element, stack: Element[]) {
55-
this.selectors.forEach(selector => selector.apply(node, stack)); // TODO move the logic in here?
54+
apply(node: Element) {
55+
this.selectors.forEach(selector => selector.apply(node)); // TODO move the logic in here?
5656
}
5757

5858
is_used(dev: boolean) {
@@ -162,10 +162,10 @@ class Atrule {
162162
this.declarations = [];
163163
}
164164

165-
apply(node: Element, stack: Element[]) {
165+
apply(node: Element) {
166166
if (this.node.name === 'media' || this.node.name === 'supports') {
167167
this.children.forEach(child => {
168-
child.apply(node, stack);
168+
child.apply(node);
169169
});
170170
}
171171

@@ -364,15 +364,9 @@ export default class Stylesheet {
364364
apply(node: Element) {
365365
if (!this.has_styles) return;
366366

367-
const stack: Element[] = [];
368-
let parent: TemplateNode = node;
369-
while (parent = parent.parent) {
370-
if (parent.type === 'Element') stack.unshift(parent as Element);
371-
}
372-
373367
for (let i = 0; i < this.children.length; i += 1) {
374368
const child = this.children[i];
375-
child.apply(node, stack);
369+
child.apply(node);
376370
}
377371
}
378372

src/compiler/compile/nodes/Element.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import list from '../../utils/list';
1616
import Let from './Let';
1717
import TemplateScope from './shared/TemplateScope';
1818
import { INode } from './interfaces';
19+
import Component from '../Component';
1920

2021
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
2122

@@ -124,7 +125,7 @@ export default class Element extends Node {
124125
namespace: string;
125126
needs_manual_style_scoping: boolean;
126127

127-
constructor(component, parent, scope, info: any) {
128+
constructor(component: Component, parent, scope, info: any) {
128129
super(component, parent, scope, info);
129130
this.name = info.name;
130131

@@ -185,7 +186,7 @@ export default class Element extends Node {
185186

186187
case 'Attribute':
187188
case 'Spread':
188-
// special case
189+
// special case
189190
if (node.name === 'xmlns') this.namespace = node.value[0].data;
190191

191192
this.attributes.push(new Attribute(component, this, scope, node));
@@ -236,7 +237,7 @@ export default class Element extends Node {
236237

237238
this.validate();
238239

239-
component.stylesheet.apply(this);
240+
component.apply_stylesheet(this);
240241
}
241242

242243
validate() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
warnings: []
3+
};

test/css/samples/general-siblings-combinator-await-not-exhaustive/expected.css

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div class="a svelte-xyz"></div>
2+
<div class="d svelte-xyz"></div>
3+
<div class="f svelte-xyz"></div>
4+
<div class="h svelte-xyz"></div>

0 commit comments

Comments
 (0)