From eae98f952dff8d908cb77ec9c5f54f7802e5008d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 12 Mar 2018 11:25:11 -0400 Subject: [PATCH 1/4] add CSS scoping classes when stringifying child nodes - fixes #1223 --- src/generators/nodes/Element.ts | 4 ++-- test/css/samples/cascade-false-nested/_config.js | 7 +++++++ .../samples/cascade-false-nested/expected.css | 1 + .../samples/cascade-false-nested/expected.html | 2 ++ test/css/samples/cascade-false-nested/input.html | 16 ++++++++++++++++ 5 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 test/css/samples/cascade-false-nested/_config.js create mode 100644 test/css/samples/cascade-false-nested/expected.css create mode 100644 test/css/samples/cascade-false-nested/expected.html create mode 100644 test/css/samples/cascade-false-nested/input.html diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index 69997e077c..e836d072ce 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -438,8 +438,8 @@ export default class Element extends Node { } node.attributes.forEach((attr: Node) => { - const value = node._needsCssAttribute && attr.name === 'class' - ? attr.value.concat({ type: 'Text', data: ` ${generator.stylesheet.id}` }) + const value = (node._needsCssAttribute && attr.name === 'class') + ? [{ type: 'Text', data: `${attr.value[0].data} ${generator.stylesheet.id}` }] : attr.value; open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(value)}` diff --git a/test/css/samples/cascade-false-nested/_config.js b/test/css/samples/cascade-false-nested/_config.js new file mode 100644 index 0000000000..b19fe39d23 --- /dev/null +++ b/test/css/samples/cascade-false-nested/_config.js @@ -0,0 +1,7 @@ +export default { + cascade: false, + + data: { + dynamic: 'x' + } +}; \ No newline at end of file diff --git a/test/css/samples/cascade-false-nested/expected.css b/test/css/samples/cascade-false-nested/expected.css new file mode 100644 index 0000000000..87ca7335f6 --- /dev/null +++ b/test/css/samples/cascade-false-nested/expected.css @@ -0,0 +1 @@ +.foo.svelte-xyz{color:red}.bar.svelte-xyz{font-style:italic} \ No newline at end of file diff --git a/test/css/samples/cascade-false-nested/expected.html b/test/css/samples/cascade-false-nested/expected.html new file mode 100644 index 0000000000..7d4198ad35 --- /dev/null +++ b/test/css/samples/cascade-false-nested/expected.html @@ -0,0 +1,2 @@ +text +x \ No newline at end of file diff --git a/test/css/samples/cascade-false-nested/input.html b/test/css/samples/cascade-false-nested/input.html new file mode 100644 index 0000000000..a71a9a497c --- /dev/null +++ b/test/css/samples/cascade-false-nested/input.html @@ -0,0 +1,16 @@ + + text + + + + {{dynamic}} + + + \ No newline at end of file From e4032ea543592f0d8f4316e4c788c54248a1f715 Mon Sep 17 00:00:00 2001 From: Conduitry Date: Mon, 12 Mar 2018 14:35:21 -0400 Subject: [PATCH 2/4] apply CSS scoping classes directly to AST (WIP) --- src/css/Selector.ts | 2 +- src/css/Stylesheet.ts | 2 +- src/generators/nodes/Attribute.ts | 23 ++------ src/generators/nodes/Element.ts | 55 ++++++++++--------- .../server-side-rendering/visitors/Element.ts | 19 +------ 5 files changed, 39 insertions(+), 62 deletions(-) diff --git a/src/css/Selector.ts b/src/css/Selector.ts index 1ca0b240f2..aea2abbe07 100644 --- a/src/css/Selector.ts +++ b/src/css/Selector.ts @@ -31,7 +31,7 @@ export default class Selector { if (toEncapsulate.length > 0) { toEncapsulate.filter((_, i) => i === 0 || i === toEncapsulate.length - 1).forEach(({ node, block }) => { - node._needsCssAttribute = true; + node.addCssClass(); block.shouldEncapsulate = true; }); diff --git a/src/css/Stylesheet.ts b/src/css/Stylesheet.ts index 3b37ca526e..cf0b8d523f 100644 --- a/src/css/Stylesheet.ts +++ b/src/css/Stylesheet.ts @@ -353,7 +353,7 @@ export default class Stylesheet { } if (this.cascade) { - if (stack.length === 0) node._needsCssAttribute = true; + if (stack.length === 0) node.addCssClass(); return; } diff --git a/src/generators/nodes/Attribute.ts b/src/generators/nodes/Attribute.ts index 69d8808730..57b387d20b 100644 --- a/src/generators/nodes/Attribute.ts +++ b/src/generators/nodes/Attribute.ts @@ -141,10 +141,6 @@ export default class Attribute { shouldCache = true; } - if (node._needsCssAttribute && name === 'class') { - value = `(${value}) + " ${this.generator.stylesheet.id}"`; - } - const isSelectValueAttribute = name === 'value' && node.name === 'select'; @@ -227,21 +223,10 @@ export default class Attribute { ); } } else { - const isScopedClassAttribute = ( - name === 'class' && - this.parent._needsCssAttribute && - !this.generator.customElement - ); - - const value = isScopedClassAttribute && this.value !== true - ? this.value.length === 0 - ? `'${this.generator.stylesheet.id}'` - : stringify(this.value[0].data.concat(` ${this.generator.stylesheet.id}`)) - : this.value === true - ? 'true' - : this.value.length === 0 - ? `''` - : stringify(this.value[0].data); + const value = + this.value === true + ? 'true' + : this.value.length === 0 ? `''` : stringify(this.value[0].data); const statement = ( isLegacyInputType diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index e836d072ce..1b5b49f498 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -212,22 +212,11 @@ export default class Element extends Node { block.builders.unmount.addLine(`@detachNode(${name});`); } - // add CSS encapsulation attribute - if (this._needsCssAttribute && !this.generator.customElement) { - if (!this.attributes.find(a => a.type === 'Attribute' && a.name === 'class')) { - block.builders.hydrate.addLine( - this.namespace - ? `@setAttribute(${name}, "class", "${this.generator.stylesheet.id}");` - : `${name}.className = "${this.generator.stylesheet.id}";` - ); - } - - // TODO move this into a class as well? - if (this._cssRefAttribute) { - block.builders.hydrate.addLine( - `@setAttribute(${name}, "svelte-ref-${this._cssRefAttribute}", "");` - ) - } + // TODO move this into a class as well? + if (this._cssRefAttribute) { + block.builders.hydrate.addLine( + `@setAttribute(${name}, "svelte-ref-${this._cssRefAttribute}", "");` + ) } // insert static children with textContent or innerHTML @@ -438,17 +427,9 @@ export default class Element extends Node { } node.attributes.forEach((attr: Node) => { - const value = (node._needsCssAttribute && attr.name === 'class') - ? [{ type: 'Text', data: `${attr.value[0].data} ${generator.stylesheet.id}` }] - : attr.value; - - open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(value)}` + open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(attr.value)}` }); - if (node._needsCssAttribute && !node.attributes.find(a => a.name === 'class')) { - open += ` class="${generator.stylesheet.id}"`; - } - if (isVoidElementName(node.name)) return open + '>'; return `${open}>${node.children.map(toHTML).join('')}`; @@ -688,6 +669,30 @@ export default class Element extends Node { return `@appendNode(${this.var}, ${name}._slotted${this.generator.legacy ? `["default"]` : `.default`});`; } + + addCssClass() { + if (this._addedCssClass || this.generator.customElement) return; + this._addedCssClass = true; + const classAttribute = this.attributes.find(a => a.name === 'class'); + if (classAttribute && classAttribute.value !== true) { + if (classAttribute.value.length === 1 && classAttribute.value[0].type === 'Text') { + classAttribute.value[0].data += ` ${this.generator.stylesheet.id}`; + } else { + (classAttribute.value).push( + new Node({ type: 'Text', data: ` ${this.generator.stylesheet.id}` }) + ); + } + } else { + this.attributes.push( + new Attribute({ + generator: this.generator, + name: 'class', + value: [new Node({ type: 'Text', data: `${this.generator.stylesheet.id}` })], + parent: this, + }) + ); + } + } } function getRenderStatement( diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts index e8a5c91c3b..62c1fc8703 100644 --- a/src/generators/server-side-rendering/visitors/Element.ts +++ b/src/generators/server-side-rendering/visitors/Element.ts @@ -50,25 +50,12 @@ export default function visitElement( block.contextualise(attribute.value[0].expression); openingTag += '${' + attribute.value[0].metadata.snippet + ' ? " ' + attribute.name + '" : "" }'; } else { - const value = attribute.name === 'class' && node._needsCssAttribute - ? attribute.value.concat({ - type: 'Text', - data: ` ${generator.stylesheet.id}` - }) - : attribute.value; - - openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, value)}"`; + openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, attribute.value)}"`; } }); - if (node._needsCssAttribute && !node.attributes.find(a => a.type === 'Attribute' && a.name === 'class')) { - openingTag += ` class="${generator.stylesheet.id}"`; - } - - if (node._needsCssAttribute) { - if (node._cssRefAttribute) { - openingTag += ` svelte-ref-${node._cssRefAttribute}`; - } + if (node._cssRefAttribute) { + openingTag += ` svelte-ref-${node._cssRefAttribute}`; } openingTag += '>'; From 92452ef3567c8d6212f5c1c419df1ee2e6af72f8 Mon Sep 17 00:00:00 2001 From: Conduitry Date: Mon, 12 Mar 2018 19:51:46 -0400 Subject: [PATCH 3/4] separate AST modification into Stylesheet#reify --- src/css/Selector.ts | 7 +++++-- src/css/Stylesheet.ts | 20 +++++++++++++++----- src/generators/Generator.ts | 1 + 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/css/Selector.ts b/src/css/Selector.ts index aea2abbe07..658f6dc52d 100644 --- a/src/css/Selector.ts +++ b/src/css/Selector.ts @@ -1,16 +1,19 @@ import MagicString from 'magic-string'; +import Stylesheet from './Stylesheet'; import { gatherPossibleValues, UNKNOWN } from './gatherPossibleValues'; import { Validator } from '../validate/index'; import { Node } from '../interfaces'; export default class Selector { node: Node; + stylesheet: Stylesheet; blocks: Block[]; localBlocks: Block[]; used: boolean; - constructor(node: Node) { + constructor(node: Node, stylesheet: Stylesheet) { this.node = node; + this.stylesheet = stylesheet; this.blocks = groupSelectors(node); @@ -31,7 +34,7 @@ export default class Selector { if (toEncapsulate.length > 0) { toEncapsulate.filter((_, i) => i === 0 || i === toEncapsulate.length - 1).forEach(({ node, block }) => { - node.addCssClass(); + this.stylesheet.nodesWithCssClass.add(node); block.shouldEncapsulate = true; }); diff --git a/src/css/Stylesheet.ts b/src/css/Stylesheet.ts index cf0b8d523f..d277905c28 100644 --- a/src/css/Stylesheet.ts +++ b/src/css/Stylesheet.ts @@ -14,10 +14,10 @@ class Rule { node: Node; parent: Atrule; - constructor(node: Node, parent?: Atrule) { + constructor(node: Node, stylesheet, parent?: Atrule) { this.node = node; this.parent = parent; - this.selectors = node.selector.children.map((node: Node) => new Selector(node)); + this.selectors = node.selector.children.map((node: Node) => new Selector(node, stylesheet)); this.declarations = node.block.children.map((node: Node) => new Declaration(node)); } @@ -274,6 +274,8 @@ export default class Stylesheet { children: (Rule|Atrule)[]; keyframes: Map; + nodesWithCssClass: Set; + constructor(source: string, parsed: Parsed, filename: string, cascade: boolean, dev: boolean) { this.source = source; this.parsed = parsed; @@ -284,6 +286,8 @@ export default class Stylesheet { this.children = []; this.keyframes = new Map(); + this.nodesWithCssClass = new Set(); + if (parsed.css && parsed.css.children.length) { this.id = `svelte-${hash(parsed.css.content.styles)}`; @@ -322,7 +326,7 @@ export default class Stylesheet { } if (node.type === 'Rule') { - const rule = new Rule(node, currentAtrule); + const rule = new Rule(node, this, currentAtrule); stack.push(rule); if (currentAtrule) { @@ -353,7 +357,7 @@ export default class Stylesheet { } if (this.cascade) { - if (stack.length === 0) node.addCssClass(); + if (stack.length === 0) this.nodesWithCssClass.add(node); return; } @@ -363,6 +367,12 @@ export default class Stylesheet { } } + reify() { + this.nodesWithCssClass.forEach((node: Node) => { + node.addCssClass(); + }); + } + render(cssOutputFilename: string, shouldTransformSelectors: boolean) { if (!this.hasStyles) { return { css: null, cssMap: null }; @@ -438,4 +448,4 @@ export default class Stylesheet { child.warnOnUnusedSelector(handler); }); } -} \ No newline at end of file +} diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 294c7fd8fa..d78bf7bbbb 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -179,6 +179,7 @@ export default class Generator { } this.walkTemplate(); + this.stylesheet.reify(); } addSourcemapLocations(node: Node) { From b35aab373684758ce80a0115d424e9a83d134420 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Mar 2018 11:28:21 -0400 Subject: [PATCH 4/4] only apply stylesheet once --- src/generators/Generator.ts | 2 +- src/generators/nodes/Element.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index d78bf7bbbb..b53851cec9 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -179,7 +179,7 @@ export default class Generator { } this.walkTemplate(); - this.stylesheet.reify(); + if (!this.customElement) this.stylesheet.reify(); } addSourcemapLocations(node: Node) { diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index 1b5b49f498..42db165f9d 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -142,8 +142,6 @@ export default class Element extends Node { this.name.replace(/[^a-zA-Z0-9_$]/g, '_') ); - this.generator.stylesheet.apply(this); - if (this.children.length) { if (this.name === 'pre' || this.name === 'textarea') stripWhitespace = false; this.initChildren(block, stripWhitespace, nextSibling); @@ -671,8 +669,6 @@ export default class Element extends Node { } addCssClass() { - if (this._addedCssClass || this.generator.customElement) return; - this._addedCssClass = true; const classAttribute = this.attributes.find(a => a.name === 'class'); if (classAttribute && classAttribute.value !== true) { if (classAttribute.value.length === 1 && classAttribute.value[0].type === 'Text') {