forked from WebReflection/uhtml
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rabbit.js
216 lines (202 loc) · 8.93 KB
/
rabbit.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import umap from 'umap';
import instrument from 'uparser';
import {indexOf, isArray} from 'uarray';
import {persistent} from 'uwire';
import {handlers} from './handlers.js';
import {createFragment, createWalker} from './node.js';
// from a fragment container, create an array of indexes
// related to its child nodes, so that it's possible
// to retrieve later on exact node via reducePath
const createPath = node => {
const path = [];
let {parentNode} = node;
while (parentNode) {
path.push(indexOf.call(parentNode.childNodes, node));
node = parentNode;
parentNode = node.parentNode;
}
return path;
};
// the prefix is used to identify either comments, attributes, or nodes
// that contain the related unique id. In the attribute cases
// isµX="attribute-name" will be used to map current X update to that
// attribute name, while comments will be like <!--isµX-->, to map
// the update to that specific comment node, hence its parent.
// style and textarea will have <!--isµX--> text content, and are handled
// directly through text-only updates.
const prefix = 'isµ';
// Template Literals are unique per scope and static, meaning a template
// should be parsed once, and once only, as it will always represent the same
// content, within the exact same amount of updates each time.
// This cache relates each template to its unique content and updates.
const cache = umap(new WeakMap);
// a RegExp that helps checking nodes that cannot contain comments
const textOnly = /^(?:plaintext|script|style|textarea|title|xmp)$/i;
export const createCache = () => ({
stack: [], // each template gets a stack for each interpolation "hole"
entry: null, // each entry contains details, such as:
// * the template that is representing
// * the type of node it represents (html or svg)
// * the content fragment with all nodes
// * the list of updates per each node (template holes)
// * the "wired" node or fragment that will get updates
// if the template or type are different from the previous one
// the entry gets re-created each time
wire: null // each rendered node represent some wired content and
// this reference to the latest one. If different, the node
// will be cleaned up and the new "wire" will be appended
});
// the entry stored in the rendered node cache, and per each "hole"
const createEntry = (type, template) => {
const {content, updates} = mapUpdates(type, template);
return {type, template, content, updates, wire: null};
};
// a template is instrumented to be able to retrieve where updates are needed.
// Each unique template becomes a fragment, cloned once per each other
// operation based on the same template, i.e. data => html`<p>${data}</p>`
const mapTemplate = (type, template) => {
const text = instrument(template, prefix, type === 'svg');
const content = createFragment(text, type);
// once instrumented and reproduced as fragment, it's crawled
// to find out where each update is in the fragment tree
const tw = createWalker(content);
const nodes = [];
const length = template.length - 1;
let i = 0;
// updates are searched via unique names, linearly increased across the tree
// <div isµ0="attr" isµ1="other"><!--isµ2--><style><!--isµ3--</style></div>
let search = `${prefix}${i}`;
while (i < length) {
const node = tw.nextNode();
// if not all updates are bound but there's nothing else to crawl
// it means that there is something wrong with the template.
if (!node)
throw `bad template: ${text}`;
// if the current node is a comment, and it contains isµX
// it means the update should take care of any content
if (node.nodeType === 8) {
// The only comments to be considered are those
// which content is exactly the same as the searched one.
if (node.data === search) {
nodes.push({type: 'node', path: createPath(node)});
search = `${prefix}${++i}`;
}
}
else {
// if the node is not a comment, loop through all its attributes
// named isµX and relate attribute updates to this node and the
// attribute name, retrieved through node.getAttribute("isµX")
// the isµX attribute will be removed as irrelevant for the layout
// let svg = -1;
while (node.hasAttribute(search)) {
nodes.push({
type: 'attr',
path: createPath(node),
name: node.getAttribute(search),
//svg: svg < 0 ? (svg = ('ownerSVGElement' in node ? 1 : 0)) : svg
});
node.removeAttribute(search);
search = `${prefix}${++i}`;
}
// if the node was a style, textarea, or others, check its content
// and if it is <!--isµX--> then update tex-only this node
if (
textOnly.test(node.tagName) &&
node.textContent.trim() === `<!--${search}-->`
){
node.textContent = '';
nodes.push({type: 'text', path: createPath(node)});
search = `${prefix}${++i}`;
}
}
}
// once all nodes to update, or their attributes, are known, the content
// will be cloned in the future to represent the template, and all updates
// related to such content retrieved right away without needing to re-crawl
// the exact same template, and its content, more than once.
return {content, nodes};
};
// if a template is unknown, perform the previous mapping, otherwise grab
// its details such as the fragment with all nodes, and updates info.
const mapUpdates = (type, template) => {
const {content, nodes} = (
cache.get(template) ||
cache.set(template, mapTemplate(type, template))
);
// clone deeply the fragment
const fragment = document.importNode(content, true);
// and relate an update handler per each node that needs one
const updates = nodes.map(handlers, fragment);
// return the fragment and all updates to use within its nodes
return {content: fragment, updates};
};
// as html and svg can be nested calls, but no parent node is known
// until rendered somewhere, the unroll operation is needed to
// discover what to do with each interpolation, which will result
// into an update operation.
export const unroll = (info, {type, template, values}) => {
const {length} = values;
// interpolations can contain holes and arrays, so these need
// to be recursively discovered
unrollValues(info, values, length);
let {entry} = info;
// if the cache entry is either null or different from the template
// and the type this unroll should resolve, create a new entry
// assigning a new content fragment and the list of updates.
if (!entry || (entry.template !== template || entry.type !== type))
info.entry = (entry = createEntry(type, template));
const {content, updates, wire} = entry;
// even if the fragment and its nodes is not live yet,
// it is already possible to update via interpolations values.
for (let i = 0; i < length; i++)
updates[i](values[i]);
// if the entry was new, or representing a different template or type,
// create a new persistent entity to use during diffing.
// This is simply a DOM node, when the template has a single container,
// as in `<p></p>`, or a "wire" in `<p></p><p></p>` and similar cases.
return wire || (entry.wire = persistent(content));
};
// the stack retains, per each interpolation value, the cache
// related to each interpolation value, or null, if the render
// was conditional and the value is not special (Array or Hole)
const unrollValues = ({stack}, values, length) => {
for (let i = 0; i < length; i++) {
const hole = values[i];
// each Hole gets unrolled and re-assigned as value
// so that domdiff will deal with a node/wire, not with a hole
if (hole instanceof Hole)
values[i] = unroll(
stack[i] || (stack[i] = createCache()),
hole
);
// arrays are recursively resolved so that each entry will contain
// also a DOM node or a wire, hence it can be diffed if/when needed
else if (isArray(hole))
unrollValues(
stack[i] || (stack[i] = createCache()),
hole,
hole.length
);
// if the value is nothing special, the stack doesn't need to retain data
// this is useful also to cleanup previously retained data, if the value
// was a Hole, or an Array, but not anymore, i.e.:
// const update = content => html`<div>${content}</div>`;
// update(listOfItems); update(null); update(html`hole`)
else
stack[i] = null;
}
if (length < stack.length)
stack.splice(length);
};
/**
* Holds all details wrappers needed to render the content further on.
* @constructor
* @param {string} type The hole type, either `html` or `svg`.
* @param {string[]} template The template literals used to the define the content.
* @param {Array} values Zero, one, or more interpolated values to render.
*/
export function Hole(type, template, values) {
this.type = type;
this.template = template;
this.values = values;
};