Skip to content

Commit 646b0c0

Browse files
committed
optimise <title> - fixes #1027
1 parent 37f8f8a commit 646b0c0

File tree

15 files changed

+424
-1
lines changed

15 files changed

+424
-1
lines changed

src/generators/Generator.ts

+3
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,9 @@ export default class Generator {
738738
} else if (node.name === ':Head') { // TODO do this in parse?
739739
node.type = 'Head';
740740
node.__proto__ = nodes.Head.prototype;
741+
} else if (node.type === 'Element' && node.name === 'title') { // TODO do this in parse?
742+
node.type = 'Title';
743+
node.__proto__ = nodes.Title.prototype;
741744
} else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) {
742745
node.type = 'Slot';
743746
node.__proto__ = nodes.Slot.prototype;

src/generators/nodes/Title.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { stringify } from '../../utils/stringify';
2+
import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
3+
import Node from './shared/Node';
4+
import Block from '../dom/Block';
5+
6+
export default class Title extends Node {
7+
build(
8+
block: Block,
9+
parentNode: string,
10+
parentNodes: string
11+
) {
12+
const isDynamic = !!this.children.find(node => node.type !== 'Text');
13+
14+
if (isDynamic) {
15+
let value;
16+
17+
const allDependencies = new Set();
18+
let shouldCache;
19+
20+
// TODO some of this code is repeated in Tag.ts — would be good to
21+
// DRY it out if that's possible without introducing crazy indirection
22+
if (this.children.length === 1) {
23+
// single {{tag}} — may be a non-string
24+
const { expression } = this.children[0];
25+
const { indexes } = block.contextualise(expression);
26+
const { dependencies, snippet } = this.children[0].metadata;
27+
28+
value = snippet;
29+
dependencies.forEach(d => {
30+
allDependencies.add(d);
31+
});
32+
33+
shouldCache = (
34+
expression.type !== 'Identifier' ||
35+
block.contexts.has(expression.name)
36+
);
37+
} else {
38+
// '{{foo}} {{bar}}' — treat as string concatenation
39+
value =
40+
(this.children[0].type === 'Text' ? '' : `"" + `) +
41+
this.children
42+
.map((chunk: Node) => {
43+
if (chunk.type === 'Text') {
44+
return stringify(chunk.data);
45+
} else {
46+
const { indexes } = block.contextualise(chunk.expression);
47+
const { dependencies, snippet } = chunk.metadata;
48+
49+
dependencies.forEach(d => {
50+
allDependencies.add(d);
51+
});
52+
53+
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
54+
}
55+
})
56+
.join(' + ');
57+
58+
shouldCache = true;
59+
}
60+
61+
const last = shouldCache && block.getUniqueName(
62+
`title_value`
63+
);
64+
65+
if (shouldCache) block.addVariable(last);
66+
67+
let updater;
68+
const init = shouldCache ? `${last} = ${value}` : value;
69+
70+
block.builders.init.addLine(
71+
`document.title = ${init};`
72+
);
73+
updater = `document.title = ${shouldCache ? last : value};`;
74+
75+
if (allDependencies.size) {
76+
const dependencies = Array.from(allDependencies);
77+
const changedCheck = (
78+
( block.hasOutroMethod ? `#outroing || ` : '' ) +
79+
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
80+
);
81+
82+
const updateCachedValue = `${last} !== (${last} = ${value})`;
83+
84+
const condition = shouldCache ?
85+
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
86+
changedCheck;
87+
88+
block.builders.update.addConditional(
89+
condition,
90+
updater
91+
);
92+
}
93+
} else {
94+
const value = stringify(this.children[0].data);
95+
block.builders.hydrate.addLine(`document.title = ${value};`);
96+
}
97+
}
98+
}

src/generators/nodes/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Ref from './Ref';
1919
import Slot from './Slot';
2020
import Text from './Text';
2121
import ThenBlock from './ThenBlock';
22+
import Title from './Title';
2223
import Transition from './Transition';
2324
import Window from './Window';
2425

@@ -43,6 +44,7 @@ const nodes: Record<string, any> = {
4344
Slot,
4445
Text,
4546
ThenBlock,
47+
Title,
4648
Transition,
4749
Window
4850
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { SsrGenerator } from '../index';
2+
import Block from '../Block';
3+
import { escape } from '../../../utils/stringify';
4+
import visit from '../visit';
5+
import { Node } from '../../../interfaces';
6+
7+
export default function visitTitle(
8+
generator: SsrGenerator,
9+
block: Block,
10+
node: Node
11+
) {
12+
generator.append(`<title>`);
13+
14+
node.children.forEach((child: Node) => {
15+
visit(generator, block, child);
16+
});
17+
18+
generator.append(`</title>`);
19+
}

src/generators/server-side-rendering/visitors/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import MustacheTag from './MustacheTag';
99
import RawMustacheTag from './RawMustacheTag';
1010
import Slot from './Slot';
1111
import Text from './Text';
12+
import Title from './Title';
1213
import Window from './Window';
1314

1415
export default {
@@ -23,5 +24,6 @@ export default {
2324
RawMustacheTag,
2425
Slot,
2526
Text,
27+
Title,
2628
Window
2729
};

src/validate/html/validateElement.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import validateEventHandler from './validateEventHandler';
33
import validate, { Validator } from '../index';
44
import { Node } from '../../interfaces';
55

6-
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)$/;
6+
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)$/;
77

88
export default function validateElement(
99
validator: Validator,
@@ -57,6 +57,24 @@ export default function validateElement(
5757
}
5858
}
5959

60+
if (node.name === 'title') {
61+
if (node.attributes.length > 0) {
62+
validator.error(
63+
`<title> cannot have attributes`,
64+
node.attributes[0].start
65+
);
66+
}
67+
68+
node.children.forEach(child => {
69+
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
70+
validator.error(
71+
`<title> can only contain text and {{tags}}`,
72+
child.start
73+
);
74+
}
75+
});
76+
}
77+
6078
let hasIntro: boolean;
6179
let hasOutro: boolean;
6280
let hasTransition: boolean;

src/validate/html/validateHead.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import validateElement from './validateElement';
12
import { Validator } from '../index';
23
import { Node } from '../../interfaces';
34

45
export default function validateHead(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {
56
if (node.attributes.length) {
67
validator.error(`<:Head> should not have any attributes or directives`, node.start);
78
}
9+
10+
// TODO ensure only valid elements are included here
11+
12+
node.children.forEach(node => {
13+
if (node.type !== 'Element') return; // TODO handle {{#if}} and friends?
14+
validateElement(validator, node, refs, refCallees, [], []);
15+
});
816
}

0 commit comments

Comments
 (0)