Skip to content

optimise <title> #1053

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/generators/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,14 @@ export default class Generator {
let indexes = new Set();
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, {
enter(node: Node, parent: Node, key: string) {
// TODO this is hacky as hell
Expand All @@ -738,6 +746,9 @@ export default class Generator {
} else if (node.name === ':Head') { // TODO do this in parse?
node.type = 'Head';
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) {
node.type = 'Slot';
Object.setPrototypeOf(node, nodes.Slot.prototype);
Expand Down
98 changes: 98 additions & 0 deletions src/generators/nodes/Title.ts
Original file line number Diff line number Diff line change
@@ -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};`);
}
}
}
2 changes: 2 additions & 0 deletions src/generators/nodes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Ref from './Ref';
import Slot from './Slot';
import Text from './Text';
import ThenBlock from './ThenBlock';
import Title from './Title';
import Transition from './Transition';
import Window from './Window';

Expand All @@ -43,6 +44,7 @@ const nodes: Record<string, any> = {
Slot,
Text,
ThenBlock,
Title,
Transition,
Window
};
Expand Down
19 changes: 19 additions & 0 deletions src/generators/server-side-rendering/visitors/Title.ts
Original file line number Diff line number Diff line change
@@ -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>`);
}
2 changes: 2 additions & 0 deletions src/generators/server-side-rendering/visitors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import Text from './Text';
import Title from './Title';
import Window from './Window';

export default {
Expand All @@ -23,5 +24,6 @@ export default {
RawMustacheTag,
Slot,
Text,
Title,
Window
};
20 changes: 19 additions & 1 deletion src/validate/html/validateElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index';
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(
validator: Validator,
Expand Down Expand Up @@ -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 hasOutro: boolean;
let hasTransition: boolean;
Expand Down
8 changes: 8 additions & 0 deletions src/validate/html/validateHead.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import validateElement from './validateElement';
import { Validator } from '../index';
import { Node } from '../../interfaces';

export default function validateHead(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {
if (node.attributes.length) {
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, [], []);
});
}
Loading