solarite 0.7.0 → 0.8.0
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/dist/Solarite-debug.js +5193 -4114
- package/dist/Solarite.js +4951 -3878
- package/dist/Solarite.min.js +2 -2
- package/dist/jsx-dev-runtime.js +3 -0
- package/dist/jsx-runtime.js +86 -0
- package/package.json +11 -10
- package/readme.md +8 -8
- package/src/MappedList.js +34 -0
- package/src/MultiValueMap.js +4 -4
- package/src/NodeGroup.js +205 -119
- package/src/Path.js +52 -53
- package/src/PathToAttribValue.js +182 -101
- package/src/PathToAttribs.js +10 -18
- package/src/PathToComponent.js +43 -19
- package/src/PathToEvent.js +18 -12
- package/src/PathToKey.js +0 -6
- package/src/PathToNodes.js +695 -155
- package/src/RootNodeGroup.js +23 -30
- package/src/Selector.js +207 -0
- package/src/Shell.js +276 -117
- package/src/Solarite.d.ts +89 -13
- package/src/Solarite.js +38 -25
- package/src/Template.js +20 -7
- package/src/Util.js +75 -60
- package/src/assert.js +1 -1
- package/src/assignAttributes.js +20 -19
- package/src/h.js +64 -37
- package/src/jsx.js +4 -7
- package/src/toEl.js +1 -1
- package/src/HtmlParser.js +0 -91
- package/src/PathToComment.js +0 -8
package/src/Shell.js
CHANGED
|
@@ -2,7 +2,6 @@ import assert from "./assert.js";
|
|
|
2
2
|
import Path from "./Path.js";
|
|
3
3
|
import Util from "./Util.js";
|
|
4
4
|
import Globals from "./Globals.js";
|
|
5
|
-
import HtmlParser from "./HtmlParser.js";
|
|
6
5
|
import PathToEvent from "./PathToEvent.js";
|
|
7
6
|
import PathToAttribValue from "./PathToAttribValue.js";
|
|
8
7
|
import PathToAttribs from "./PathToAttribs.js";
|
|
@@ -21,7 +20,7 @@ export default class Shell {
|
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
22
|
* @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
|
|
24
|
-
|
|
23
|
+
docFrag;
|
|
25
24
|
|
|
26
25
|
/** @type {Path[]} Paths to where expressions should go. */
|
|
27
26
|
paths = [];
|
|
@@ -41,10 +40,22 @@ export default class Shell {
|
|
|
41
40
|
/** @type {boolean} True if any of this Shell's own paths is a PathToComponent. */
|
|
42
41
|
hasComponentPaths = false;
|
|
43
42
|
|
|
43
|
+
/** @type {boolean} True if any path binds an attribute that's a live HTML property
|
|
44
|
+
* (checked, value, selected — Util.isHtmlProp). Users flip those underneath the template,
|
|
45
|
+
* so "expression unchanged" doesn't mean "DOM unchanged" and the skip shortcuts exempt them. */
|
|
46
|
+
hasLivePropPaths = false;
|
|
47
|
+
|
|
44
48
|
/** @type {boolean} True if every path consumes exactly one expression and none are components.
|
|
45
49
|
* Lets NodeGroup.applyExprs() use a fast loop without allocating per-path expression arrays. */
|
|
46
50
|
pathsSingleExpr = false;
|
|
47
51
|
|
|
52
|
+
/** @type {boolean} True when a NodeGroup whose values are unchanged still has work to do:
|
|
53
|
+
* components re-render so changes deeper in the tree surface, and live HTML properties are
|
|
54
|
+
* rewritten because a click can flip them underneath the cached expression. The list scans
|
|
55
|
+
* check this before calling PathToNodes.refreshSameItem(), so the overwhelmingly common
|
|
56
|
+
* unchanged row costs one field read instead of a call. */
|
|
57
|
+
needsRefresh = false;
|
|
58
|
+
|
|
48
59
|
/** @type {boolean} True if this Shell has any ids, styles, or scripts. */
|
|
49
60
|
hasEmbeds = false;
|
|
50
61
|
|
|
@@ -60,6 +71,52 @@ export default class Shell {
|
|
|
60
71
|
* with no per-instance Path objects. See the stampPaths setup in the constructor. */
|
|
61
72
|
stampable = false;
|
|
62
73
|
|
|
74
|
+
// The remaining fields are only filled in for some shells (resolve program, stampable),
|
|
75
|
+
// but they're all declared here so every Shell instance shares one hidden class.
|
|
76
|
+
// NodeGroup's per-row code (its constructor, applyStamp, resolveStampSlots) reads these
|
|
77
|
+
// off whichever shell it's given, and a single shape keeps those loads monomorphic.
|
|
78
|
+
|
|
79
|
+
/** @type {?string} The Template close key, cached here by the NodeGroup constructor so
|
|
80
|
+
* each new template row skips a WeakMap lookup. See Template.getCloseKey(). */
|
|
81
|
+
closeKey;
|
|
82
|
+
|
|
83
|
+
/** @type {?int[]} The resolve program: flat [parentSlot, childIndex] pairs in dependency
|
|
84
|
+
* order; pair i fills slot i+1, slot 0 being the fragment. Built by buildResolveProgram();
|
|
85
|
+
* undefined for shells with components. */
|
|
86
|
+
resolveOps;
|
|
87
|
+
|
|
88
|
+
/** @type {?Node[]} Reusable scratch array for resolved nodes; safe because resolution
|
|
89
|
+
* never re-enters. */
|
|
90
|
+
resolveSlots;
|
|
91
|
+
|
|
92
|
+
// The stamp program, set only when stampable is true:
|
|
93
|
+
|
|
94
|
+
/** @type {?int[]} Indexes of PathToNodes paths, checked for primitive exprs before stamping. */
|
|
95
|
+
nodesPathIdx;
|
|
96
|
+
|
|
97
|
+
/** @type {?Path[]} One shared stamper per path; nodeMarker/parentNg are set per use. */
|
|
98
|
+
stampPaths;
|
|
99
|
+
|
|
100
|
+
/** @type {?Uint8Array} Opcode per path; see the stamp-program comment in the constructor. */
|
|
101
|
+
stampOp;
|
|
102
|
+
|
|
103
|
+
/** @type {?Uint16Array} paths[i].markerSlot, in a flat array so the hot loop
|
|
104
|
+
* doesn't load the Path object to find its slot. */
|
|
105
|
+
stampSlot;
|
|
106
|
+
|
|
107
|
+
/** @type {?Path[]} Per-path extra the stamp program needs: the event stamper for op 3
|
|
108
|
+
* (it carries delegatedKey and eventName), the attribute name for op 4, null otherwise. */
|
|
109
|
+
stampAux;
|
|
110
|
+
|
|
111
|
+
/** @type {?string[]} The delegatable event names this shell binds, so a loop can register
|
|
112
|
+
* their dispatchers once for the whole run of rows instead of testing every bound node. */
|
|
113
|
+
stampEventNames;
|
|
114
|
+
|
|
115
|
+
/** @type {?Uint8Array} Per-path flags the in-place rewrite loop needs, so it reads one byte
|
|
116
|
+
* from a flat array instead of two properties from a Path object it otherwise wouldn't
|
|
117
|
+
* touch. Bit 1 = the path binds a live HTML property, bit 2 = it's a whole-parent child. */
|
|
118
|
+
stampFlags;
|
|
119
|
+
|
|
63
120
|
/**
|
|
64
121
|
* Create the nodes but without filling in the expressions.
|
|
65
122
|
* This is useful because the expression-less nodes created by a template can be cached.
|
|
@@ -69,13 +126,13 @@ export default class Shell {
|
|
|
69
126
|
if (!html)
|
|
70
127
|
return;
|
|
71
128
|
|
|
72
|
-
//#
|
|
129
|
+
//#IFDEBUG
|
|
73
130
|
this._html = html.join('');
|
|
74
131
|
//#ENDIF
|
|
75
132
|
|
|
76
133
|
// If no html tags or entities, just create a text node.
|
|
77
134
|
if (html.length === 1 && !html[0].match(/[<&]/)) {
|
|
78
|
-
this.
|
|
135
|
+
this.docFrag = Globals.doc.createTextNode(html[0]);
|
|
79
136
|
return;
|
|
80
137
|
}
|
|
81
138
|
|
|
@@ -93,29 +150,29 @@ export default class Shell {
|
|
|
93
150
|
let frag = Globals.doc.createDocumentFragment();
|
|
94
151
|
while (svgEl.firstChild)
|
|
95
152
|
frag.append(svgEl.firstChild);
|
|
96
|
-
this.
|
|
153
|
+
this.docFrag = frag;
|
|
97
154
|
}
|
|
98
155
|
else {
|
|
99
156
|
template.innerHTML = htmlWithPlaceholders;
|
|
100
|
-
this.
|
|
157
|
+
this.docFrag = template.content;
|
|
101
158
|
}
|
|
102
159
|
}
|
|
103
160
|
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
161
|
template.content.append(Globals.doc.createTextNode(''))
|
|
105
|
-
this.
|
|
162
|
+
this.docFrag = template.content;
|
|
106
163
|
}
|
|
107
164
|
|
|
108
165
|
// 1b. Remove whitespace-only text nodes inside table-structure elements.
|
|
109
166
|
// The parser foster-parents non-whitespace text out of tables, and whitespace-only
|
|
110
167
|
// text between cells/rows is never rendered, so removing it is invisible.
|
|
111
168
|
// Smaller fragments make cloning, path resolution, and insertion faster.
|
|
112
|
-
stripTableWhitespace(this.
|
|
169
|
+
stripTableWhitespace(this.docFrag);
|
|
113
170
|
|
|
114
171
|
// 2. Find placeholders
|
|
115
172
|
let node;
|
|
116
173
|
let toRemove = [];
|
|
117
174
|
let placeholdersUsed = 0;
|
|
118
|
-
const walker = Globals.doc.createTreeWalker(this.
|
|
175
|
+
const walker = Globals.doc.createTreeWalker(this.docFrag, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
119
176
|
while (node = walker.nextNode()) {
|
|
120
177
|
|
|
121
178
|
// Remove previous elements after each iteration, so paths will still be calculated correctly.
|
|
@@ -133,13 +190,20 @@ export default class Shell {
|
|
|
133
190
|
// The reserved key attribute identifies this template within a keyed list.
|
|
134
191
|
// It's consumed here and never written to the DOM or passed to components.
|
|
135
192
|
if (attr.name === 'key') {
|
|
193
|
+
|
|
194
|
+
// These three are template-authoring mistakes, and every one of them fails SILENTLY if
|
|
195
|
+
// it isn't caught: the reconciler would key rows on a garbage value and reuse the wrong
|
|
196
|
+
// DOM, with nothing reported. So they ship, unlike the assertions elsewhere in this
|
|
197
|
+
// file. The cost is one regex split per unique template \u2014 never per render, never per
|
|
198
|
+
// row \u2014 which is why they are affordable to keep.
|
|
136
199
|
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
137
200
|
if (parts.length !== 2 || parts[0] !== '' || parts[1] !== '')
|
|
138
|
-
throw new Error(`
|
|
139
|
-
if (node.parentNode !== this.
|
|
140
|
-
throw new Error(`
|
|
201
|
+
throw new Error(`Solarite: key must be one whole expression.`);
|
|
202
|
+
if (node.parentNode !== this.docFrag)
|
|
203
|
+
throw new Error(`Solarite: key must be on a top-level element.`);
|
|
141
204
|
if (this.keyIndex >= 0)
|
|
142
|
-
throw new Error(`
|
|
205
|
+
throw new Error(`Solarite: duplicate key attribute.`);
|
|
206
|
+
|
|
143
207
|
this.keyIndex = attr.value.charCodeAt(0) - attribPlaceholder;
|
|
144
208
|
|
|
145
209
|
let path = new PathToKey(null, node);
|
|
@@ -184,19 +248,33 @@ export default class Shell {
|
|
|
184
248
|
}
|
|
185
249
|
|
|
186
250
|
placeholdersUsed += parts.length - 1;
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
251
|
+
// An attribute whose whole value is one expression is removed from the shell:
|
|
252
|
+
// its stamped value is always the empty string, so every clone would carry a
|
|
253
|
+
// useless empty attribute that costs storage on creation and a slot in the
|
|
254
|
+
// element's attribute list forever, and apply() writes the real value anyway
|
|
255
|
+
// (a missing attribute reads back as '', so an empty expression still writes
|
|
256
|
+
// nothing). Event attributes must be removed for the same reason plus a
|
|
257
|
+
// stricter one: an empty onclick="" violates a strict CSP when the event fires.
|
|
258
|
+
// In svgMode, setting typed SVG attributes (viewBox, r, etc.) with the
|
|
259
|
+
// placeholders stripped out makes the browser log parse errors, both here and
|
|
260
|
+
// when the fragment is cloned, so those are removed whether or not they're whole.
|
|
261
|
+
if (svgMode || !nonEmptyParts)
|
|
193
262
|
node.removeAttribute(attr.name);
|
|
194
|
-
|
|
263
|
+
|
|
264
|
+
// setAttribute throws only when the template author wrote a name the browser
|
|
265
|
+
// refuses, such as one holding a space or a quote. That name comes from a tagged
|
|
266
|
+
// template literal's static text, so it is a typo that surfaces the first time the
|
|
267
|
+
// template renders and can never appear later or for only some users. Development
|
|
268
|
+
// therefore wraps the call to rethrow with the attribute name and the tag included,
|
|
269
|
+
// because the browser's own DOMException names neither and leaves the author
|
|
270
|
+
// hunting. Production ships the bare call and lets that DOMException through: the
|
|
271
|
+
// friendlier wording is only worth its bytes to whoever can still fix the template.
|
|
272
|
+
else /*#IFDEBUG*/try {/*#ENDIF*/
|
|
195
273
|
node.setAttribute(attr.name, parts.join(''));
|
|
196
|
-
}
|
|
274
|
+
/*#IFDEBUG*/}
|
|
197
275
|
catch (e) {
|
|
198
276
|
throw new Error(`Error setting attribute "${attr.name}" on node <${node.tagName}>: ${e.message}`);
|
|
199
|
-
}
|
|
277
|
+
}/*#ENDIF*/
|
|
200
278
|
}
|
|
201
279
|
}
|
|
202
280
|
}
|
|
@@ -218,7 +296,7 @@ export default class Shell {
|
|
|
218
296
|
else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
|
|
219
297
|
|
|
220
298
|
if (node?.parentNode?.closest && node?.parentNode?.closest('[contenteditable]'))
|
|
221
|
-
throw new Error(`
|
|
299
|
+
throw new Error(`Solarite: no \${...} inside contenteditable; use value="\${...}".`);
|
|
222
300
|
|
|
223
301
|
let parent = node.parentNode;
|
|
224
302
|
|
|
@@ -242,7 +320,7 @@ export default class Shell {
|
|
|
242
320
|
nodeBefore = Globals.doc.createComment('Path:'+this.paths.length);
|
|
243
321
|
node.parentNode.insertBefore(nodeBefore, node)
|
|
244
322
|
}
|
|
245
|
-
/*#
|
|
323
|
+
/*#IFDEBUG*/assert(nodeBefore);/*#ENDIF*/
|
|
246
324
|
|
|
247
325
|
// Get the next node.
|
|
248
326
|
let nodeMarker;
|
|
@@ -257,7 +335,7 @@ export default class Shell {
|
|
|
257
335
|
nodeMarker = node;
|
|
258
336
|
nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
|
|
259
337
|
}
|
|
260
|
-
/*#
|
|
338
|
+
/*#IFDEBUG*/assert(nodeMarker);/*#ENDIF*/
|
|
261
339
|
|
|
262
340
|
let path = new PathToNodes(nodeBefore, nodeMarker);
|
|
263
341
|
this.paths.push(path);
|
|
@@ -265,11 +343,6 @@ export default class Shell {
|
|
|
265
343
|
}
|
|
266
344
|
}
|
|
267
345
|
|
|
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
346
|
// Sometimes users will comment out a block of html code that has expressions.
|
|
274
347
|
// Here we look for expressions in comments.
|
|
275
348
|
// We don't actually update them dynamically, but we still add paths for them.
|
|
@@ -283,29 +356,39 @@ export default class Shell {
|
|
|
283
356
|
}
|
|
284
357
|
}
|
|
285
358
|
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
359
|
+
// A few elements have raw-text bodies, which the html parser reads as literal characters
|
|
360
|
+
// rather than as markup. A comment placeholder written inside one therefore never becomes
|
|
361
|
+
// a comment node; it arrives here as ordinary text. A textarea can't support expressions
|
|
362
|
+
// in its body at all, while script and style can, by splitting their text around each
|
|
363
|
+
// placeholder so that every expression gets a text node of its own to write into.
|
|
364
|
+
else if (node.nodeType === 3) { // Node.TEXT_NODE
|
|
365
|
+
let parentName = node.parentNode?.nodeName;
|
|
366
|
+
|
|
367
|
+
if (parentName === 'TEXTAREA' && node.textContent.includes(commentPlaceholder))
|
|
368
|
+
throw new Error(`Solarite: no \${...} inside textarea; use value="\${...}".`);
|
|
369
|
+
|
|
370
|
+
else if (parentName === 'SCRIPT' || parentName === 'STYLE') {
|
|
371
|
+
let parts = node.textContent.split(commentPlaceholder);
|
|
372
|
+
if (parts.length > 1) {
|
|
373
|
+
|
|
374
|
+
// Every part is inserted before the original node, in order, so from the second
|
|
375
|
+
// part onward the text node made on the previous iteration is already sitting
|
|
376
|
+
// immediately before this one and serves as the new path's nodeBefore.
|
|
377
|
+
for (let i = 0; i<parts.length; i++) {
|
|
378
|
+
let current = Globals.doc.createTextNode(parts[i]);
|
|
379
|
+
node.parentNode.insertBefore(current, node);
|
|
380
|
+
if (i > 0) {
|
|
381
|
+
let path = new PathToNodes(current.previousSibling, current);
|
|
382
|
+
this.paths.push(path);
|
|
383
|
+
placeholdersUsed ++;
|
|
384
|
+
|
|
385
|
+
/*#IFDEBUG*/path.verify();/*#ENDIF*/
|
|
386
|
+
}
|
|
387
|
+
}
|
|
303
388
|
|
|
304
|
-
|
|
389
|
+
// Removing it here will mess up the treeWalker.
|
|
390
|
+
toRemove.push(node);
|
|
305
391
|
}
|
|
306
|
-
|
|
307
|
-
// Removing them here will mess up the treeWalker.
|
|
308
|
-
toRemove.push(node);
|
|
309
392
|
}
|
|
310
393
|
}
|
|
311
394
|
}
|
|
@@ -314,31 +397,37 @@ export default class Shell {
|
|
|
314
397
|
// Less than or equal because there can be one path to multiple expressions
|
|
315
398
|
// if those expressions are in the same attribute value.
|
|
316
399
|
if (placeholdersUsed !== html.length-1)
|
|
317
|
-
throw new Error(`
|
|
400
|
+
throw new Error(`Solarite: bad html or duplicate attribute: ${html.join('${...}')}`);
|
|
318
401
|
|
|
319
402
|
for (let path of this.paths) {
|
|
320
|
-
|
|
321
|
-
|
|
403
|
+
// -1 when the path has no nodeBefore. Assigned unconditionally so every shell path
|
|
404
|
+
// of a given class takes the same property-addition order and shares one hidden class.
|
|
405
|
+
path.nodeBeforeIndex = path.nodeBefore
|
|
406
|
+
? Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore)
|
|
407
|
+
: -1;
|
|
322
408
|
|
|
323
409
|
// Must be calculated after we remove the toRemove nodes:
|
|
324
410
|
path.nodeMarkerPath = Path.get(path.nodeMarker)
|
|
325
|
-
|
|
326
|
-
|
|
327
411
|
}
|
|
328
412
|
|
|
329
413
|
this.findEmbeds();
|
|
330
|
-
this.buildResolveProgram();
|
|
331
414
|
|
|
415
|
+
// This scan must run before buildResolveProgram(), which skips shells with components
|
|
416
|
+
// and reads hasComponentPaths rather than walking the paths a second time.
|
|
332
417
|
this.pathsSingleExpr = true;
|
|
333
418
|
for (let path of this.paths) {
|
|
334
419
|
if (path instanceof PathToComponent) {
|
|
335
420
|
this.hasComponentPaths = true;
|
|
336
421
|
this.pathsSingleExpr = false;
|
|
337
|
-
break; // Both facts are now decided.
|
|
338
422
|
}
|
|
339
|
-
if (path.getExpressionCount() !== 1)
|
|
340
|
-
this.pathsSingleExpr = false;
|
|
423
|
+
else if (path.getExpressionCount() !== 1)
|
|
424
|
+
this.pathsSingleExpr = false;
|
|
425
|
+
if (path.isHtmlProperty) // needs the full scan — no early break
|
|
426
|
+
this.hasLivePropPaths = true;
|
|
341
427
|
}
|
|
428
|
+
this.needsRefresh = this.hasComponentPaths || (this.hasLivePropPaths && this.pathsSingleExpr);
|
|
429
|
+
|
|
430
|
+
this.buildResolveProgram();
|
|
342
431
|
|
|
343
432
|
// Stampable shells create NodeGroups without allocating any Path objects:
|
|
344
433
|
// NodeGroup.applyStamp() writes expressions through these shared stamper paths,
|
|
@@ -363,17 +452,50 @@ export default class Shell {
|
|
|
363
452
|
}
|
|
364
453
|
if (ok) {
|
|
365
454
|
this.stampable = true;
|
|
366
|
-
|
|
367
|
-
/** @type {int[]} Indexes of PathToNodes paths, checked for primitive exprs before stamping. */
|
|
368
455
|
this.nodesPathIdx = nodesIdx;
|
|
369
|
-
|
|
370
|
-
/** @type {Path[]} One shared stamper per path; nodeMarker/parentNg are set per use. */
|
|
371
456
|
this.stampPaths = this.paths.map(p => p.cloneWithNodes(null, p.nodeMarker));
|
|
372
457
|
|
|
458
|
+
// Compiled stamp program: one opcode per path lets applyStamp() write a fresh
|
|
459
|
+
// row through a flat branch chain instead of dispatching applySingle() per path.
|
|
460
|
+
// 0 = generic (shared stamper fallback), 1 = list key (no DOM), 2 = wholeParent
|
|
461
|
+
// child text, 3 = delegatable single-expression event (written as node expandos
|
|
462
|
+
// when the root delegates, the default).
|
|
463
|
+
let n = this.paths.length;
|
|
464
|
+
this.stampOp = new Uint8Array(n);
|
|
465
|
+
this.stampSlot = new Uint16Array(n);
|
|
466
|
+
this.stampAux = new Array(n).fill(null);
|
|
467
|
+
this.stampFlags = new Uint8Array(n);
|
|
468
|
+
|
|
469
|
+
let eventNames = null;
|
|
470
|
+
for (let i=0; i<n; i++) {
|
|
471
|
+
let p = this.paths[i], sp = this.stampPaths[i];
|
|
472
|
+
this.stampSlot[i] = p.markerSlot;
|
|
473
|
+
this.stampFlags[i] = (sp.isHtmlProperty ? 1 : 0) | (sp.wholeParent ? 2 : 0);
|
|
474
|
+
if (p instanceof PathToKey)
|
|
475
|
+
this.stampOp[i] = 1;
|
|
476
|
+
else if (sp.wholeParent)
|
|
477
|
+
this.stampOp[i] = 2;
|
|
478
|
+
else if (sp instanceof PathToEvent && sp.delegatedKey !== undefined && !sp.attrValue) {
|
|
479
|
+
this.stampOp[i] = 3;
|
|
480
|
+
this.stampAux[i] = sp;
|
|
481
|
+
(eventNames ??= []).push(sp.eventName);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// A plain attribute holding one whole expression. The shell no longer carries
|
|
485
|
+
// the attribute at all (see the placeholder handling above), so on a freshly
|
|
486
|
+
// cloned row the value is known to be absent and a string can be written
|
|
487
|
+
// without first reading back what's there.
|
|
488
|
+
else if (sp instanceof PathToAttribValue && !sp.attrValue && !sp.isHtmlProperty
|
|
489
|
+
&& !sp.isComponentAttrib) {
|
|
490
|
+
this.stampOp[i] = 4;
|
|
491
|
+
this.stampAux[i] = sp.attribName;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
this.stampEventNames = eventNames;
|
|
373
495
|
}
|
|
374
496
|
}
|
|
375
497
|
|
|
376
|
-
/*#
|
|
498
|
+
/*#IFDEBUG*/this.verify();/*#ENDIF*/
|
|
377
499
|
}
|
|
378
500
|
|
|
379
501
|
/**
|
|
@@ -384,42 +506,64 @@ export default class Shell {
|
|
|
384
506
|
* @param htmlChunks {string[]}
|
|
385
507
|
* @returns {string} Html with the placeholders in place. */
|
|
386
508
|
static addPlaceholders(htmlChunks) {
|
|
387
|
-
let result =
|
|
509
|
+
let result = '';
|
|
510
|
+
|
|
511
|
+
// Where the tokenizer is as it walks the chunks. An expression can sit in the middle of an attribute
|
|
512
|
+
// value, so both of these have to survive from one chunk to the next. Nothing else has to: an
|
|
513
|
+
// expression anywhere inside a tag gets the same attribute placeholder, so the machine only has to
|
|
514
|
+
// know whether it is inside a tag at all, and whether a quoted value is currently open.
|
|
515
|
+
let inTag = false; // True from the '<' that opens a tag or comment through the '>' that closes it.
|
|
516
|
+
let quote = null; // The quote character that opened the attribute value we're inside of: null, '"', or "'".
|
|
388
517
|
|
|
389
|
-
let htmlParser = new HtmlParser(); // Reset the context.
|
|
390
518
|
for (let i = 0; i < htmlChunks.length; i++) {
|
|
391
|
-
let
|
|
519
|
+
let html = htmlChunks[i];
|
|
392
520
|
|
|
393
521
|
// Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
|
|
394
|
-
let lastIndex = 0;
|
|
395
|
-
let
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
|
|
522
|
+
let lastIndex = 0; // Start of the run of this chunk not yet copied into result.
|
|
523
|
+
for (let j = 0; j < html.length; j++) {
|
|
524
|
+
const char = html[j];
|
|
525
|
+
|
|
526
|
+
if (!inTag) {
|
|
527
|
+
if (char === '<' && html[j + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
|
|
528
|
+
inTag = true;
|
|
529
|
+
|
|
530
|
+
// A component suffix can only ever be added right here, at the '<' that opens the tag, so
|
|
531
|
+
// the name is matched on the spot with a sticky regex rather than collected into a buffer
|
|
532
|
+
// and matched later. The greedy tag-name class can't run past the name, because every
|
|
533
|
+
// character that can follow a tag name is outside it.
|
|
534
|
+
isWebComponentTagName.lastIndex = j;
|
|
535
|
+
let match = isWebComponentTagName.exec(html);
|
|
536
|
+
if (match) {
|
|
537
|
+
let end = j + match[0].length;
|
|
538
|
+
result += html.slice(lastIndex, end) + '-SOLARITE-PLACEHOLDER';
|
|
539
|
+
lastIndex = end;
|
|
540
|
+
}
|
|
406
541
|
}
|
|
542
|
+
}
|
|
407
543
|
|
|
408
|
-
|
|
544
|
+
// Inside a tag, only two characters end anything: the quote that closes the value we're in, or,
|
|
545
|
+
// when we're not in one, the '>' that closes the tag. Attribute names, '=', unquoted values and
|
|
546
|
+
// whitespace all need no handling at all.
|
|
547
|
+
else if (quote) {
|
|
548
|
+
if (char === quote)
|
|
549
|
+
quote = null;
|
|
409
550
|
}
|
|
410
|
-
|
|
411
|
-
|
|
551
|
+
else if (char === '"' || char === "'")
|
|
552
|
+
quote = char;
|
|
553
|
+
else if (char === '>')
|
|
554
|
+
inTag = false;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
result += html.slice(lastIndex);
|
|
412
558
|
|
|
413
559
|
// Insert placeholders
|
|
414
|
-
if (i < htmlChunks.length - 1)
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
result.push(String.fromCharCode(attribPlaceholder + i));
|
|
419
|
-
}
|
|
560
|
+
if (i < htmlChunks.length - 1)
|
|
561
|
+
result += inTag
|
|
562
|
+
? String.fromCharCode(attribPlaceholder + i)
|
|
563
|
+
: commentPlaceholder; // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
420
564
|
}
|
|
421
565
|
|
|
422
|
-
return result
|
|
566
|
+
return result;
|
|
423
567
|
}
|
|
424
568
|
|
|
425
569
|
/**
|
|
@@ -431,21 +575,18 @@ export default class Shell {
|
|
|
431
575
|
* this.ids
|
|
432
576
|
* this.staticComponents */
|
|
433
577
|
findEmbeds() {
|
|
434
|
-
this.scripts = Array.prototype.map.call(this.
|
|
578
|
+
this.scripts = Array.prototype.map.call(this.docFrag.querySelectorAll('script'), el => Path.get(el))
|
|
435
579
|
|
|
436
580
|
// TODO: only find styles that have Paths in them?
|
|
437
|
-
this.styles = Array.prototype.map.call(this.
|
|
581
|
+
this.styles = Array.prototype.map.call(this.docFrag.querySelectorAll('style'), el => Path.get(el))
|
|
438
582
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
this.ids = Array.prototype.map.call(idEls, el => Path.get(el))
|
|
583
|
+
// An id that would clobber a built-in element property is reported by Util.bindId(), which
|
|
584
|
+
// asks the real component object, with `in`, at the moment the binding happens. The check
|
|
585
|
+
// that used to stand here asked Globals.div.hasOwnProperty(id) instead, and a freshly
|
|
586
|
+
// created element has no own properties at all — every DOM property an element exposes
|
|
587
|
+
// lives on its interface prototype — so that test could never be true and the error it
|
|
588
|
+
// guarded was never reachable.
|
|
589
|
+
this.ids = Array.prototype.map.call(this.docFrag.querySelectorAll('[id],[data-id]'), el => Path.get(el))
|
|
449
590
|
|
|
450
591
|
this.hasEmbeds = this.ids.length > 0 || this.styles.length > 0 || this.scripts.length > 0;
|
|
451
592
|
}
|
|
@@ -456,25 +597,37 @@ export default class Shell {
|
|
|
456
597
|
* Replaces per-path root-to-node walks in the hot NodeGroup creation path.
|
|
457
598
|
* Skipped for shells with components, whose clone() has special attribPaths behavior. */
|
|
458
599
|
buildResolveProgram() {
|
|
459
|
-
|
|
460
|
-
for (let path of this.paths)
|
|
461
|
-
if (path instanceof PathToComponent) {
|
|
462
|
-
hasComponents = true;
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
if (hasComponents || !this.paths.length)
|
|
600
|
+
if (this.hasComponentPaths || !this.paths.length)
|
|
466
601
|
return;
|
|
467
602
|
|
|
468
603
|
let ops = [];
|
|
469
604
|
let slotOf = new Map();
|
|
470
|
-
let frag = this.
|
|
605
|
+
let frag = this.docFrag;
|
|
471
606
|
let nextSlot = 1;
|
|
472
607
|
let getSlot = node => {
|
|
473
608
|
if (node === frag)
|
|
474
609
|
return 0;
|
|
475
610
|
let s = slotOf.get(node);
|
|
476
611
|
if (s === undefined) {
|
|
477
|
-
|
|
612
|
+
// Two ways to reach a node, costing one pointer step each: walk forward from an
|
|
613
|
+
// already-resolved earlier sibling, or take the parent's firstChild and walk
|
|
614
|
+
// forward. Sibling steps win whenever they're no more numerous, and they can
|
|
615
|
+
// also spare the parent a slot of its own — in a row of cells, resolving each
|
|
616
|
+
// <td> from the previous one is one step instead of firstChild plus its index.
|
|
617
|
+
let d = 0, from = -1;
|
|
618
|
+
for (let sib = node.previousSibling; sib; sib = sib.previousSibling) {
|
|
619
|
+
d++;
|
|
620
|
+
let ss = slotOf.get(sib);
|
|
621
|
+
if (ss !== undefined) {
|
|
622
|
+
from = ss;
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
let index = Array.prototype.indexOf.call(node.parentNode.childNodes, node);
|
|
627
|
+
if (from >= 0 && d <= index + 1)
|
|
628
|
+
ops.push(from, -d); // A negative step count means "walk nextSibling from that slot".
|
|
629
|
+
else
|
|
630
|
+
ops.push(getSlot(node.parentNode), index);
|
|
478
631
|
s = nextSlot++;
|
|
479
632
|
slotOf.set(node, s);
|
|
480
633
|
}
|
|
@@ -485,10 +638,7 @@ export default class Shell {
|
|
|
485
638
|
path.beforeSlot = path.nodeBefore ? getSlot(path.nodeBefore) : -1;
|
|
486
639
|
}
|
|
487
640
|
|
|
488
|
-
/** @type {?int[]} Flat [parentSlot, childIndex] pairs; pair i fills slot i+1. */
|
|
489
641
|
this.resolveOps = ops;
|
|
490
|
-
|
|
491
|
-
/** @type {Node[]} Reusable scratch array for resolved nodes; safe because resolution never re-enters. */
|
|
492
642
|
this.resolveSlots = new Array(nextSlot);
|
|
493
643
|
|
|
494
644
|
// A lone root element means slot 1 is always that element (the first op pair is [0, 0]),
|
|
@@ -523,15 +673,15 @@ export default class Shell {
|
|
|
523
673
|
lastSvgMode = svgMode;
|
|
524
674
|
lastShell = result;
|
|
525
675
|
|
|
526
|
-
/*#
|
|
676
|
+
/*#IFDEBUG*/result.verify();/*#ENDIF*/
|
|
527
677
|
return result;
|
|
528
678
|
}
|
|
529
679
|
|
|
530
|
-
//#
|
|
680
|
+
//#IFDEBUG
|
|
531
681
|
// For debugging only:
|
|
532
682
|
verify() {
|
|
533
683
|
for (let path of this.paths) {
|
|
534
|
-
assert(this.
|
|
684
|
+
assert(this.docFrag.contains(path.getParentNode()))
|
|
535
685
|
path.verify();
|
|
536
686
|
}
|
|
537
687
|
}
|
|
@@ -541,6 +691,15 @@ export default class Shell {
|
|
|
541
691
|
|
|
542
692
|
const commentPlaceholder = `<!--!✨!-->`;
|
|
543
693
|
|
|
694
|
+
// A tag name with a dash in the middle, which is what makes an element a web component. addPlaceholders()
|
|
695
|
+
// tests this at each '<' that opens a tag, and a match gets -solarite-placeholder appended to its tag name.
|
|
696
|
+
// That way we can gather a component's constructor arguments and its children before we call its constructor;
|
|
697
|
+
// later PathToComponent.applyAll() replaces the placeholder tag with the real component. The suffix is written in
|
|
698
|
+
// caps wherever it appears, so that the several copies of it in this project compress well. It's sticky rather
|
|
699
|
+
// than anchored so it can be tested at an offset within the chunk instead of against a sliced-out token.
|
|
700
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
701
|
+
const isWebComponentTagName = /<\/?[a-z][a-z0-9]*-[a-z0-9-]+/iy;
|
|
702
|
+
|
|
544
703
|
// Elements whose whitespace-only text children are never rendered.
|
|
545
704
|
const tableTags = ['TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR'];
|
|
546
705
|
|