solarite 0.5.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/Solarite-debug.js +4847 -3878
- package/dist/Solarite.js +4583 -3626
- package/dist/Solarite.min.js +2 -4
- package/package.json +19 -6
- package/readme.md +58 -11
- package/src/Globals.js +54 -72
- package/src/HtmlParser.js +90 -90
- package/src/MultiValueMap.js +57 -105
- package/src/NodeGroup.js +624 -470
- package/src/Path.js +224 -211
- package/src/PathToAttribValue.js +401 -261
- package/src/PathToAttribs.js +113 -80
- package/src/PathToComment.js +7 -7
- package/src/PathToComponent.js +183 -188
- package/src/PathToEvent.js +76 -64
- package/src/PathToKey.js +19 -0
- package/src/PathToNodes.js +1053 -566
- package/src/RootNodeGroup.js +120 -8
- package/src/Shell.js +570 -348
- package/src/Solarite.d.ts +134 -113
- package/src/Solarite.js +243 -286
- package/src/Template.js +195 -274
- package/src/Util.js +353 -351
- package/src/assert.js +10 -10
- package/src/assignAttributes.js +63 -0
- package/src/delve.js +55 -43
- package/src/h.js +220 -138
- package/src/jsx-dev-runtime.d.ts +1 -0
- package/src/jsx-dev-runtime.js +5 -0
- package/src/jsx-runtime.d.ts +21 -0
- package/src/jsx-runtime.js +84 -0
- package/src/jsx.js +194 -0
- package/src/toEl.js +77 -82
- package/dist/udomdiff-license.txt +0 -18
- package/src/getArg.js +0 -137
- package/src/hash.js +0 -89
- package/src/udomdiff.js +0 -176
- package/src/unused/FastLookupArray.js +0 -54
- package/src/unused/Hashes.js +0 -339
- package/src/unused/InUse.test.js +0 -92
- package/src/unused/InUseMap.js +0 -98
- package/src/unused/LinkedList.js +0 -117
- package/src/unused/LinkedList.test.js +0 -115
- package/src/unused/Misc.js +0 -13
- package/src/unused/Perf.js +0 -47
- package/src/unused/TrackedArray.js +0 -54
- package/src/unused/WeakArray.js +0 -33
- package/src/watch.js +0 -543
package/src/Shell.js
CHANGED
|
@@ -1,348 +1,570 @@
|
|
|
1
|
-
import assert from "./assert.js";
|
|
2
|
-
import Path from "./Path.js";
|
|
3
|
-
import Util from "./Util.js";
|
|
4
|
-
import Globals from "./Globals.js";
|
|
5
|
-
import HtmlParser from "./HtmlParser.js";
|
|
6
|
-
import PathToEvent from "./PathToEvent.js";
|
|
7
|
-
import PathToAttribValue from "./PathToAttribValue.js";
|
|
8
|
-
import PathToAttribs from "./PathToAttribs.js";
|
|
9
|
-
import PathToNodes from "./PathToNodes.js";
|
|
10
|
-
import PathToComponent from "./PathToComponent.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// events
|
|
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
|
-
if (
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
for (let path of this.paths) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
1
|
+
import assert from "./assert.js";
|
|
2
|
+
import Path from "./Path.js";
|
|
3
|
+
import Util from "./Util.js";
|
|
4
|
+
import Globals from "./Globals.js";
|
|
5
|
+
import HtmlParser from "./HtmlParser.js";
|
|
6
|
+
import PathToEvent from "./PathToEvent.js";
|
|
7
|
+
import PathToAttribValue from "./PathToAttribValue.js";
|
|
8
|
+
import PathToAttribs from "./PathToAttribs.js";
|
|
9
|
+
import PathToNodes from "./PathToNodes.js";
|
|
10
|
+
import PathToComponent from "./PathToComponent.js";
|
|
11
|
+
import PathToKey from "./PathToKey.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A Shell is created from a tagged template expression instantiated as Nodes,
|
|
15
|
+
* but without any expressions filled in.
|
|
16
|
+
* Only one Shell is created for all the items in a loop.
|
|
17
|
+
*
|
|
18
|
+
* When a NodeGroup is created from a Template's html strings,
|
|
19
|
+
* the NodeGroup then clones the Shell's fragment to be its nodes. */
|
|
20
|
+
export default class Shell {
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
|
|
24
|
+
fragment;
|
|
25
|
+
|
|
26
|
+
/** @type {Path[]} Paths to where expressions should go. */
|
|
27
|
+
paths = [];
|
|
28
|
+
|
|
29
|
+
// Elements with events. Is there a reason to use this? We already mark event Exprs in Shell.js.
|
|
30
|
+
// events = [];
|
|
31
|
+
|
|
32
|
+
/** @type {int[][]} Array of paths */
|
|
33
|
+
ids = [];
|
|
34
|
+
|
|
35
|
+
/** @type {int[][]} Array of paths */
|
|
36
|
+
styles = [];
|
|
37
|
+
|
|
38
|
+
/** @type {int[][]} Array of paths */
|
|
39
|
+
scripts = [];
|
|
40
|
+
|
|
41
|
+
/** @type {boolean} True if any of this Shell's own paths is a PathToComponent. */
|
|
42
|
+
hasComponentPaths = false;
|
|
43
|
+
|
|
44
|
+
/** @type {boolean} True if every path consumes exactly one expression and none are components.
|
|
45
|
+
* Lets NodeGroup.applyExprs() use a fast loop without allocating per-path expression arrays. */
|
|
46
|
+
pathsSingleExpr = false;
|
|
47
|
+
|
|
48
|
+
/** @type {boolean} True if this Shell has any ids, styles, or scripts. */
|
|
49
|
+
hasEmbeds = false;
|
|
50
|
+
|
|
51
|
+
/** @type {int} Index of the key=${} expression, or -1 when the template isn't keyed. */
|
|
52
|
+
keyIndex = -1;
|
|
53
|
+
|
|
54
|
+
/** @type {boolean} True when the fragment holds exactly one root element and the resolve
|
|
55
|
+
* program exists. NodeGroups then clone that element directly, skipping a throwaway
|
|
56
|
+
* DocumentFragment wrapper per clone. See setPathsFromFragment(). */
|
|
57
|
+
singleRoot = false;
|
|
58
|
+
|
|
59
|
+
/** @type {boolean} True when NodeGroups can be created via NodeGroup.applyStamp()
|
|
60
|
+
* with no per-instance Path objects. See the stampPaths setup in the constructor. */
|
|
61
|
+
stampable = false;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create the nodes but without filling in the expressions.
|
|
65
|
+
* This is useful because the expression-less nodes created by a template can be cached.
|
|
66
|
+
* @param html {string[]} Html strings, split on places where an expression exists.
|
|
67
|
+
* @param svgMode {boolean} Parse the html in the SVG namespace. */
|
|
68
|
+
constructor(html=null, svgMode=false) {
|
|
69
|
+
if (!html)
|
|
70
|
+
return;
|
|
71
|
+
|
|
72
|
+
//#IFDEV
|
|
73
|
+
this._html = html.join('');
|
|
74
|
+
//#ENDIF
|
|
75
|
+
|
|
76
|
+
// If no html tags or entities, just create a text node.
|
|
77
|
+
if (html.length === 1 && !html[0].match(/[<&]/)) {
|
|
78
|
+
this.fragment = Globals.doc.createTextNode(html[0]);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
// 1. Add placeholders
|
|
84
|
+
let htmlWithPlaceholders = Shell.addPlaceholders(html);
|
|
85
|
+
|
|
86
|
+
let template = Globals.doc.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
87
|
+
if (htmlWithPlaceholders) {
|
|
88
|
+
// Wrap in <svg> so the parser's foreign-content rules create the nodes in the SVG namespace,
|
|
89
|
+
// then lift the children back out so the fragment has no wrapper.
|
|
90
|
+
if (svgMode) {
|
|
91
|
+
template.innerHTML = '<svg>' + htmlWithPlaceholders + '</svg>';
|
|
92
|
+
let svgEl = template.content.firstChild;
|
|
93
|
+
let frag = Globals.doc.createDocumentFragment();
|
|
94
|
+
while (svgEl.firstChild)
|
|
95
|
+
frag.append(svgEl.firstChild);
|
|
96
|
+
this.fragment = frag;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
template.innerHTML = htmlWithPlaceholders;
|
|
100
|
+
this.fragment = template.content;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else { // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
104
|
+
template.content.append(Globals.doc.createTextNode(''))
|
|
105
|
+
this.fragment = template.content;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 1b. Remove whitespace-only text nodes inside table-structure elements.
|
|
109
|
+
// The parser foster-parents non-whitespace text out of tables, and whitespace-only
|
|
110
|
+
// text between cells/rows is never rendered, so removing it is invisible.
|
|
111
|
+
// Smaller fragments make cloning, path resolution, and insertion faster.
|
|
112
|
+
stripTableWhitespace(this.fragment);
|
|
113
|
+
|
|
114
|
+
// 2. Find placeholders
|
|
115
|
+
let node;
|
|
116
|
+
let toRemove = [];
|
|
117
|
+
let placeholdersUsed = 0;
|
|
118
|
+
const walker = Globals.doc.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
119
|
+
while (node = walker.nextNode()) {
|
|
120
|
+
|
|
121
|
+
// Remove previous elements after each iteration, so paths will still be calculated correctly.
|
|
122
|
+
toRemove.map(el => el.remove());
|
|
123
|
+
toRemove = [];
|
|
124
|
+
|
|
125
|
+
// Replace attributes
|
|
126
|
+
if (node.nodeType === 1) {
|
|
127
|
+
const hasIs = node.hasAttribute('is');
|
|
128
|
+
const isComponent = (hasIs || node.tagName.includes('-'));
|
|
129
|
+
const componentAttribPaths = [];
|
|
130
|
+
|
|
131
|
+
for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes with placeholders as we go.
|
|
132
|
+
|
|
133
|
+
// The reserved key attribute identifies this template within a keyed list.
|
|
134
|
+
// It's consumed here and never written to the DOM or passed to components.
|
|
135
|
+
if (attr.name === 'key') {
|
|
136
|
+
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
137
|
+
if (parts.length !== 2 || parts[0] !== '' || parts[1] !== '')
|
|
138
|
+
throw new Error(`The key attribute is reserved and must be a single expression: key=\${...}`);
|
|
139
|
+
if (node.parentNode !== this.fragment)
|
|
140
|
+
throw new Error(`The key attribute must be on a top-level element of its template.`);
|
|
141
|
+
if (this.keyIndex >= 0)
|
|
142
|
+
throw new Error(`A template can have only one key attribute.`);
|
|
143
|
+
this.keyIndex = attr.value.charCodeAt(0) - attribPlaceholder;
|
|
144
|
+
|
|
145
|
+
let path = new PathToKey(null, node);
|
|
146
|
+
this.paths.push(path);
|
|
147
|
+
if (isComponent)
|
|
148
|
+
componentAttribPaths.push(path); // Keeps PathToComponent's contiguous expression slices aligned; it skips PathToKey when building args.
|
|
149
|
+
|
|
150
|
+
placeholdersUsed++;
|
|
151
|
+
node.removeAttribute('key');
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// One or more whole attributes
|
|
156
|
+
let matches = attr.name.match(/^[\ue000-\uf8ff]$/)
|
|
157
|
+
if (matches) {
|
|
158
|
+
let path = new PathToAttribs(null, node);
|
|
159
|
+
this.paths.push(path);
|
|
160
|
+
if (isComponent) {
|
|
161
|
+
path.isComponentAttrib = true;
|
|
162
|
+
componentAttribPaths.push(path);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
placeholdersUsed ++;
|
|
166
|
+
node.removeAttribute(matches[0]); // TODO: Is this necessary?
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Just the attribute value.
|
|
170
|
+
else {
|
|
171
|
+
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
172
|
+
if (parts.length > 1) {
|
|
173
|
+
let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
|
|
174
|
+
|
|
175
|
+
let isEvent = Util.isEvent(attr.name);
|
|
176
|
+
let path = isEvent
|
|
177
|
+
? new PathToEvent(null, node, attr.name, nonEmptyParts)
|
|
178
|
+
: new PathToAttribValue(null, node, attr.name, nonEmptyParts);
|
|
179
|
+
path.isHtmlProperty = Util.isHtmlProp(node, attr.name);
|
|
180
|
+
this.paths.push(path);
|
|
181
|
+
if (isComponent) {
|
|
182
|
+
path.isComponentAttrib = true;
|
|
183
|
+
componentAttribPaths.push(path);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
placeholdersUsed += parts.length - 1;
|
|
187
|
+
// In svgMode, setting typed SVG attributes (viewBox, r, etc.) with the placeholders
|
|
188
|
+
// stripped out makes the browser log parse errors, both here and when the fragment is cloned.
|
|
189
|
+
// Remove the attribute instead; apply() recreates it with the real values.
|
|
190
|
+
// Event attributes bound to a single expression are removed because they bind via
|
|
191
|
+
// addEventListener; leaving an empty onclick="" attribute violates a strict CSP when the event fires.
|
|
192
|
+
if (svgMode || (isEvent && !nonEmptyParts))
|
|
193
|
+
node.removeAttribute(attr.name);
|
|
194
|
+
else try {
|
|
195
|
+
node.setAttribute(attr.name, parts.join(''));
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
throw new Error(`Error setting attribute "${attr.name}" on node <${node.tagName}>: ${e.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Web components
|
|
205
|
+
if (isComponent) {
|
|
206
|
+
let path = new PathToComponent(null, node);
|
|
207
|
+
path.attribPaths = componentAttribPaths;
|
|
208
|
+
this.paths.splice(this.paths.length - componentAttribPaths.length, 0, path); // Insert before its componentAttribPaths
|
|
209
|
+
|
|
210
|
+
if (hasIs) {
|
|
211
|
+
node.setAttribute('_is', node.getAttribute('is'));
|
|
212
|
+
node.removeAttribute('is');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Replace comment placeholders
|
|
218
|
+
else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
|
|
219
|
+
|
|
220
|
+
if (node?.parentNode?.closest && node?.parentNode?.closest('[contenteditable]'))
|
|
221
|
+
throw new Error(`Contenteditable can't have expressions inside them. Use <div contenteditable value="\${...}"> instead.`);
|
|
222
|
+
|
|
223
|
+
let parent = node.parentNode;
|
|
224
|
+
|
|
225
|
+
// The expression is the only child of an element, so the element itself
|
|
226
|
+
// can delimit the expression's nodes and no marker comments are needed.
|
|
227
|
+
// Components and slots are excluded because they move their children
|
|
228
|
+
// during instantiation, which would orphan the expression's region.
|
|
229
|
+
if (parent.nodeType === 1 && !node.previousSibling && !node.nextSibling
|
|
230
|
+
&& !parent.tagName.includes('-') && parent.tagName !== 'SLOT' && !parent.hasAttribute('is')) {
|
|
231
|
+
let path = new PathToNodes(null, parent);
|
|
232
|
+
path.wholeParent = true;
|
|
233
|
+
this.paths.push(path);
|
|
234
|
+
placeholdersUsed ++;
|
|
235
|
+
toRemove.push(node); // Removing it here would mess up the treeWalker.
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
else {
|
|
239
|
+
// Get or create nodeBefore.
|
|
240
|
+
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
241
|
+
if (!nodeBefore) {
|
|
242
|
+
nodeBefore = Globals.doc.createComment('Path:'+this.paths.length);
|
|
243
|
+
node.parentNode.insertBefore(nodeBefore, node)
|
|
244
|
+
}
|
|
245
|
+
/*#IFDEV*/assert(nodeBefore);/*#ENDIF*/
|
|
246
|
+
|
|
247
|
+
// Get the next node.
|
|
248
|
+
let nodeMarker;
|
|
249
|
+
|
|
250
|
+
// A subsequent node is available to be a nodeMarker.
|
|
251
|
+
if (node.nextSibling && (node.nextSibling.nodeType !== 8 || node.nextSibling.textContent !== '!✨!')) {
|
|
252
|
+
nodeMarker = node.nextSibling;
|
|
253
|
+
toRemove.push(node); // Removing them here will mess up the treeWalker.
|
|
254
|
+
}
|
|
255
|
+
// Re-use existing comment placeholder.
|
|
256
|
+
else {
|
|
257
|
+
nodeMarker = node;
|
|
258
|
+
nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
|
|
259
|
+
}
|
|
260
|
+
/*#IFDEV*/assert(nodeMarker);/*#ENDIF*/
|
|
261
|
+
|
|
262
|
+
let path = new PathToNodes(nodeBefore, nodeMarker);
|
|
263
|
+
this.paths.push(path);
|
|
264
|
+
placeholdersUsed ++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Comments become text nodes when inside textareas.
|
|
269
|
+
else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
|
|
270
|
+
throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
// Sometimes users will comment out a block of html code that has expressions.
|
|
274
|
+
// Here we look for expressions in comments.
|
|
275
|
+
// We don't actually update them dynamically, but we still add paths for them.
|
|
276
|
+
// That way the expression count still matches.
|
|
277
|
+
else if (node.nodeType === 8) { // Node.COMMENT_NODE
|
|
278
|
+
let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
|
|
279
|
+
for (let i=0; i<parts.length-1; i++) {
|
|
280
|
+
let path = new Path(node.previousSibling, node)
|
|
281
|
+
this.paths.push(path);
|
|
282
|
+
placeholdersUsed ++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Replace comment placeholders inside script and style tags, which have become text nodes.
|
|
287
|
+
else if (node.nodeType === 3 && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) { // Node.TEXT_NODE
|
|
288
|
+
let parts = node.textContent.split(commentPlaceholder);
|
|
289
|
+
if (parts.length > 1) {
|
|
290
|
+
|
|
291
|
+
let placeholders = [];
|
|
292
|
+
for (let i = 0; i<parts.length; i++) {
|
|
293
|
+
let current = Globals.doc.createTextNode(parts[i]);
|
|
294
|
+
node.parentNode.insertBefore(current, node);
|
|
295
|
+
if (i > 0)
|
|
296
|
+
placeholders.push(current)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (let i=0, node; node=placeholders[i]; i++) {
|
|
300
|
+
let path = new PathToNodes(node.previousSibling, node);
|
|
301
|
+
this.paths.push(path);
|
|
302
|
+
placeholdersUsed ++;
|
|
303
|
+
|
|
304
|
+
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Removing them here will mess up the treeWalker.
|
|
308
|
+
toRemove.push(node);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
toRemove.map(el => el.remove());
|
|
313
|
+
|
|
314
|
+
// Less than or equal because there can be one path to multiple expressions
|
|
315
|
+
// if those expressions are in the same attribute value.
|
|
316
|
+
if (placeholdersUsed !== html.length-1)
|
|
317
|
+
throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
|
|
318
|
+
|
|
319
|
+
for (let path of this.paths) {
|
|
320
|
+
if (path.nodeBefore)
|
|
321
|
+
path.nodeBeforeIndex = Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore)
|
|
322
|
+
|
|
323
|
+
// Must be calculated after we remove the toRemove nodes:
|
|
324
|
+
path.nodeMarkerPath = Path.get(path.nodeMarker)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.findEmbeds();
|
|
330
|
+
this.buildResolveProgram();
|
|
331
|
+
|
|
332
|
+
this.pathsSingleExpr = true;
|
|
333
|
+
for (let path of this.paths) {
|
|
334
|
+
if (path instanceof PathToComponent) {
|
|
335
|
+
this.hasComponentPaths = true;
|
|
336
|
+
this.pathsSingleExpr = false;
|
|
337
|
+
break; // Both facts are now decided.
|
|
338
|
+
}
|
|
339
|
+
if (path.getExpressionCount() !== 1)
|
|
340
|
+
this.pathsSingleExpr = false; // Keep scanning for components.
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Stampable shells create NodeGroups without allocating any Path objects:
|
|
344
|
+
// NodeGroup.applyStamp() writes expressions through these shared stamper paths,
|
|
345
|
+
// and real paths are materialized only if a NodeGroup is later rewritten in place.
|
|
346
|
+
// Child-node paths must be wholeParent so their bare-text state can be recovered.
|
|
347
|
+
if (this.singleRoot && this.pathsSingleExpr) {
|
|
348
|
+
let nodesIdx = [];
|
|
349
|
+
let ok = true;
|
|
350
|
+
for (let i=0; i<this.paths.length; i++) {
|
|
351
|
+
let path = this.paths[i];
|
|
352
|
+
if (path instanceof PathToNodes) {
|
|
353
|
+
if (!path.wholeParent) {
|
|
354
|
+
ok = false;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
nodesIdx.push(i);
|
|
358
|
+
}
|
|
359
|
+
else if (!(path instanceof PathToAttribValue || path instanceof PathToKey)) {
|
|
360
|
+
ok = false; // Base Paths from commented-out expressions, etc.
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (ok) {
|
|
365
|
+
this.stampable = true;
|
|
366
|
+
|
|
367
|
+
/** @type {int[]} Indexes of PathToNodes paths, checked for primitive exprs before stamping. */
|
|
368
|
+
this.nodesPathIdx = nodesIdx;
|
|
369
|
+
|
|
370
|
+
/** @type {Path[]} One shared stamper per path; nodeMarker/parentNg are set per use. */
|
|
371
|
+
this.stampPaths = this.paths.map(p => p.cloneWithNodes(null, p.nodeMarker));
|
|
372
|
+
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 1. Add a Unicode placeholder char for where expressions go within attributes.
|
|
381
|
+
* 2. Add a comment placeholder for where expressions are children of other nodes.
|
|
382
|
+
* 3. Append -solarite-placeholder to the tag names of custom components so that we can instantiate them later
|
|
383
|
+
* when we can manually call their constructors with the proper attribute and children arguments from evaluated expressions.
|
|
384
|
+
* @param htmlChunks {string[]}
|
|
385
|
+
* @returns {string} Html with the placeholders in place. */
|
|
386
|
+
static addPlaceholders(htmlChunks) {
|
|
387
|
+
let result = [];
|
|
388
|
+
|
|
389
|
+
let htmlParser = new HtmlParser(); // Reset the context.
|
|
390
|
+
for (let i = 0; i < htmlChunks.length; i++) {
|
|
391
|
+
let lastHtml = htmlChunks[i];
|
|
392
|
+
|
|
393
|
+
// Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
|
|
394
|
+
let lastIndex = 0;
|
|
395
|
+
let context = htmlParser.parse(lastHtml, (html, index, prevContext/*, nextContext*/) => { // This function is called every time the html context changes.
|
|
396
|
+
if (lastIndex !== index) {
|
|
397
|
+
let token = html.slice(lastIndex, index);
|
|
398
|
+
|
|
399
|
+
if (prevContext === HtmlParser.Tag) {
|
|
400
|
+
// Find Web Component tags and append -solarite-placeholder to their tag names
|
|
401
|
+
// This way we can gather their constructor arguments and their children before we call their constructor.
|
|
402
|
+
// Later, PathToComponent.apply() will replace them with the real components.
|
|
403
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
404
|
+
const isWebComponentTagName = /^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i; // a dash in the middle
|
|
405
|
+
token = token.replace(isWebComponentTagName, match => match + '-SOLARITE-PLACEHOLDER'); // caps to match other instances of this string, for better compression.
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
result.push(token);
|
|
409
|
+
}
|
|
410
|
+
lastIndex = index;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Insert placeholders
|
|
414
|
+
if (i < htmlChunks.length - 1) {
|
|
415
|
+
if (context === HtmlParser.Text)
|
|
416
|
+
result.push(commentPlaceholder) // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
417
|
+
else
|
|
418
|
+
result.push(String.fromCharCode(attribPlaceholder + i));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return result.join('');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
|
|
427
|
+
* When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths.
|
|
428
|
+
* Populates:
|
|
429
|
+
* this.scripts
|
|
430
|
+
* this.styles
|
|
431
|
+
* this.ids
|
|
432
|
+
* this.staticComponents */
|
|
433
|
+
findEmbeds() {
|
|
434
|
+
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('script'), el => Path.get(el))
|
|
435
|
+
|
|
436
|
+
// TODO: only find styles that have Paths in them?
|
|
437
|
+
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => Path.get(el))
|
|
438
|
+
|
|
439
|
+
let idEls = this.fragment.querySelectorAll('[id],[data-id]');
|
|
440
|
+
|
|
441
|
+
// Check for valid id names.
|
|
442
|
+
for (let el of idEls) {
|
|
443
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id')
|
|
444
|
+
if (Globals.div.hasOwnProperty(id))
|
|
445
|
+
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.ids = Array.prototype.map.call(idEls, el => Path.get(el))
|
|
449
|
+
|
|
450
|
+
this.hasEmbeds = this.ids.length > 0 || this.styles.length > 0 || this.scripts.length > 0;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Precompute a flat program that resolves every path's nodeMarker/nodeBefore in a cloned
|
|
455
|
+
* fragment with one childNodes access per unique node, sharing ancestor lookups between paths.
|
|
456
|
+
* Replaces per-path root-to-node walks in the hot NodeGroup creation path.
|
|
457
|
+
* Skipped for shells with components, whose clone() has special attribPaths behavior. */
|
|
458
|
+
buildResolveProgram() {
|
|
459
|
+
let hasComponents = false;
|
|
460
|
+
for (let path of this.paths)
|
|
461
|
+
if (path instanceof PathToComponent) {
|
|
462
|
+
hasComponents = true;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
if (hasComponents || !this.paths.length)
|
|
466
|
+
return;
|
|
467
|
+
|
|
468
|
+
let ops = [];
|
|
469
|
+
let slotOf = new Map();
|
|
470
|
+
let frag = this.fragment;
|
|
471
|
+
let nextSlot = 1;
|
|
472
|
+
let getSlot = node => {
|
|
473
|
+
if (node === frag)
|
|
474
|
+
return 0;
|
|
475
|
+
let s = slotOf.get(node);
|
|
476
|
+
if (s === undefined) {
|
|
477
|
+
ops.push(getSlot(node.parentNode), Array.prototype.indexOf.call(node.parentNode.childNodes, node));
|
|
478
|
+
s = nextSlot++;
|
|
479
|
+
slotOf.set(node, s);
|
|
480
|
+
}
|
|
481
|
+
return s;
|
|
482
|
+
};
|
|
483
|
+
for (let path of this.paths) {
|
|
484
|
+
path.markerSlot = path.nodeMarker === frag ? 0 : getSlot(path.nodeMarker);
|
|
485
|
+
path.beforeSlot = path.nodeBefore ? getSlot(path.nodeBefore) : -1;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** @type {?int[]} Flat [parentSlot, childIndex] pairs; pair i fills slot i+1. */
|
|
489
|
+
this.resolveOps = ops;
|
|
490
|
+
|
|
491
|
+
/** @type {Node[]} Reusable scratch array for resolved nodes; safe because resolution never re-enters. */
|
|
492
|
+
this.resolveSlots = new Array(nextSlot);
|
|
493
|
+
|
|
494
|
+
// A lone root element means slot 1 is always that element (the first op pair is [0, 0]),
|
|
495
|
+
// so a NodeGroup can clone the element directly and seed slot 1 with it.
|
|
496
|
+
// Embeds are excluded because their paths are fragment-relative.
|
|
497
|
+
if (!this.hasEmbeds && frag.childNodes.length === 1 && frag.firstChild.nodeType === 1
|
|
498
|
+
&& ops.length >= 2 && ops[0] === 0 && ops[1] === 0)
|
|
499
|
+
this.singleRoot = true;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Get the shell for the html strings.
|
|
504
|
+
* @param htmlStrings {string[]} Typically comes from a Template.
|
|
505
|
+
* @param svgMode {boolean} Parse the html in the SVG namespace.
|
|
506
|
+
* @returns {Shell} */
|
|
507
|
+
static get(htmlStrings, svgMode=false) {
|
|
508
|
+
// One-entry memo, since loops request the same shell for every item.
|
|
509
|
+
if (htmlStrings === lastHtmlStrings && svgMode === lastSvgMode)
|
|
510
|
+
return lastShell;
|
|
511
|
+
|
|
512
|
+
let entry = Globals.shells.get(htmlStrings);
|
|
513
|
+
if (!entry) {
|
|
514
|
+
entry = {};
|
|
515
|
+
Globals.shells.set(htmlStrings, entry); // cache
|
|
516
|
+
}
|
|
517
|
+
let key = svgMode ? 'svg' : 'html';
|
|
518
|
+
let result = entry[key];
|
|
519
|
+
if (!result)
|
|
520
|
+
result = entry[key] = new Shell(htmlStrings, svgMode);
|
|
521
|
+
|
|
522
|
+
lastHtmlStrings = htmlStrings;
|
|
523
|
+
lastSvgMode = svgMode;
|
|
524
|
+
lastShell = result;
|
|
525
|
+
|
|
526
|
+
/*#IFDEV*/result.verify();/*#ENDIF*/
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
//#IFDEV
|
|
531
|
+
// For debugging only:
|
|
532
|
+
verify() {
|
|
533
|
+
for (let path of this.paths) {
|
|
534
|
+
assert(this.fragment.contains(path.getParentNode()))
|
|
535
|
+
path.verify();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
//#ENDIF
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
const commentPlaceholder = `<!--!✨!-->`;
|
|
543
|
+
|
|
544
|
+
// Elements whose whitespace-only text children are never rendered.
|
|
545
|
+
const tableTags = ['TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR'];
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Recursively remove whitespace-only text children of table-structure elements.
|
|
549
|
+
* @param el {DocumentFragment|HTMLElement} */
|
|
550
|
+
function stripTableWhitespace(el) {
|
|
551
|
+
let isTable = el.nodeType === 1 && tableTags.includes(el.tagName);
|
|
552
|
+
let child = el.firstChild;
|
|
553
|
+
while (child) {
|
|
554
|
+
let next = child.nextSibling;
|
|
555
|
+
if (child.nodeType === 1)
|
|
556
|
+
stripTableWhitespace(child);
|
|
557
|
+
else if (isTable && child.nodeType === 3 && !child.nodeValue.trim())
|
|
558
|
+
child.remove();
|
|
559
|
+
child = next;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// One-entry memo for Shell.get().
|
|
564
|
+
let lastHtmlStrings = null, lastSvgMode = false, lastShell = null;
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
// We increment the placeholder char as we go because nodes can't have the same attribute more than once.
|
|
568
|
+
const attribPlaceholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
|
|
569
|
+
|
|
570
|
+
|