Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scope css sibling combinator #5427

Merged
merged 7 commits into from Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
## Unreleased

* Add `|nonpassive` event modifier, explicitly passing `passive: false` ([#2068](https://github.com/sveltejs/svelte/issues/2068))
* Scope CSS selectors with `~` and `+` combinators ([#3104](https://github.com/sveltejs/svelte/issues/3104))
* Fix keyed `{#each}` not reacting to key changing ([#5444](https://github.com/sveltejs/svelte/issues/5444))
* Fix destructuring into store values ([#5449](https://github.com/sveltejs/svelte/issues/5449))
* Fix erroneous `missing-declaration` warning with `use:obj.method` ([#5451](https://github.com/sveltejs/svelte/issues/5451))
Expand Down
8 changes: 7 additions & 1 deletion src/compiler/compile/Component.ts
Expand Up @@ -29,6 +29,7 @@ import add_to_set from './utils/add_to_set';
import check_graph_for_cycles from './utils/check_graph_for_cycles';
import { print, x, b } from 'code-red';
import { is_reserved_keyword } from './utils/reserved_keywords';
import Element from './nodes/Element';

interface ComponentOptions {
namespace?: string;
Expand Down Expand Up @@ -85,6 +86,7 @@ export default class Component {
file: string;
locate: (c: number) => { line: number; column: number };

elements: Element[] = [];
stylesheet: Stylesheet;

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

this.walk_instance_js_post_template();

this.elements.forEach(element => this.stylesheet.apply(element));
if (!compile_options.customElement) this.stylesheet.reify();

this.stylesheet.warn_on_unused_selectors(this);
}

Expand Down Expand Up @@ -221,6 +223,10 @@ export default class Component {
return this.aliases.get(name);
}

apply_stylesheet(element: Element) {
this.elements.push(element);
}

global(name: string) {
const alias = this.alias(name);
this.globals.set(name, alias);
Expand Down
187 changes: 179 additions & 8 deletions src/compiler/compile/css/Selector.ts
Expand Up @@ -4,12 +4,20 @@ import { gather_possible_values, UNKNOWN } from './gather_possible_values';
import { CssNode } from './interfaces';
import Component from '../Component';
import Element from '../nodes/Element';
import { INode } from '../nodes/interfaces';
import EachBlock from '../nodes/EachBlock';
import IfBlock from '../nodes/IfBlock';
import AwaitBlock from '../nodes/AwaitBlock';

enum BlockAppliesToNode {
NotPossible,
Possible,
UnknownSelectorType
}
enum NodeExist {
Probably = 1,
Definitely = 2,
}

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

apply(node: Element, stack: Element[]) {
apply(node: Element) {
const to_encapsulate: any[] = [];

apply_selector(this.local_blocks.slice(), node, stack.slice(), to_encapsulate);
apply_selector(this.local_blocks.slice(), node, to_encapsulate);

if (to_encapsulate.length > 0) {
to_encapsulate.forEach(({ node, block }) => {
Expand Down Expand Up @@ -149,7 +157,7 @@ export default class Selector {
}
}

function apply_selector(blocks: Block[], node: Element, stack: Element[], to_encapsulate: any[]): boolean {
function apply_selector(blocks: Block[], node: Element, to_encapsulate: any[]): boolean {
const block = blocks.pop();
if (!block) return false;

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

case BlockAppliesToNode.UnknownSelectorType:
// bail. TODO figure out what these could be
// bail. TODO figure out what these could be
to_encapsulate.push({ node, block });
return true;
}
Expand All @@ -174,9 +182,10 @@ function apply_selector(blocks: Block[], node: Element, stack: Element[], to_enc
continue;
}

for (const stack_node of stack) {
if (block_might_apply_to_node(ancestor_block, stack_node) !== BlockAppliesToNode.NotPossible) {
to_encapsulate.push({ node: stack_node, block: ancestor_block });
let parent = node;
while (parent = get_element_parent(parent)) {
if (block_might_apply_to_node(ancestor_block, parent) !== BlockAppliesToNode.NotPossible) {
to_encapsulate.push({ node: parent, block: ancestor_block });
}
}

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

return false;
} else if (block.combinator.name === '>') {
if (apply_selector(blocks, stack.pop(), stack, to_encapsulate)) {
if (apply_selector(blocks, get_element_parent(node), to_encapsulate)) {
to_encapsulate.push({ node, block });
return true;
}

return false;
} else if (block.combinator.name === '+' || block.combinator.name === '~') {
const siblings = get_possible_element_siblings(node, block.combinator.name === '+');
let has_match = false;
for (const possible_sibling of siblings.keys()) {
if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) {
to_encapsulate.push({ node, block });
has_match = true;
}
}
return has_match;
}

// TODO other combinators
Expand Down Expand Up @@ -376,6 +395,158 @@ function unquote(value: CssNode) {
return str;
}

function get_element_parent(node: Element): Element | null {
let parent: INode = node;
while ((parent = parent.parent) && parent.type !== 'Element');
return parent as Element | null;
}

function get_possible_element_siblings(node: INode, adjacent_only: boolean): Map<Element, NodeExist> {
const result: Map<Element, NodeExist> = new Map();
let prev: INode = node;
while (prev = prev.prev) {
if (prev.type === 'Element') {
if (!prev.attributes.find(attr => attr.name.toLowerCase() === 'slot')) {
result.set(prev, NodeExist.Definitely);
}

if (adjacent_only) {
break;
}
} else if (prev.type === 'EachBlock' || prev.type === 'IfBlock' || prev.type === 'AwaitBlock') {
const possible_last_child = get_possible_last_child(prev, adjacent_only);

add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
}
}

if (!prev || !adjacent_only) {
let parent: INode = node;
let skip_each_for_last_child = node.type === 'ElseBlock';
while ((parent = parent.parent) && (parent.type === 'EachBlock' || parent.type === 'IfBlock' || parent.type === 'ElseBlock' || parent.type === 'AwaitBlock')) {
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
add_to_map(possible_siblings, result);

if (parent.type === 'EachBlock') {
// first child of each block can select the last child of each block as previous sibling
if (skip_each_for_last_child) {
skip_each_for_last_child = false;
} else {
add_to_map(get_possible_last_child(parent, adjacent_only), result);
}
} else if (parent.type === 'ElseBlock') {
skip_each_for_last_child = true;
parent = parent.parent;
}

if (adjacent_only && has_definite_elements(possible_siblings)) {
break;
}
}
}

return result;
}

function get_possible_last_child(block: EachBlock | IfBlock | AwaitBlock, adjacent_only: boolean): Map<Element, NodeExist> {
const result: Map<Element, NodeExist> = new Map();

if (block.type === 'EachBlock') {
const each_result: Map<Element, NodeExist> = loop_child(block.children, adjacent_only);
const else_result: Map<Element, NodeExist> = block.else ? loop_child(block.else.children, adjacent_only) : new Map();

const not_exhaustive = !has_definite_elements(else_result);

if (not_exhaustive) {
mark_as_probably(each_result);
mark_as_probably(else_result);
}
add_to_map(each_result, result);
add_to_map(else_result, result);
} else if (block.type === 'IfBlock') {
const if_result: Map<Element, NodeExist> = loop_child(block.children, adjacent_only);
const else_result: Map<Element, NodeExist> = block.else ? loop_child(block.else.children, adjacent_only) : new Map();

const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result);

if (not_exhaustive) {
mark_as_probably(if_result);
mark_as_probably(else_result);
}

add_to_map(if_result, result);
add_to_map(else_result, result);
} else if (block.type === 'AwaitBlock') {
const pending_result: Map<Element, NodeExist> = block.pending ? loop_child(block.pending.children, adjacent_only) : new Map();
const then_result: Map<Element, NodeExist> = block.then ? loop_child(block.then.children, adjacent_only) : new Map();
const catch_result: Map<Element, NodeExist> = block.catch ? loop_child(block.catch.children, adjacent_only) : new Map();

const not_exhaustive = !has_definite_elements(pending_result) || !has_definite_elements(then_result) || !has_definite_elements(catch_result);

if (not_exhaustive) {
mark_as_probably(pending_result);
mark_as_probably(then_result);
mark_as_probably(catch_result);
}

add_to_map(pending_result, result);
add_to_map(then_result, result);
add_to_map(catch_result, result);
}

return result;
}

function has_definite_elements(result: Map<Element, NodeExist>): boolean {
if (result.size === 0) return false;
for (const exist of result.values()) {
if (exist === NodeExist.Definitely) {
return true;
}
}
return false;
}

function add_to_map(from: Map<Element, NodeExist>, to: Map<Element, NodeExist>) {
from.forEach((exist, element) => {
to.set(element, higher_existance(exist, to.get(element)));
});
}

function higher_existance(exist1: NodeExist | null, exist2: NodeExist | null): NodeExist {
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
return exist1 > exist2 ? exist1 : exist2;
}

function mark_as_probably(result: Map<Element, NodeExist>) {
for (const key of result.keys()) {
result.set(key, NodeExist.Probably);
}
}

function loop_child(children: INode[], adjacent_only: boolean) {
const result: Map<Element, NodeExist> = new Map();
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (child.type === 'Element') {
result.set(child, NodeExist.Definitely);
if (adjacent_only) {
break;
}
} else if (child.type === 'EachBlock' || child.type === 'IfBlock' || child.type === 'AwaitBlock') {
const child_result = get_possible_last_child(child, adjacent_only);
add_to_map(child_result, result);
if (adjacent_only && has_definite_elements(child_result)) {
break;
}
}
}
return result;
}

class Block {
global: boolean;
combinator: CssNode;
Expand Down
18 changes: 6 additions & 12 deletions src/compiler/compile/css/Stylesheet.ts
Expand Up @@ -2,7 +2,7 @@ import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import Selector from './Selector';
import Element from '../nodes/Element';
import { Ast, TemplateNode } from '../../interfaces';
import { Ast } from '../../interfaces';
import Component from '../Component';
import { CssNode } from './interfaces';
import hash from "../utils/hash";
Expand Down Expand Up @@ -51,8 +51,8 @@ class Rule {
this.declarations = node.block.children.map((node: CssNode) => new Declaration(node));
}

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

is_used(dev: boolean) {
Expand Down Expand Up @@ -162,10 +162,10 @@ class Atrule {
this.declarations = [];
}

apply(node: Element, stack: Element[]) {
apply(node: Element) {
if (this.node.name === 'media' || this.node.name === 'supports') {
this.children.forEach(child => {
child.apply(node, stack);
child.apply(node);
});
}

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

const stack: Element[] = [];
let parent: TemplateNode = node;
while (parent = parent.parent) {
if (parent.type === 'Element') stack.unshift(parent as Element);
}

for (let i = 0; i < this.children.length; i += 1) {
const child = this.children[i];
child.apply(node, stack);
child.apply(node);
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/compiler/compile/nodes/Element.ts
Expand Up @@ -16,6 +16,7 @@ import list from '../../utils/list';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import Component from '../Component';

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)$/;

Expand Down Expand Up @@ -124,7 +125,7 @@ export default class Element extends Node {
namespace: string;
needs_manual_style_scoping: boolean;

constructor(component, parent, scope, info: any) {
constructor(component: Component, parent, scope, info: any) {
super(component, parent, scope, info);
this.name = info.name;

Expand Down Expand Up @@ -185,7 +186,7 @@ export default class Element extends Node {

case 'Attribute':
case 'Spread':
// special case
// special case
if (node.name === 'xmlns') this.namespace = node.value[0].data;

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

this.validate();

component.stylesheet.apply(this);
component.apply_stylesheet(this);
}

validate() {
Expand Down
@@ -0,0 +1,3 @@
export default {
warnings: []
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,4 @@
<div class="a svelte-xyz"></div>
<div class="d svelte-xyz"></div>
<div class="f svelte-xyz"></div>
<div class="h svelte-xyz"></div>