apply encapsulating attributes to correct elements and selector parts (#767)

pull/770/head
Rich Harris 7 years ago
parent 276b7998f9
commit c135d0cfab

@ -26,15 +26,16 @@ export default class Selector {
} }
apply(node: Node, stack: Node[]) { apply(node: Node, stack: Node[]) {
const applies = selectorAppliesTo(this.localBlocks.slice(), node, stack.slice()); const toEncapsulate: Node[] = [];
selectorAppliesTo(this.localBlocks.slice(), node, stack.slice(), toEncapsulate);
if (applies) { if (toEncapsulate.length > 0) {
this.used = true; toEncapsulate.filter((_, i) => i === 0 || i === toEncapsulate.length - 1).forEach(({ node, block }) => {
node._needsCssAttribute = true;
block.shouldEncapsulate = true;
});
// add svelte-123xyz attribute to outermost and innermost this.used = true;
// elements — no need to add it to intermediate elements
node._needsCssAttribute = true;
if (stack[0] && this.node.children.find(isDescendantSelector)) stack[0]._needsCssAttribute = true;
} }
} }
@ -86,9 +87,9 @@ export default class Selector {
const first = selector.children[0]; const first = selector.children[0];
const last = selector.children[selector.children.length - 1]; const last = selector.children[selector.children.length - 1];
code.remove(selector.start, first.start).remove(last.end, selector.end); code.remove(selector.start, first.start).remove(last.end, selector.end);
} else if (i === 0 || i === this.blocks.length - 1) {
encapsulateBlock(block);
} }
if (block.shouldEncapsulate) encapsulateBlock(block);
}); });
} }
@ -126,7 +127,7 @@ function isDescendantSelector(selector: Node) {
return selector.type === 'WhiteSpace' || selector.type === 'Combinator'; return selector.type === 'WhiteSpace' || selector.type === 'Combinator';
} }
function selectorAppliesTo(blocks: Block[], node: Node, stack: Node[]): boolean { function selectorAppliesTo(blocks: Block[], node: Node, stack: Node[], toEncapsulate: any[]): boolean {
const block = blocks.pop(); const block = blocks.pop();
if (!block) return false; if (!block) return false;
@ -169,6 +170,7 @@ function selectorAppliesTo(blocks: Block[], node: Node, stack: Node[]): boolean
else if (selector.type === 'RefSelector') { else if (selector.type === 'RefSelector') {
if (node.attributes.some((attr: Node) => attr.type === 'Ref' && attr.name === selector.name)) { if (node.attributes.some((attr: Node) => attr.type === 'Ref' && attr.name === selector.name)) {
node._cssRefAttribute = selector.name; node._cssRefAttribute = selector.name;
toEncapsulate.push({ node, block });
return true; return true;
} }
return; return;
@ -176,6 +178,7 @@ function selectorAppliesTo(blocks: Block[], node: Node, stack: Node[]): boolean
else { else {
// bail. TODO figure out what these could be // bail. TODO figure out what these could be
toEncapsulate.push({ node, block });
return true; return true;
} }
} }
@ -183,20 +186,27 @@ function selectorAppliesTo(blocks: Block[], node: Node, stack: Node[]): boolean
if (block.combinator) { if (block.combinator) {
if (block.combinator.type === 'WhiteSpace') { if (block.combinator.type === 'WhiteSpace') {
while (stack.length) { while (stack.length) {
if (selectorAppliesTo(blocks.slice(), stack.pop(), stack)) { if (selectorAppliesTo(blocks.slice(), stack.pop(), stack, toEncapsulate)) {
toEncapsulate.push({ node, block });
return true; return true;
} }
} }
return false; return false;
} else if (block.combinator.name === '>') { } else if (block.combinator.name === '>') {
return selectorAppliesTo(blocks, stack.pop(), stack); if (selectorAppliesTo(blocks, stack.pop(), stack, toEncapsulate)) {
toEncapsulate.push({ node, block });
return true;
}
return false;
} }
// TODO other combinators // TODO other combinators
toEncapsulate.push({ node, block });
return true; return true;
} }
toEncapsulate.push({ node, block });
return true; return true;
} }
@ -247,6 +257,7 @@ class Block {
selectors: Node[] selectors: Node[]
start: number; start: number;
end: number; end: number;
shouldEncapsulate: boolean;
constructor(combinator: Node) { constructor(combinator: Node) {
this.combinator = combinator; this.combinator = combinator;
@ -255,6 +266,8 @@ class Block {
this.start = null; this.start = null;
this.end = null; this.end = null;
this.shouldEncapsulate = false;
} }
add(selector: Node) { add(selector: Node) {

@ -1,66 +1,78 @@
import assert from "assert"; import assert from 'assert';
import * as fs from "fs"; import * as fs from 'fs';
import { env, normalizeHtml, svelte } from "../helpers.js"; import { env, normalizeHtml, svelte } from '../helpers.js';
function tryRequire(file) { function tryRequire(file) {
try { try {
const mod = require(file); const mod = require(file);
return mod.default || mod; return mod.default || mod;
} catch (err) { } catch (err) {
if (err.code !== "MODULE_NOT_FOUND") throw err; if (err.code !== 'MODULE_NOT_FOUND') throw err;
return null; return null;
} }
} }
function normalizeWarning(warning) { function normalizeWarning(warning) {
warning.frame = warning.frame.replace(/^\n/, '').replace(/^\t+/gm, '').replace(/\s+$/gm, ''); warning.frame = warning.frame
.replace(/^\n/, '')
.replace(/^\t+/gm, '')
.replace(/\s+$/gm, '');
delete warning.filename; delete warning.filename;
delete warning.toString; delete warning.toString;
return warning; return warning;
} }
describe("css", () => { describe('css', () => {
fs.readdirSync("test/css/samples").forEach(dir => { fs.readdirSync('test/css/samples').forEach(dir => {
if (dir[0] === ".") return; if (dir[0] === '.') return;
// add .solo to a sample directory name to only run that test // add .solo to a sample directory name to only run that test
const solo = /\.solo/.test(dir); const solo = /\.solo/.test(dir);
const skip = /\.skip/.test(dir); const skip = /\.skip/.test(dir);
if (solo && process.env.CI) { if (solo && process.env.CI) {
throw new Error("Forgot to remove `solo: true` from test"); throw new Error('Forgot to remove `solo: true` from test');
} }
(solo ? it.only : skip ? it.skip : it)(dir, () => { (solo ? it.only : skip ? it.skip : it)(dir, () => {
const config = tryRequire(`./samples/${dir}/_config.js`) || {}; const config = tryRequire(`./samples/${dir}/_config.js`) || {};
const input = fs const input = fs
.readFileSync(`test/css/samples/${dir}/input.html`, "utf-8") .readFileSync(`test/css/samples/${dir}/input.html`, 'utf-8')
.replace(/\s+$/, ""); .replace(/\s+$/, '');
const expectedWarnings = (config.warnings || []).map(normalizeWarning); const expectedWarnings = (config.warnings || []).map(normalizeWarning);
const domWarnings = []; const domWarnings = [];
const ssrWarnings = []; const ssrWarnings = [];
const dom = svelte.compile(input, Object.assign(config, { const dom = svelte.compile(
format: 'iife', input,
name: 'SvelteComponent', Object.assign(config, {
onwarn: warning => { format: 'iife',
domWarnings.push(warning); name: 'SvelteComponent',
} onwarn: warning => {
})); domWarnings.push(warning);
}
const ssr = svelte.compile(input, Object.assign(config, { })
format: 'iife', );
generate: 'ssr',
name: 'SvelteComponent', const ssr = svelte.compile(
onwarn: warning => { input,
ssrWarnings.push(warning); Object.assign(config, {
} format: 'iife',
})); generate: 'ssr',
name: 'SvelteComponent',
onwarn: warning => {
ssrWarnings.push(warning);
}
})
);
assert.equal(dom.css, ssr.css); assert.equal(dom.css, ssr.css);
assert.deepEqual(domWarnings.map(normalizeWarning), ssrWarnings.map(normalizeWarning)); assert.deepEqual(
domWarnings.map(normalizeWarning),
ssrWarnings.map(normalizeWarning)
);
assert.deepEqual(domWarnings.map(normalizeWarning), expectedWarnings); assert.deepEqual(domWarnings.map(normalizeWarning), expectedWarnings);
fs.writeFileSync(`test/css/samples/${dir}/_actual.css`, dom.css); fs.writeFileSync(`test/css/samples/${dir}/_actual.css`, dom.css);
@ -75,25 +87,32 @@ describe("css", () => {
if (expected.html !== null) { if (expected.html !== null) {
const window = env(); const window = env();
const Component = eval(`(function () { ${dom.code}; return SvelteComponent; }())`); const Component = eval(
const target = window.document.querySelector("main"); `(function () { ${dom.code}; return SvelteComponent; }())`
);
const target = window.document.querySelector('main');
new Component({ target, data: config.data }); new Component({ target, data: config.data });
const html = target.innerHTML; const html = target.innerHTML;
// dom
assert.equal(
normalizeHtml(window, html.replace(/svelte-\d+/g, 'svelte-xyz')),
normalizeHtml(window, expected.html)
);
fs.writeFileSync(`test/css/samples/${dir}/_actual.html`, html); fs.writeFileSync(`test/css/samples/${dir}/_actual.html`, html);
// dom
assert.equal(
normalizeHtml(window, html.replace(/svelte-\d+/g, 'svelte-xyz')),
normalizeHtml(window, expected.html)
);
// ssr // ssr
const component = eval(`(function () { ${ssr.code}; return SvelteComponent; }())`); const component = eval(
`(function () { ${ssr.code}; return SvelteComponent; }())`
);
assert.equal( assert.equal(
normalizeHtml(window, component.render(config.data).replace(/svelte-\d+/g, 'svelte-xyz')), normalizeHtml(
window,
component.render(config.data).replace(/svelte-\d+/g, 'svelte-xyz')
),
normalizeHtml(window, expected.html) normalizeHtml(window, expected.html)
); );
} }
@ -104,7 +123,7 @@ describe("css", () => {
function read(file) { function read(file) {
try { try {
return fs.readFileSync(file, 'utf-8'); return fs.readFileSync(file, 'utf-8');
} catch(err) { } catch (err) {
return null; return null;
} }
} }

@ -0,0 +1 @@
<div><p svelte-xyz=''><span svelte-xyz=''>styled</span></p></div>

@ -0,0 +1,11 @@
<div>
<p>
<span>styled</span>
</p>
</div>
<style>
p span {
color: red;
}
</style>
Loading…
Cancel
Save