Skip to content

Commit 1b17192

Browse files
authored
add support for rendering partials (#29)
1 parent 502f474 commit 1b17192

File tree

6 files changed

+128
-41
lines changed

6 files changed

+128
-41
lines changed

scripts/benchmark.js

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {renderAst} from '../dist/src/index.js';
1+
import {renderAstDocument, renderAstNodes} from '../dist/src/index.js';
22
import {parse} from '../dist/test/html-utils.js';
33
import Benchmark from 'benchmark';
44

@@ -8,32 +8,59 @@ function getDoc(n) {
88
return parse(`<html><head></head><body>${nodes}</body></html>`);
99
}
1010

11+
// Returns N-lenth array where each element is a NodeProto
12+
function getNodes(n) {
13+
return new Array(n).fill({
14+
tagid: 0,
15+
value: 'bento-component',
16+
children: [],
17+
attributes: [],
18+
});
19+
}
20+
1121
const doc1 = getDoc(1);
1222
const doc10 = getDoc(10);
1323
const doc100 = getDoc(100);
1424
const doc1000 = getDoc(1000);
1525
const doc10000 = getDoc(10000);
1626
const doc100000 = getDoc(100000);
1727

28+
const nodes1 = getNodes(1);
29+
const nodes10 = getNodes(10);
30+
const nodes100 = getNodes(100);
31+
const nodes1000 = getNodes(1000);
32+
1833
var suite = new Benchmark.Suite();
1934
suite
2035
.add('Document: 1 node', function () {
21-
renderAst(doc1, {});
36+
renderAstDocument(doc1, {});
2237
})
2338
.add('Document: 10 nodes', function () {
24-
renderAst(doc10, {});
39+
renderAstDocument(doc10, {});
2540
})
2641
.add('Document: 100 nodes', function () {
27-
renderAst(doc100, {});
42+
renderAstDocument(doc100, {});
2843
})
2944
.add('Document: 1000 nodes', function () {
30-
renderAst(doc1000, {});
45+
renderAstDocument(doc1000, {});
3146
})
3247
.add('Document: 10000 nodes', function () {
33-
renderAst(doc10000, {});
48+
renderAstDocument(doc10000, {});
3449
})
3550
.add('Document: 100000 nodes', function () {
36-
renderAst(doc100000, {});
51+
renderAstDocument(doc100000, {});
52+
})
53+
.add('Nodes: 1', function () {
54+
renderAstNodes(nodes1, {});
55+
})
56+
.add('Nodes: 10', function () {
57+
renderAstNodes(nodes10, {});
58+
})
59+
.add('Nodes: 100', function () {
60+
renderAstNodes(nodes100, {});
61+
})
62+
.add('Nodes: 1000', function () {
63+
renderAstNodes(nodes1000, {});
3764
})
3865
.on('complete', function () {
3966
const results = Array.from(this);

src/ast.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function isElementNode(node: NodeProto): node is ElementNodeProto {
2121
}
2222

2323
export function fromDocument(doc: Document): TreeProto {
24-
const children = Array.from(doc.childNodes).map(mapDomNodeToNodeProto);
24+
const children = Array.from(doc.childNodes).map(fromNode);
2525
return {
2626
quirks_mode: doc.compatMode === 'BackCompat',
2727
tree: [{tagid: 92, children}],
@@ -34,7 +34,7 @@ enum NodeType {
3434
Text = 3,
3535
}
3636

37-
function mapDomNodeToNodeProto(node: Node): NodeProto {
37+
export function fromNode(node: Node): NodeProto {
3838
if (node.nodeType !== NodeType.Element && node.nodeType !== NodeType.Text) {
3939
throw new Error(`Unsupported nodeType: ${node.nodeType}`);
4040
}
@@ -52,7 +52,7 @@ function mapDomNodeToNodeProto(node: Node): NodeProto {
5252
tagid: getTagId(elementNode.tagName),
5353
value: elementNode.tagName.toLowerCase(),
5454
attributes,
55-
children: Array.from(node.childNodes).map(mapDomNodeToNodeProto),
55+
children: Array.from(node.childNodes).map(fromNode),
5656
};
5757
}
5858

src/dom.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ export function fromTreeProto(ast: TreeProto) {
4747
return doc;
4848
}
4949

50-
export function fromTreeProtoHelper(
51-
nodes: NodeProto[],
52-
doc: Document,
53-
parent: Node
54-
) {
50+
// TODO: research optimization opportunity to reuse the same doc.
51+
export function fromNodeProto(node: NodeProto): Element {
52+
const doc: Document = createDocument();
53+
fromTreeProtoHelper([node], doc, doc);
54+
return doc.children[0];
55+
}
56+
57+
function fromTreeProtoHelper(nodes: NodeProto[], doc: Document, parent: Node) {
5558
for (let i = 0; i < nodes.length; i++) {
5659
const node = nodes[i];
5760
if (!isElementNode(node)) {

src/index.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
import * as ast from './ast.js';
1717
import * as dom from './dom.js';
18-
import {TreeProto} from './protos.js';
18+
import {TreeProto, NodeProto} from './protos.js';
1919

2020
export * from './protos.js';
2121

@@ -27,16 +27,49 @@ function defaultHandleError(tagName, e: Error) {
2727
throw new Error(`[${tagName}]: ${e.stack}`);
2828
}
2929

30-
export function renderAst(
30+
export function renderAstDocument(
3131
tree: TreeProto,
3232
instructions: InstructionMap,
3333
{handleError = defaultHandleError} = {}
3434
): TreeProto {
3535
const doc = dom.fromTreeProto(tree);
36+
renderNodeDeep(doc, instructions, {handleError});
37+
return {
38+
...ast.fromDocument(doc),
39+
// these two don't have clear equivalents in worker-dom,
40+
// so we retain them here.
41+
root: tree.root,
42+
quirks_mode: tree.quirks_mode,
43+
};
44+
}
45+
46+
export function renderAstNodes(
47+
nodes: NodeProto[],
48+
instructions: InstructionMap,
49+
{handleError = defaultHandleError} = {}
50+
): NodeProto[] {
51+
return nodes.map((astNode: NodeProto) => {
52+
const domNode = dom.fromNodeProto(astNode);
53+
renderNodeDeep(domNode, instructions, {handleError});
54+
return ast.fromNode(domNode);
55+
});
56+
}
3657

58+
function renderNodeDeep(
59+
node: Element,
60+
instructions: InstructionMap,
61+
{handleError = defaultHandleError} = {}
62+
): void {
63+
// First render given node if applicable
64+
if (node.tagName.toLowerCase() in instructions) {
65+
const buildDom = instructions[node.tagName.toLowerCase()];
66+
buildDom(node);
67+
}
68+
69+
// Then deeply render all children.
3770
// TODO: Optimization opportunity by writing a custom walk instead of N querySelectorAll.
3871
for (let [tagName, buildDom] of Object.entries(instructions)) {
39-
const elements = doc.querySelectorAll(tagName);
72+
const elements = Array.from(node.querySelectorAll(tagName));
4073
for (const element of elements) {
4174
// Do not render anything inside of templates.
4275
if (isInTemplate(element)) {
@@ -50,12 +83,6 @@ export function renderAst(
5083
}
5184
}
5285
}
53-
54-
const transformedAst = ast.fromDocument(doc);
55-
transformedAst.root = tree.root;
56-
transformedAst.quirks_mode = tree.quirks_mode;
57-
58-
return transformedAst;
5986
}
6087

6188
function isInTemplate(node: Node) {

test/html-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ import * as parse5 from 'parse5';
1717
import {getNumTerms, isElementNode} from '../src/ast.js';
1818
import {DocumentNodeProto, NodeProto, TreeProto} from '../src/protos.js';
1919
import {getTagId} from '../src/htmltagenum.js';
20-
import {renderAst, InstructionMap} from '../src/index.js';
20+
import {renderAstDocument, InstructionMap} from '../src/index.js';
2121

2222
export function renderHtml(html: string, instructions: InstructionMap): string {
2323
const tree = parse(html);
24-
const renderedAst = renderAst(tree, instructions);
24+
const renderedAst = renderAstDocument(tree, instructions);
2525
return print(renderedAst);
2626
}
2727

test/test-index.ts

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import test from 'ava';
1717
import {DocumentNodeProto, NodeProto, TreeProto} from '../src/protos.js';
1818
import {getTagId} from '../src/htmltagenum.js';
19-
import {renderAst} from '../src/index.js';
19+
import {renderAstDocument, renderAstNodes} from '../src/index.js';
2020

2121
/**
2222
* Helper for generating NodeProtos.
@@ -54,7 +54,7 @@ function treeProto(
5454

5555
test('should have no effect with empty instructions', (t) => {
5656
const ast = treeProto();
57-
t.deepEqual(renderAst(ast, {}), ast);
57+
t.deepEqual(renderAstDocument(ast, {}), ast);
5858
});
5959

6060
test('should render provided instruction', (t) => {
@@ -70,7 +70,7 @@ test('should render provided instruction', (t) => {
7070
const expected = treeProto([
7171
h('amp-element', {rendered: ''}, ['answer: 42']),
7272
]);
73-
t.deepEqual(renderAst(ast, instructions), expected);
73+
t.deepEqual(renderAstDocument(ast, instructions), expected);
7474
});
7575

7676
test('should return the error if a single instruction throws', (t) => {
@@ -81,7 +81,7 @@ test('should return the error if a single instruction throws', (t) => {
8181
};
8282

8383
const ast = treeProto(h('amp-fail'));
84-
t.throws(() => renderAst(ast, instructions), {message: /amp-fail/});
84+
t.throws(() => renderAstDocument(ast, instructions), {message: /amp-fail/});
8585
});
8686

8787
test('should only throw the first error even if multiple would throw', (t) => {
@@ -97,10 +97,10 @@ test('should only throw the first error even if multiple would throw', (t) => {
9797
},
9898
};
9999

100-
const ast = treeProto(
101-
h('amp-success', {}, [h('amp-fail1'), h('amp-fail2'), h('amp-fail2')])
102-
);
103-
t.throws(() => renderAst(ast, instructions), {message: /amp-fail1/});
100+
const nodes = h('amp-success', {}, [h('amp-fail1'), h('amp-fail2')]);
101+
const ast = treeProto(nodes);
102+
t.throws(() => renderAstNodes([nodes], instructions), {message: /amp-fail1/});
103+
t.throws(() => renderAstDocument(ast, instructions), {message: /amp-fail1/});
104104
});
105105

106106
test('should allow a custom error handler', (t) => {
@@ -122,7 +122,7 @@ test('should allow a custom error handler', (t) => {
122122

123123
const errors = [];
124124
const handleError = (tagName) => errors.push(tagName);
125-
renderAst(ast, instructions, {handleError});
125+
renderAstDocument(ast, instructions, {handleError});
126126
t.deepEqual(errors, ['amp-fail1', 'amp-fail1', 'amp-fail2']);
127127
});
128128

@@ -136,7 +136,7 @@ test('should be unaffected by async modifications', async (t) => {
136136
};
137137

138138
const ast = treeProto([h('amp-element')]);
139-
const rendered = renderAst(ast, instructions);
139+
const rendered = renderAstDocument(ast, instructions);
140140
await new Promise((r) => setTimeout(r)); // Waits a macrotask.
141141

142142
t.deepEqual(rendered, ast);
@@ -150,18 +150,18 @@ test('should not render elements within templates', (t) => {
150150
};
151151

152152
const ast = treeProto(h('template', {}, [h('amp-element')]));
153-
const rendered = renderAst(ast, instructions);
153+
const rendered = renderAstDocument(ast, instructions);
154154

155155
t.deepEqual(rendered, ast);
156156
});
157157

158158
test('should conserve quirks_mode and root', (t) => {
159159
const tree: [DocumentNodeProto] = [{tagid: 92, children: []}];
160160
let ast: TreeProto = {root: 42, quirks_mode: true, tree};
161-
t.deepEqual(renderAst(ast, {}), ast);
161+
t.deepEqual(renderAstDocument(ast, {}), ast);
162162

163163
ast = {root: 7, quirks_mode: false, tree};
164-
t.deepEqual(renderAst(ast, {}), ast);
164+
t.deepEqual(renderAstDocument(ast, {}), ast);
165165
});
166166

167167
test('should set tagids of element nodes', (t) => {
@@ -172,7 +172,7 @@ test('should set tagids of element nodes', (t) => {
172172
}
173173

174174
const inputAst: TreeProto = treeProto(h('amp-list'));
175-
let renderedAst = renderAst(inputAst, {'amp-list': buildAmpList});
175+
let renderedAst = renderAstDocument(inputAst, {'amp-list': buildAmpList});
176176

177177
t.deepEqual(
178178
renderedAst,
@@ -191,7 +191,7 @@ test('should set num_terms of text nodes', (t) => {
191191
}
192192

193193
const inputAst: TreeProto = treeProto([h('amp-list')]);
194-
let result = renderAst(inputAst, {'amp-list': buildAmpList});
194+
let result = renderAstDocument(inputAst, {'amp-list': buildAmpList});
195195

196196
t.deepEqual(
197197
result,
@@ -200,3 +200,33 @@ test('should set num_terms of text nodes', (t) => {
200200
t.is(result.tree[0].children[0]?.['children']?.[0]?.num_terms, 2);
201201
t.is(result.tree[0].children[0]?.['children']?.[1]?.num_terms, 3);
202202
});
203+
204+
test('should render node partials', (t) => {
205+
function buildAmpList(element) {
206+
const doc = element.ownerDocument;
207+
element.appendChild(doc.createTextNode('element text'));
208+
}
209+
210+
const inputNode: NodeProto = h('amp-list');
211+
const renderedNode: NodeProto = h('amp-list', {}, ['element text']);
212+
213+
let resultSingle = renderAstNodes([inputNode], {'amp-list': buildAmpList});
214+
t.deepEqual(resultSingle, [renderedNode]);
215+
216+
let resultDouble = renderAstNodes([inputNode, inputNode], {
217+
'amp-list': buildAmpList,
218+
});
219+
t.deepEqual(resultDouble, [renderedNode, renderedNode]);
220+
});
221+
222+
test('should deeply render node partials', (t) => {
223+
function buildAmpEl(element: Element) {
224+
element.setAttribute('rendered', '');
225+
}
226+
227+
const inputAst: NodeProto = h('amp-el', {}, [h('amp-el')]);
228+
let result = renderAstNodes([inputAst], {'amp-el': buildAmpEl});
229+
t.deepEqual(result, [
230+
h('amp-el', {rendered: ''}, [h('amp-el', {rendered: ''})]),
231+
]);
232+
});

0 commit comments

Comments
 (0)