diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c2470c1c..5eb2814712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Svelte changelog +## 1.36.0 + +* Optimize `style` attributes where possible ([#455](https://github.com/sveltejs/svelte/issues/455)) + ## 1.35.0 * `set` and `get` continue to work until `destroy` is complete ([#788](https://github.com/sveltejs/svelte/issues/788)) diff --git a/package.json b/package.json index a86cc22e26..a8fdc83580 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "1.35.0", + "version": "1.36.0", "description": "The magical disappearing UI framework", "main": "compiler/svelte.js", "files": [ diff --git a/src/generators/dom/visitors/Element/Attribute.ts b/src/generators/dom/visitors/Element/Attribute.ts index 042c1abdce..47bbea808f 100644 --- a/src/generators/dom/visitors/Element/Attribute.ts +++ b/src/generators/dom/visitors/Element/Attribute.ts @@ -1,5 +1,6 @@ import attributeLookup from './lookup'; import deindent from '../../../../utils/deindent'; +import visitStyleAttribute, { optimizeStyle } from './StyleAttribute'; import { stringify } from '../../../../utils/stringify'; import getStaticAttributeValue from '../../../shared/getStaticAttributeValue'; import { DomGenerator } from '../../index'; @@ -16,6 +17,14 @@ export default function visitAttribute( ) { const name = attribute.name; + if (name === 'style') { + const styleProps = optimizeStyle(attribute.value); + if (styleProps) { + visitStyleAttribute(generator, block, state, node, attribute, styleProps); + return; + } + } + let metadata = state.namespace ? null : attributeLookup[name]; if (metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf(node.name)) metadata = null; @@ -211,4 +220,4 @@ export default function visitAttribute( block.builders.hydrate.addLine(updateValue); if (isDynamic) block.builders.update.addLine(updateValue); } -} +} \ No newline at end of file diff --git a/src/generators/dom/visitors/Element/StyleAttribute.ts b/src/generators/dom/visitors/Element/StyleAttribute.ts new file mode 100644 index 0000000000..b821ed4719 --- /dev/null +++ b/src/generators/dom/visitors/Element/StyleAttribute.ts @@ -0,0 +1,188 @@ +import attributeLookup from './lookup'; +import deindent from '../../../../utils/deindent'; +import { stringify } from '../../../../utils/stringify'; +import getExpressionPrecedence from '../../../../utils/getExpressionPrecedence'; +import getStaticAttributeValue from '../../../shared/getStaticAttributeValue'; +import { DomGenerator } from '../../index'; +import Block from '../../Block'; +import { Node } from '../../../../interfaces'; +import { State } from '../../interfaces'; + +export interface StyleProp { + key: string; + value: Node[]; +} + +export default function visitStyleAttribute( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + attribute: Node, + styleProps: StyleProp[] +) { + styleProps.forEach((prop: StyleProp) => { + let value; + + if (isDynamic(prop.value)) { + const allDependencies = new Set(); + let shouldCache; + let hasChangeableIndex; + + value = + ((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) + + prop.value + .map((chunk: Node) => { + if (chunk.type === 'Text') { + return stringify(chunk.data); + } else { + const { snippet, dependencies, indexes } = block.contextualise(chunk.expression); + + if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) { + hasChangeableIndex = true; + } + + dependencies.forEach(d => { + allDependencies.add(d); + }); + + return getExpressionPrecedence(chunk.expression) <= 13 ? `( ${snippet} )` : snippet; + } + }) + .join(' + '); + + if (allDependencies.size || hasChangeableIndex) { + const dependencies = Array.from(allDependencies); + const condition = ( + ( block.hasOutroMethod ? `#outroing || ` : '' ) + + dependencies.map(dependency => `changed.${dependency}`).join(' || ') + ); + + block.builders.update.addConditional( + condition, + `@setStyle(${node.var}, '${prop.key}', ${value});` + ); + } + } else { + value = stringify(prop.value[0].data); + } + + block.builders.hydrate.addLine( + `@setStyle(${node.var}, '${prop.key}', ${value});` + ); + }); +} + +export function optimizeStyle(value: Node[]) { + let expectingKey = true; + let i = 0; + + const props: { key: string, value: Node[] }[] = []; + let chunks = value.slice(); + + while (chunks.length) { + const chunk = chunks[0]; + + if (chunk.type !== 'Text') return null; + + const keyMatch = /^\s*([\w-]+):\s*/.exec(chunk.data); + if (!keyMatch) return null; + + const key = keyMatch[1]; + + const offset = keyMatch.index + keyMatch[0].length; + const remainingData = chunk.data.slice(offset); + + if (remainingData) { + chunks[0] = { + start: chunk.start + offset, + end: chunk.end, + type: 'Text', + data: remainingData + }; + } else { + chunks.shift(); + } + + const result = getStyleValue(chunks); + if (!result) return null; + + props.push({ key, value: result.value }); + chunks = result.chunks; + } + + return props; +} + +function getStyleValue(chunks: Node[]) { + const value: Node[] = []; + + let inUrl = false; + let quoteMark = null; + let escaped = false; + + while (chunks.length) { + const chunk = chunks.shift(); + + if (chunk.type === 'Text') { + let c = 0; + while (c < chunk.data.length) { + const char = chunk.data[c]; + + if (escaped) { + escaped = false; + } else if (char === '\\') { + escaped = true; + } else if (char === quoteMark) { + quoteMark === null; + } else if (char === '"' || char === "'") { + quoteMark = char; + } else if (char === ')' && inUrl) { + inUrl = false; + } else if (char === 'u' && chunk.data.slice(c, c + 4) === 'url(') { + inUrl = true; + } else if (char === ';' && !inUrl && !quoteMark) { + break; + } + + c += 1; + } + + if (c > 0) { + value.push({ + type: 'Text', + start: chunk.start, + end: chunk.start + c, + data: chunk.data.slice(0, c) + }); + } + + while (/[;\s]/.test(chunk.data[c])) c += 1; + const remainingData = chunk.data.slice(c); + + if (remainingData) { + chunks.unshift({ + start: chunk.start + c, + end: chunk.end, + type: 'Text', + data: remainingData + }); + + break; + } + } + + else { + value.push(chunk); + } + } + + return { + chunks, + value + }; +} + +function isDynamic(value: Node[]) { + return value.length > 1 || value[0].type !== 'Text'; +} \ No newline at end of file diff --git a/src/shared/dom.js b/src/shared/dom.js index 0372d96fdc..e3b1676e1c 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -137,4 +137,8 @@ export function setInputType(input, type) { try { input.type = type; } catch (e) {} +} + +export function setStyle(node, key, value) { + node.style.setProperty(key, value); } \ No newline at end of file diff --git a/src/utils/getExpressionPrecedence.ts b/src/utils/getExpressionPrecedence.ts new file mode 100644 index 0000000000..3e136246ba --- /dev/null +++ b/src/utils/getExpressionPrecedence.ts @@ -0,0 +1,53 @@ +import { Node } from '../interfaces'; + +const binaryOperators: Record = { + '**': 15, + '*': 14, + '/': 14, + '%': 14, + '+': 13, + '-': 13, + '<<': 12, + '>>': 12, + '>>>': 12, + '<': 11, + '<=': 11, + '>': 11, + '>=': 11, + 'in': 11, + 'instanceof': 11, + '==': 10, + '!=': 10, + '===': 10, + '!==': 10, + '&': 9, + '^': 8, + '|': 7 +}; + +const logicalOperators: Record = { + '&&': 6, + '||': 5 +}; + +const precedence: Record number> = { + Literal: () => 21, + Identifier: () => 21, + ParenthesizedExpression: () => 20, + MemberExpression: () => 19, + NewExpression: () => 19, // can be 18 (if no args) but makes no practical difference + CallExpression: () => 19, + UpdateExpression: () => 17, + UnaryExpression: () => 16, + BinaryExpression: (expression: Node) => binaryOperators[expression.operator], + LogicalExpression: (expression: Node) => logicalOperators[expression.operator], + ConditionalExpression: () => 4, + AssignmentExpression: () => 3, + YieldExpression: () => 2, + SpreadElement: () => 1, + SequenceExpression: () => 0 +}; + +export default function getExpressionPrecedence(expression: Node) { + return expression.type in precedence ? precedence[expression.type](expression) : 0; +} \ No newline at end of file diff --git a/test/js/index.js b/test/js/index.js index aa55e7ebad..581df93dd8 100644 --- a/test/js/index.js +++ b/test/js/index.js @@ -39,7 +39,7 @@ describe("js", () => { fs.writeFileSync(`${dir}/_actual.js`, actual); return rollup({ - entry: `${dir}/_actual.js`, + input: `${dir}/_actual.js`, plugins: [ { resolveId(importee, importer) { diff --git a/test/js/samples/inline-style-optimized-multiple/input.html b/test/js/samples/inline-style-optimized-multiple/input.html new file mode 100644 index 0000000000..92d9cb805d --- /dev/null +++ b/test/js/samples/inline-style-optimized-multiple/input.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/test/js/samples/inline-style-optimized-url/input.html b/test/js/samples/inline-style-optimized-url/input.html new file mode 100644 index 0000000000..7e3b2928eb --- /dev/null +++ b/test/js/samples/inline-style-optimized-url/input.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/test/js/samples/inline-style-optimized/input.html b/test/js/samples/inline-style-optimized/input.html new file mode 100644 index 0000000000..ff7d95fb6e --- /dev/null +++ b/test/js/samples/inline-style-optimized/input.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/test/js/samples/inline-style-unoptimized/input.html b/test/js/samples/inline-style-unoptimized/input.html new file mode 100644 index 0000000000..87e4592bfd --- /dev/null +++ b/test/js/samples/inline-style-unoptimized/input.html @@ -0,0 +1,2 @@ +
+
\ No newline at end of file