smarter handling of keyframes - fixes #774

pull/775/head
Rich Harris 7 years ago
parent 9b950f9ac0
commit bf4e6ef6b1

@ -52,7 +52,7 @@ export default class Selector {
}); });
} }
transform(code: MagicString, attr: string, id: string) { transform(code: MagicString, attr: string) {
function encapsulateBlock(block: Block) { function encapsulateBlock(block: Block) {
let i = block.selectors.length; let i = block.selectors.length;
while (i--) { while (i--) {

@ -12,13 +12,11 @@ class Rule {
node: Node; node: Node;
parent: Atrule; parent: Atrule;
constructor(node: Node, parent: Atrule) { constructor(node: Node, parent?: Atrule) {
this.node = node; this.node = node;
this.parent = parent; 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));
this.declarations = node.block.children.map((node: Node) => new Declaration(node)); this.declarations = node.block.children.map((node: Node) => new Declaration(node));
if (parent) parent.rules.push(this);
} }
apply(node: Node, stack: Node[]) { apply(node: Node, stack: Node[]) {
@ -96,6 +94,18 @@ class Rule {
this.declarations.forEach(declaration => declaration.transform(code, keyframes)); this.declarations.forEach(declaration => declaration.transform(code, keyframes));
} }
validate(validator: Validator) {
this.selectors.forEach(selector => {
selector.validate(validator);
});
}
warnOnUnusedSelector(handler: (selector: Selector) => void) {
this.selectors.forEach(selector => {
if (!selector.used) handler(selector);
});
}
} }
class Declaration { class Declaration {
@ -131,11 +141,27 @@ class Declaration {
class Atrule { class Atrule {
node: Node; node: Node;
rules: Rule[]; children: (Atrule|Rule)[];
constructor(node: Node) { constructor(node: Node) {
this.node = node; this.node = node;
this.rules = []; this.children = [];
}
apply(node: Node, stack: Node[]) {
if (this.node.name === 'media') {
this.children.forEach(child => {
child.apply(node, stack);
});
}
else if (this.node.name === 'keyframes') {
this.children.forEach((rule: Rule) => {
rule.selectors.forEach(selector => {
selector.used = true;
});
});
}
} }
isUsed() { isUsed() {
@ -166,11 +192,11 @@ class Atrule {
if (this.node.block) { if (this.node.block) {
let c = this.node.block.start + 1; let c = this.node.block.start + 1;
this.rules.forEach(rule => { this.children.forEach(child => {
if (cascade || rule.isUsed()) { if (cascade || child.isUsed()) {
code.remove(c, rule.node.start); code.remove(c, child.node.start);
rule.minify(code, cascade); child.minify(code, cascade);
c = rule.node.end; c = child.node.end;
} }
}); });
@ -178,19 +204,35 @@ class Atrule {
} }
} }
transform(code: MagicString, id: string, keyframes: Map<string, string>) { transform(code: MagicString, id: string, keyframes: Map<string, string>, cascade: boolean) {
if (this.node.name !== 'keyframes') return; if (this.node.name === 'keyframes') {
this.node.expression.children.forEach(({ type, name, start, end }: Node) => {
this.node.expression.children.forEach((expression: Node) => { if (type === 'Identifier') {
if (expression.type === 'Identifier') { if (name.startsWith('-global-')) {
if (expression.name.startsWith('-global-')) { code.remove(start, start + 8);
code.remove(expression.start, expression.start + 8); } else {
} else { code.overwrite(start, end, keyframes.get(name));
const newName = `${id}-${expression.name}`; }
code.overwrite(expression.start, expression.end, newName);
keyframes.set(expression.name, newName);
} }
} });
}
this.children.forEach(child => {
child.transform(code, id, keyframes, cascade);
})
}
validate(validator: Validator) {
this.children.forEach(child => {
child.validate(validator);
});
}
warnOnUnusedSelector(handler: (selector: Selector) => void) {
if (this.node.name !== 'media') return;
this.children.forEach(child => {
child.warnOnUnusedSelector(handler);
}); });
} }
} }
@ -206,9 +248,8 @@ export default class Stylesheet {
hasStyles: boolean; hasStyles: boolean;
id: string; id: string;
nodes: (Rule|Atrule)[]; children: (Rule|Atrule)[];
rules: Rule[]; keyframes: Map<string, string>;
atrules: Atrule[];
constructor(source: string, parsed: Parsed, filename: string, cascade: boolean) { constructor(source: string, parsed: Parsed, filename: string, cascade: boolean) {
this.source = source; this.source = source;
@ -218,9 +259,8 @@ export default class Stylesheet {
this.id = `svelte-${parsed.hash}`; this.id = `svelte-${parsed.hash}`;
this.nodes = []; this.children = [];
this.rules = []; this.keyframes = new Map();
this.atrules = [];
if (parsed.css && parsed.css.children.length) { if (parsed.css && parsed.css.children.length) {
this.hasStyles = true; this.hasStyles = true;
@ -231,23 +271,33 @@ export default class Stylesheet {
walk(this.parsed.css, { walk(this.parsed.css, {
enter: (node: Node) => { enter: (node: Node) => {
if (node.type === 'Atrule') { if (node.type === 'Atrule') {
const atrule = currentAtrule = new Atrule(node); const atrule = new Atrule(node);
stack.push(atrule); stack.push(atrule);
this.nodes.push(atrule); if (currentAtrule) {
this.atrules.push(atrule); currentAtrule.children.push(atrule);
} else {
this.children.push(atrule);
}
if (node.name === 'keyframes') {
node.expression.children.forEach((expression: Node) => {
if (expression.type === 'Identifier' && !expression.name.startsWith('-global-')) {
this.keyframes.set(expression.name, `${this.id}-${expression.name}`);
}
});
}
currentAtrule = atrule;
} }
if (node.type === 'Rule') { if (node.type === 'Rule') {
// TODO this is a bit confusing. Don't have a separate
// array of rules, just transform top-level nodes and
// let them worry about their children
const rule = new Rule(node, currentAtrule); const rule = new Rule(node, currentAtrule);
this.rules.push(rule); if (currentAtrule) {
currentAtrule.children.push(rule);
if (!currentAtrule) { } else {
this.nodes.push(rule); this.children.push(rule);
} }
} }
}, },
@ -272,9 +322,9 @@ export default class Stylesheet {
return; return;
} }
for (let i = 0; i < this.rules.length; i += 1) { for (let i = 0; i < this.children.length; i += 1) {
const rule = this.rules[i]; const child = this.children[i];
rule.apply(node, stack); child.apply(node, stack);
} }
} }
@ -292,23 +342,16 @@ export default class Stylesheet {
} }
}); });
// TODO all transform/minify in single pass. The mutation of this.children.forEach((child: (Atrule|Rule)) => {
// `keyframes` here is confusing child.transform(code, this.id, this.keyframes, this.cascade);
const keyframes = new Map();
this.atrules.forEach((atrule: Atrule) => {
atrule.transform(code, this.id, keyframes);
});
this.rules.forEach((rule: Rule) => {
rule.transform(code, this.id, keyframes, this.cascade);
}); });
let c = 0; let c = 0;
this.nodes.forEach(node => { this.children.forEach(child => {
if (this.cascade || node.isUsed()) { if (this.cascade || child.isUsed()) {
code.remove(c, node.node.start); code.remove(c, child.node.start);
node.minify(code, this.cascade); child.minify(code, this.cascade);
c = node.node.end; c = child.node.end;
} }
}); });
@ -325,10 +368,8 @@ export default class Stylesheet {
} }
validate(validator: Validator) { validate(validator: Validator) {
this.rules.forEach(rule => { this.children.forEach(child => {
rule.selectors.forEach(selector => { child.validate(validator);
selector.validate(validator);
});
}); });
} }
@ -337,27 +378,27 @@ export default class Stylesheet {
let locator; let locator;
this.rules.forEach((rule: Rule) => { const handler = (selector: Selector) => {
rule.selectors.forEach(selector => { const pos = selector.node.start;
if (!selector.used) {
const pos = selector.node.start; if (!locator) locator = getLocator(this.source);
const { line, column } = locator(pos);
if (!locator) locator = getLocator(this.source);
const { line, column } = locator(pos); const frame = getCodeFrame(this.source, line, column);
const message = `Unused CSS selector`;
const frame = getCodeFrame(this.source, line, column);
const message = `Unused CSS selector`; onwarn({
message,
onwarn({ frame,
message, loc: { line: line + 1, column },
frame, pos,
loc: { line: line + 1, column }, filename: this.filename,
pos, toString: () => `${message} (${line + 1}:${column})\n${frame}`,
filename: this.filename,
toString: () => `${message} (${line + 1}:${column})\n${frame}`,
});
}
}); });
};
this.children.forEach(child => {
child.warnOnUnusedSelector(handler);
}); });
} }
} }

@ -0,0 +1,3 @@
export default {
cascade: false
};

@ -0,0 +1 @@
@keyframes svelte-xyz-why{from{color:red}to{color:blue}}.animated[svelte-xyz]{animation:svelte-xyz-why 2s}

@ -0,0 +1,12 @@
<div class='animated'>animated</div>
<style>
@keyframes why {
from { color: red; }
to { color: blue; }
}
.animated {
animation: why 2s;
}
</style>
Loading…
Cancel
Save