Merge pull request #1053 from sveltejs/gh-1027

optimise <title>
pull/1064/head
Rich Harris 7 years ago committed by GitHub
commit 0df6cfa3d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -721,6 +721,14 @@ export default class Generator {
let indexes = new Set(); let indexes = new Set();
const indexesStack: Set<string>[] = [indexes]; const indexesStack: Set<string>[] = [indexes];
function parentIsHead(node) {
if (!node) return false;
if (node.type === 'Component' || node.type === 'Element') return false;
if (node.type === 'Head') return true;
return parentIsHead(node.parent);
}
walk(html, { walk(html, {
enter(node: Node, parent: Node, key: string) { enter(node: Node, parent: Node, key: string) {
// TODO this is hacky as hell // TODO this is hacky as hell
@ -738,6 +746,9 @@ export default class Generator {
} else if (node.name === ':Head') { // TODO do this in parse? } else if (node.name === ':Head') { // TODO do this in parse?
node.type = 'Head'; node.type = 'Head';
Object.setPrototypeOf(node, nodes.Head.prototype); Object.setPrototypeOf(node, nodes.Head.prototype);
} else if (node.type === 'Element' && node.name === 'title' && parentIsHead(parent)) { // TODO do this in parse?
node.type = 'Title';
Object.setPrototypeOf(node, nodes.Title.prototype);
} else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) { } else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) {
node.type = 'Slot'; node.type = 'Slot';
Object.setPrototypeOf(node, nodes.Slot.prototype); Object.setPrototypeOf(node, nodes.Slot.prototype);

@ -0,0 +1,98 @@
import { stringify } from '../../utils/stringify';
import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
import Node from './shared/Node';
import Block from '../dom/Block';
export default class Title extends Node {
build(
block: Block,
parentNode: string,
parentNodes: string
) {
const isDynamic = !!this.children.find(node => node.type !== 'Text');
if (isDynamic) {
let value;
const allDependencies = new Set();
let shouldCache;
// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.children.length === 1) {
// single {{tag}} — may be a non-string
const { expression } = this.children[0];
const { indexes } = block.contextualise(expression);
const { dependencies, snippet } = this.children[0].metadata;
value = snippet;
dependencies.forEach(d => {
allDependencies.add(d);
});
shouldCache = (
expression.type !== 'Identifier' ||
block.contexts.has(expression.name)
);
} else {
// '{{foo}} {{bar}}' — treat as string concatenation
value =
(this.children[0].type === 'Text' ? '' : `"" + `) +
this.children
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
dependencies.forEach(d => {
allDependencies.add(d);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
shouldCache = true;
}
const last = shouldCache && block.getUniqueName(
`title_value`
);
if (shouldCache) block.addVariable(last);
let updater;
const init = shouldCache ? `${last} = ${value}` : value;
block.builders.init.addLine(
`document.title = ${init};`
);
updater = `document.title = ${shouldCache ? last : value};`;
if (allDependencies.size) {
const dependencies = Array.from(allDependencies);
const changedCheck = (
( block.hasOutroMethod ? `#outroing || ` : '' ) +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck;
block.builders.update.addConditional(
condition,
updater
);
}
} else {
const value = stringify(this.children[0].data);
block.builders.hydrate.addLine(`document.title = ${value};`);
}
}
}

@ -19,6 +19,7 @@ import Ref from './Ref';
import Slot from './Slot'; import Slot from './Slot';
import Text from './Text'; import Text from './Text';
import ThenBlock from './ThenBlock'; import ThenBlock from './ThenBlock';
import Title from './Title';
import Transition from './Transition'; import Transition from './Transition';
import Window from './Window'; import Window from './Window';
@ -43,6 +44,7 @@ const nodes: Record<string, any> = {
Slot, Slot,
Text, Text,
ThenBlock, ThenBlock,
Title,
Transition, Transition,
Window Window
}; };

@ -0,0 +1,19 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { escape } from '../../../utils/stringify';
import visit from '../visit';
import { Node } from '../../../interfaces';
export default function visitTitle(
generator: SsrGenerator,
block: Block,
node: Node
) {
generator.append(`<title>`);
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.append(`</title>`);
}

@ -9,6 +9,7 @@ import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag'; import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot'; import Slot from './Slot';
import Text from './Text'; import Text from './Text';
import Title from './Title';
import Window from './Window'; import Window from './Window';
export default { export default {
@ -23,5 +24,6 @@ export default {
RawMustacheTag, RawMustacheTag,
Slot, Slot,
Text, Text,
Title,
Window Window
}; };

@ -3,7 +3,7 @@ import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index'; import validate, { Validator } from '../index';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
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|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/; 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|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
export default function validateElement( export default function validateElement(
validator: Validator, validator: Validator,
@ -61,6 +61,24 @@ export default function validateElement(
} }
} }
if (node.name === 'title') {
if (node.attributes.length > 0) {
validator.error(
`<title> cannot have attributes`,
node.attributes[0].start
);
}
node.children.forEach(child => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
validator.error(
`<title> can only contain text and {{tags}}`,
child.start
);
}
});
}
let hasIntro: boolean; let hasIntro: boolean;
let hasOutro: boolean; let hasOutro: boolean;
let hasTransition: boolean; let hasTransition: boolean;

@ -1,3 +1,4 @@
import validateElement from './validateElement';
import { Validator } from '../index'; import { Validator } from '../index';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
@ -5,4 +6,11 @@ export default function validateHead(validator: Validator, node: Node, refs: Map
if (node.attributes.length) { if (node.attributes.length) {
validator.error(`<:Head> should not have any attributes or directives`, node.start); validator.error(`<:Head> should not have any attributes or directives`, node.start);
} }
// TODO ensure only valid elements are included here
node.children.forEach(node => {
if (node.type !== 'Element') return; // TODO handle {{#if}} and friends?
validateElement(validator, node, refs, refCallees, [], []);
});
} }

@ -0,0 +1,232 @@
function noop() {}
function assign(target) {
var k,
source,
i = 1,
len = arguments.length;
for (; i < len; i++) {
source = arguments[i];
for (k in source) target[k] = source[k];
}
return target;
}
function appendNode(node, target) {
target.appendChild(node);
}
function insertNode(node, target, anchor) {
target.insertBefore(node, anchor);
}
function detachNode(node) {
node.parentNode.removeChild(node);
}
function createSvgElement(name) {
return document.createElementNS('http://www.w3.org/2000/svg', name);
}
function createText(data) {
return document.createTextNode(data);
}
function blankObject() {
return Object.create(null);
}
function destroy(detach) {
this.destroy = noop;
this.fire('destroy');
this.set = this.get = noop;
if (detach !== false) this._fragment.u();
this._fragment.d();
this._fragment = this._state = null;
}
function differs(a, b) {
return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
function dispatchObservers(component, group, changed, newState, oldState) {
for (var key in group) {
if (!changed[key]) continue;
var newValue = newState[key];
var oldValue = oldState[key];
var callbacks = group[key];
if (!callbacks) continue;
for (var i = 0; i < callbacks.length; i += 1) {
var callback = callbacks[i];
if (callback.__calling) continue;
callback.__calling = true;
callback.call(component, newValue, oldValue);
callback.__calling = false;
}
}
}
function fire(eventName, data) {
var handlers =
eventName in this._handlers && this._handlers[eventName].slice();
if (!handlers) return;
for (var i = 0; i < handlers.length; i += 1) {
handlers[i].call(this, data);
}
}
function get(key) {
return key ? this._state[key] : this._state;
}
function init(component, options) {
component._observers = { pre: blankObject(), post: blankObject() };
component._handlers = blankObject();
component._bind = options._bind;
component.options = options;
component.root = options.root || component;
component.store = component.root.store || options.store;
}
function observe(key, callback, options) {
var group = options && options.defer
? this._observers.post
: this._observers.pre;
(group[key] || (group[key] = [])).push(callback);
if (!options || options.init !== false) {
callback.__calling = true;
callback.call(this, this._state[key]);
callback.__calling = false;
}
return {
cancel: function() {
var index = group[key].indexOf(callback);
if (~index) group[key].splice(index, 1);
}
};
}
function on(eventName, handler) {
if (eventName === 'teardown') return this.on('destroy', handler);
var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
handlers.push(handler);
return {
cancel: function() {
var index = handlers.indexOf(handler);
if (~index) handlers.splice(index, 1);
}
};
}
function set(newState) {
this._set(assign({}, newState));
if (this.root._lock) return;
this.root._lock = true;
callAll(this.root._beforecreate);
callAll(this.root._oncreate);
callAll(this.root._aftercreate);
this.root._lock = false;
}
function _set(newState) {
var oldState = this._state,
changed = {},
dirty = false;
for (var key in newState) {
if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
}
if (!dirty) return;
this._state = assign({}, oldState, newState);
this._recompute(changed, this._state);
if (this._bind) this._bind(changed, this._state);
if (this._fragment) {
dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
this._fragment.p(changed, this._state);
dispatchObservers(this, this._observers.post, changed, this._state, oldState);
}
}
function callAll(fns) {
while (fns && fns.length) fns.pop()();
}
function _mount(target, anchor) {
this._fragment.m(target, anchor);
}
function _unmount() {
if (this._fragment) this._fragment.u();
}
var proto = {
destroy: destroy,
get: get,
fire: fire,
observe: observe,
on: on,
set: set,
teardown: destroy,
_recompute: noop,
_set: _set,
_mount: _mount,
_unmount: _unmount
};
/* generated by Svelte vX.Y.Z */
function create_main_fragment(state, component) {
var svg, title, text;
return {
c: function create() {
svg = createSvgElement("svg");
title = createSvgElement("title");
text = createText("a title");
},
m: function mount(target, anchor) {
insertNode(svg, target, anchor);
appendNode(title, svg);
appendNode(text, title);
},
p: noop,
u: function unmount() {
detachNode(svg);
},
d: noop
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
this._fragment = create_main_fragment(this._state, this);
if (options.target) {
this._fragment.c();
this._fragment.m(options.target, options.anchor || null);
}
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;

@ -0,0 +1,43 @@
/* generated by Svelte vX.Y.Z */
import { appendNode, assign, createSvgElement, createText, detachNode, init, insertNode, noop, proto } from "svelte/shared.js";
function create_main_fragment(state, component) {
var svg, title, text;
return {
c: function create() {
svg = createSvgElement("svg");
title = createSvgElement("title");
text = createText("a title");
},
m: function mount(target, anchor) {
insertNode(svg, target, anchor);
appendNode(title, svg);
appendNode(text, title);
},
p: noop,
u: function unmount() {
detachNode(svg);
},
d: noop
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
this._fragment = create_main_fragment(this._state, this);
if (options.target) {
this._fragment.c();
this._fragment.m(options.target, options.anchor || null);
}
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;

@ -0,0 +1,3 @@
<svg>
<title>a title</title>
</svg>

After

Width:  |  Height:  |  Size: 36 B

@ -0,0 +1,208 @@
function noop() {}
function assign(target) {
var k,
source,
i = 1,
len = arguments.length;
for (; i < len; i++) {
source = arguments[i];
for (k in source) target[k] = source[k];
}
return target;
}
function blankObject() {
return Object.create(null);
}
function destroy(detach) {
this.destroy = noop;
this.fire('destroy');
this.set = this.get = noop;
if (detach !== false) this._fragment.u();
this._fragment.d();
this._fragment = this._state = null;
}
function differs(a, b) {
return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
function dispatchObservers(component, group, changed, newState, oldState) {
for (var key in group) {
if (!changed[key]) continue;
var newValue = newState[key];
var oldValue = oldState[key];
var callbacks = group[key];
if (!callbacks) continue;
for (var i = 0; i < callbacks.length; i += 1) {
var callback = callbacks[i];
if (callback.__calling) continue;
callback.__calling = true;
callback.call(component, newValue, oldValue);
callback.__calling = false;
}
}
}
function fire(eventName, data) {
var handlers =
eventName in this._handlers && this._handlers[eventName].slice();
if (!handlers) return;
for (var i = 0; i < handlers.length; i += 1) {
handlers[i].call(this, data);
}
}
function get(key) {
return key ? this._state[key] : this._state;
}
function init(component, options) {
component._observers = { pre: blankObject(), post: blankObject() };
component._handlers = blankObject();
component._bind = options._bind;
component.options = options;
component.root = options.root || component;
component.store = component.root.store || options.store;
}
function observe(key, callback, options) {
var group = options && options.defer
? this._observers.post
: this._observers.pre;
(group[key] || (group[key] = [])).push(callback);
if (!options || options.init !== false) {
callback.__calling = true;
callback.call(this, this._state[key]);
callback.__calling = false;
}
return {
cancel: function() {
var index = group[key].indexOf(callback);
if (~index) group[key].splice(index, 1);
}
};
}
function on(eventName, handler) {
if (eventName === 'teardown') return this.on('destroy', handler);
var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
handlers.push(handler);
return {
cancel: function() {
var index = handlers.indexOf(handler);
if (~index) handlers.splice(index, 1);
}
};
}
function set(newState) {
this._set(assign({}, newState));
if (this.root._lock) return;
this.root._lock = true;
callAll(this.root._beforecreate);
callAll(this.root._oncreate);
callAll(this.root._aftercreate);
this.root._lock = false;
}
function _set(newState) {
var oldState = this._state,
changed = {},
dirty = false;
for (var key in newState) {
if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
}
if (!dirty) return;
this._state = assign({}, oldState, newState);
this._recompute(changed, this._state);
if (this._bind) this._bind(changed, this._state);
if (this._fragment) {
dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
this._fragment.p(changed, this._state);
dispatchObservers(this, this._observers.post, changed, this._state, oldState);
}
}
function callAll(fns) {
while (fns && fns.length) fns.pop()();
}
function _mount(target, anchor) {
this._fragment.m(target, anchor);
}
function _unmount() {
if (this._fragment) this._fragment.u();
}
var proto = {
destroy: destroy,
get: get,
fire: fire,
observe: observe,
on: on,
set: set,
teardown: destroy,
_recompute: noop,
_set: _set,
_mount: _mount,
_unmount: _unmount
};
/* generated by Svelte vX.Y.Z */
function create_main_fragment(state, component) {
var title_value;
document.title = title_value = "a " + state.custom + " title";
return {
c: noop,
m: noop,
p: function update(changed, state) {
if ((changed.custom) && title_value !== (title_value = "a " + state.custom + " title")) {
document.title = title_value;
}
},
u: noop,
d: noop
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
this._fragment = create_main_fragment(this._state, this);
if (options.target) {
this._fragment.c();
this._fragment.m(options.target, options.anchor || null);
}
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;

@ -0,0 +1,39 @@
/* generated by Svelte vX.Y.Z */
import { assign, init, noop, proto } from "svelte/shared.js";
function create_main_fragment(state, component) {
var title_value;
document.title = title_value = "a " + state.custom + " title";
return {
c: noop,
m: noop,
p: function update(changed, state) {
if ((changed.custom) && title_value !== (title_value = "a " + state.custom + " title")) {
document.title = title_value;
}
},
u: noop,
d: noop
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
this._fragment = create_main_fragment(this._state, this);
if (options.target) {
this._fragment.c();
this._fragment.m(options.target, options.anchor || null);
}
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;

@ -0,0 +1,3 @@
<:Head>
<title>a {{custom}} title</title>
</:Head>

@ -59,6 +59,7 @@ describe("runtime", () => {
} }
const cwd = path.resolve(`test/runtime/samples/${dir}`); const cwd = path.resolve(`test/runtime/samples/${dir}`);
global.document.title = '';
compileOptions = config.compileOptions || {}; compileOptions = config.compileOptions || {};
compileOptions.shared = shared; compileOptions.shared = shared;

@ -0,0 +1,8 @@
[{
"message": "<title> cannot have attributes",
"loc": {
"line": 2,
"column": 8
},
"pos": 16
}]

@ -0,0 +1,3 @@
<:Head>
<title data-invalid=true>a static title</title>
</:Head>

@ -0,0 +1,8 @@
[{
"message": "<title> can only contain text and {{tags}}",
"loc": {
"line": 2,
"column": 11
},
"pos": 19
}]

@ -0,0 +1,3 @@
<:Head>
<title>an <strong>invalid</strong> title</title>
</:Head>
Loading…
Cancel
Save