solarite 0.5.2 → 0.7.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Solarite-debug.js +2823 -1856
  3. package/dist/Solarite.js +2779 -1824
  4. package/dist/Solarite.min.js +2 -4
  5. package/package.json +16 -3
  6. package/readme.md +58 -11
  7. package/src/Globals.js +54 -72
  8. package/src/HtmlParser.js +90 -90
  9. package/src/MultiValueMap.js +57 -105
  10. package/src/NodeGroup.js +624 -470
  11. package/src/Path.js +224 -211
  12. package/src/PathToAttribValue.js +401 -261
  13. package/src/PathToAttribs.js +113 -80
  14. package/src/PathToComment.js +7 -7
  15. package/src/PathToComponent.js +183 -188
  16. package/src/PathToEvent.js +76 -64
  17. package/src/PathToKey.js +19 -0
  18. package/src/PathToNodes.js +1053 -566
  19. package/src/RootNodeGroup.js +120 -8
  20. package/src/Shell.js +570 -348
  21. package/src/Solarite.d.ts +134 -113
  22. package/src/Solarite.js +243 -286
  23. package/src/Template.js +195 -274
  24. package/src/Util.js +353 -351
  25. package/src/assert.js +10 -10
  26. package/src/assignAttributes.js +63 -0
  27. package/src/delve.js +55 -43
  28. package/src/h.js +220 -138
  29. package/src/jsx-dev-runtime.d.ts +1 -0
  30. package/src/jsx-dev-runtime.js +5 -0
  31. package/src/jsx-runtime.d.ts +21 -0
  32. package/src/jsx-runtime.js +84 -0
  33. package/src/jsx.js +194 -0
  34. package/src/toEl.js +77 -82
  35. package/dist/udomdiff-license.txt +0 -18
  36. package/src/getArg.js +0 -137
  37. package/src/hash.js +0 -89
  38. package/src/udomdiff.js +0 -176
  39. package/src/unused/FastLookupArray.js +0 -54
  40. package/src/unused/Hashes.js +0 -339
  41. package/src/unused/InUse.test.js +0 -92
  42. package/src/unused/InUseMap.js +0 -98
  43. package/src/unused/LinkedList.js +0 -117
  44. package/src/unused/LinkedList.test.js +0 -115
  45. package/src/unused/Misc.js +0 -13
  46. package/src/unused/Perf.js +0 -47
  47. package/src/unused/TrackedArray.js +0 -54
  48. package/src/unused/WeakArray.js +0 -33
  49. package/src/watch.js +0 -543
package/src/NodeGroup.js CHANGED
@@ -1,470 +1,624 @@
1
- import assert from "./assert.js";
2
- import Util, {flattenAndIndent, nodeToArrayTree, setIndent} from "./Util.js";
3
- import Shell from "./Shell.js";
4
- import RootNodeGroup from './RootNodeGroup.js';
5
- import Path from "./Path.js";
6
- import Globals from './Globals.js';
7
- import PathToComponent from "./PathToComponent.js";
8
- import PathToNodes from "./PathToNodes.js";
9
-
10
- /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
11
-
12
- /**
13
- * A group of Nodes instantiated from a Shell, with Expr's filled in.
14
- *
15
- * The range is determined by startNode and nodeMarker.
16
- * startNode - never null. An empty text node is created before the first path if none exists.
17
- * nodeMarker - null if this Nodegroup is at the end of its parents' nodes.*/
18
- export default class NodeGroup {
19
-
20
- /**
21
- * @Type {RootNodeGroup} */
22
- rootNg;
23
-
24
- /** @type {Path} */
25
- parentPath;
26
-
27
- /** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
28
- startNode;
29
-
30
- /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
31
- * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
32
- * TODO: But sometimes startNode and endNode point to the same node. Document this inconsistency. */
33
- endNode;
34
-
35
- /** @type {Path[]} */
36
- paths = [];
37
-
38
- /** @type {string} Key that matches the template and the expressions. */
39
- exactKey;
40
-
41
- /** @type {string} Key that only matches the template. */
42
- closeKey;
43
-
44
- /**
45
- * @internal
46
- * @type {Node[]} Cached result of getNodes() used only for improving performance.*/
47
- nodesCache;
48
-
49
- /**
50
- * A map between <style> Elements and their text content.
51
- * This lets NodeGroup.updateStyles() see when the style text has changed.
52
- * @type {?Map<HTMLStyleElement, string>} */
53
- styles;
54
-
55
- /** @type {Template} */
56
- template;
57
-
58
- /**
59
- * Root node at the top of the hierarchy.
60
- * Should be moved to RootNodeGroup
61
- * @type {HTMLElement} */
62
- root;
63
-
64
-
65
- /**
66
- * Create an "instantiated" NodeGroup from a Template and add it to an element.
67
- * Don't call applyExprs() yet to apply expressions or instantiate components yet.
68
- * @param template {Template} Create it from the html strings and expressions in this template.
69
- * @param parentPath {?Path}
70
- * @param el {?HTMLElement} Optional, pre-existing htmlElement that will be the root.
71
- * @param options {?object} Only used for RootNodeGroup */
72
- constructor(template, parentPath=null, el=null, options=null) {
73
- this.rootNg = parentPath?.parentNg?.rootNg || this;
74
- this.parentPath = parentPath;
75
-
76
- /*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
77
- this.template = template;
78
- this.closeKey = template.getCloseKey();
79
-
80
- // If it's just a text node, skip a bunch of unnecessary steps.
81
- if (template.isText) {
82
- this.startNode = this.endNode = Globals.doc.createTextNode(template.html[0]);
83
- }
84
-
85
- else {
86
- // Get a cached version of the parsed and instantiated html, and Paths:
87
- const shell = Shell.get(template.html);
88
- const shellFragment = shell.fragment.cloneNode(true);
89
-
90
- if (shellFragment.nodeType === 11) { // DocumentFragment
91
- this.startNode = shellFragment.firstChild;
92
- this.endNode = shellFragment.lastChild;
93
- } else
94
- this.startNode = this.endNode = shellFragment;
95
-
96
-
97
- // Special setup for RootNodeGroup
98
- if (this instanceof RootNodeGroup) {
99
-
100
-
101
- let startingPathDepth = 0;
102
- this.options = options;
103
- if (shellFragment instanceof Text) {
104
- if (!el)
105
- throw new Error('Cannot create a standalone text node');
106
-
107
- this.root = el;
108
- if (shellFragment.nodeValue.length)
109
- this.root.append(shellFragment);
110
- }
111
-
112
- else {
113
- if (el) {
114
- this.root = el;
115
-
116
- // Save slot
117
- // 1. Globals.currentSlotChildren is set if this is called via PathToComponent.applyComponent() calls render()
118
- // 2. el.childNodes is set if render() is called manually for the first time.
119
- let slotChildren;
120
- if (Globals.currentSlotChildren || el.childNodes.length) {
121
- slotChildren = Globals.doc.createDocumentFragment();
122
- slotChildren.append(...(Globals.currentSlotChildren || el.childNodes));
123
- }
124
-
125
- // If el should replace the root node of the fragment.
126
- if (isReplaceEl(shellFragment, this.root.tagName)) {
127
- this.root.append(...shellFragment.children[0].childNodes);
128
-
129
- // Copy attributes
130
- for (let attrib of shellFragment.children[0].attributes)
131
- if (!this.root.hasAttribute(attrib.name))
132
- this.root.setAttribute(attrib.name, attrib.value);
133
-
134
- // Go one level deeper into all of shell's paths.
135
- startingPathDepth = 1;
136
- }
137
-
138
- else {
139
- let isEmpty = shellFragment.childNodes.length === 1 && shellFragment.childNodes[0].nodeType === 3 && shellFragment.childNodes[0].textContent === '';
140
- if (!isEmpty)
141
- this.root.append(...shellFragment.childNodes);
142
- }
143
-
144
-
145
- // Setup slot children (deprecated)
146
- if (slotChildren) {
147
- // Named slots
148
- for (let slot of el.querySelectorAll('slot[name]')) {
149
- let name = slot.getAttribute('name')
150
- if (name) {
151
- let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
152
- slot.append(...slotChildren2);
153
- }
154
- }
155
- // Unnamed slots
156
- let unamedSlot = el.querySelector('slot:not([name])')
157
- if (unamedSlot)
158
- unamedSlot.append(slotChildren);
159
- // No slots
160
- else
161
- el.append(slotChildren);
162
- }
163
- }
164
-
165
- // Instantiate as a standalone element.
166
- else {
167
- let onlyChild = getSingleEl(shellFragment);
168
- this.root = onlyChild || shellFragment; // We return the whole fragment when calling h() with a collection of nodes.
169
- if (onlyChild)
170
- startingPathDepth = 1;
171
- }
172
-
173
- // Exclude the path to ourself. Otherwise we get infinite recursion.
174
- // let paths = [...shell.paths];
175
- // if (paths[0] instanceof PathToComponent)
176
- // paths.shift();
177
-
178
- this.setPathsFromFragment(this.root, shell.paths, startingPathDepth);
179
- this.activateEmbeds(this.root, shell, startingPathDepth);
180
- }
181
- this.startNode = this.endNode = this.root;
182
-
183
- Globals.rootNodeGroups.set(this.root, this);
184
- } // end if RootNodeGroup
185
-
186
- else if (shell) {
187
- if (shell.paths.length) {
188
- this.setPathsFromFragment(shellFragment, shell.paths);
189
- }
190
-
191
- this.activateEmbeds(shellFragment, shell);
192
- }
193
- }
194
-
195
- //#IFDEV
196
- this.verify();
197
- //#ENDIF
198
- }
199
-
200
-
201
- /**
202
- * Use the paths to insert the given expressions.
203
- * Dispatches expression handling to other functions depending on the path type.
204
- * @param exprs {(*|*[]|function|Template)[]}
205
- * @param changed {boolean} If true, the expr's have changed since the last time thsi function was called.
206
- * @param includeNonComponents {boolean}
207
- * We still need to call PathToComponent.apply() even if changed=false so the user can handle the rendering. */
208
- applyExprs(exprs, changed=true, includeNonComponents=true) {
209
-
210
- /*#IFDEV*/
211
- this.verify();
212
- /*#ENDIF*/
213
-
214
- let paths = this.paths;
215
-
216
- // Things to consider:
217
- // 1. Paths consume a varying number of expressions.
218
- // An PathToAttribs may use multipe expressions. E.g. <div class="${1} ${2}">
219
- // While an PathToComponent uses zero.
220
- // 2. An PathToComponent references other Paths that set its attribute values.
221
- // 3. We apply them in reverse order so that a <select> box has its children created from an expression
222
- // before its instantiated and its value attribute is set via an expression.
223
- let exprIndex = exprs.length; // Update exprs at paths.
224
- let pathExprs = new Array(paths.length); // Store all the expressions that map to a single path. Only paths to attribute values can have more than one.
225
- for (let i = paths.length - 1, path; path = paths[i]; i--) {
226
- if (i===0 && path instanceof PathToComponent && path.nodeMarker === this.getRootNode())
227
- continue;
228
-
229
- // Get the expressions associated with this path.
230
- let exprCount = path.getExpressionCount();
231
- pathExprs[i] = exprs.slice(exprIndex-exprCount, exprIndex); // slice() probably doesn't allocate if the JS vm implements copy on write.
232
- exprIndex -= exprCount;
233
-
234
- // Component expressions don't have a corresponding user-provided expression.
235
- // They use expressions from the paths that provide their attributes.
236
- if (path instanceof PathToComponent) {
237
- let attribExprs = pathExprs.slice(i+1, i+1 + path.attribPaths.length); // +1 b/c we move forward from the component path.
238
- path.apply(attribExprs, true, changed);
239
- }
240
- else if (includeNonComponents)
241
- path.apply(pathExprs[i]);
242
- }
243
-
244
- // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
245
- // and the number of paths not matching.
246
- /*#IFDEV*/
247
- assert(exprIndex === 0);
248
- /*#ENDIF*/
249
-
250
-
251
- if (includeNonComponents) {
252
-
253
- // TODO: Only do this if we have Paths within styles?
254
- this.updateStyles();
255
-
256
- // Invalidate the nodes cache because we just changed it.
257
- this.nodesCache = null;
258
-
259
- }
260
-
261
- /*#IFDEV*/
262
- this.verify();
263
- /*#ENDIF*/
264
- }
265
-
266
- // TODO: Give it a better name.
267
- applyExprs2(exprs) {
268
-
269
- }
270
-
271
- /**
272
- * Get all the nodes inclusive between startNode and endNode.
273
- * TODO: when not using nodesCache, could this use less memory with yield?
274
- * But we'd need to save the reference to the next Node in case it's removed.
275
- * @return {(Node|HTMLElement)[]} */
276
- getNodes() {
277
- // applyExprs() invalidates this cache.
278
- let result = this.nodesCache;
279
- if (result) // This does speed up the partialUpdate benchmark by 10-15%.
280
- return result;
281
-
282
- result = [];
283
- let current = this.startNode
284
- let afterLast = this.endNode?.nextSibling
285
- while (current && current !== afterLast) {
286
- result.push(current)
287
- current = current.nextSibling
288
- }
289
-
290
- this.nodesCache = result;
291
- return result;
292
- }
293
-
294
- /**
295
- * Get the root element of the NodeGroup's RootNodeGroup.
296
- * @returns {HTMLElement|DocumentFragment} */
297
- getRootNode() {
298
- return this.rootNg.root;
299
- }
300
-
301
- /**
302
- * @returns {RootNodeGroup} */
303
- getRootNodeGroup() {
304
- return this.rootNg;
305
- }
306
-
307
- /**
308
- * Copy paths in fragment to this.paths.
309
- * @param fragment {DocumentFragment|HTMLElement}
310
- * @param paths
311
- * @param startingPathDepth {int} */
312
- setPathsFromFragment(fragment, paths, startingPathDepth=0) {
313
- let pathLength = paths.length; // For faster iteration
314
- this.paths.length = pathLength;
315
- for (let i=0; i<pathLength; i++) {
316
- let path = paths[i].clone(fragment, startingPathDepth)
317
- path.parentNg = this;
318
- this.paths[i] = path;
319
- }
320
- }
321
-
322
- updateStyles() {
323
- if (this.styles)
324
- for (let [style, oldText] of this.styles) {
325
- let newText = style.textContent;
326
- if (oldText !== newText)
327
- Util.bindStyles(style, this.getRootNodeGroup().root);
328
- }
329
- }
330
-
331
- /**
332
- * @param root {HTMLElement|DocumentFragment}
333
- * @param shell {Shell}
334
- * @param pathOffset {int} */
335
- activateEmbeds(root, shell, pathOffset=0) {
336
-
337
- let rootEl = this.rootNg.root;
338
- if (rootEl) {
339
- let options = this.rootNg.options;
340
-
341
- // ids
342
- if (options?.ids !== false) {
343
- for (let path of shell.ids) {
344
- if (pathOffset)
345
- path = path.slice(0, -pathOffset);
346
- let el = Path.resolve(root, path);
347
- Util.bindId(rootEl, el);
348
- }
349
- }
350
-
351
- // styles
352
- if (options?.styles !== false) {
353
- if (shell.styles.length)
354
- this.styles = new Map();
355
- for (let path of shell.styles) {
356
- if (pathOffset)
357
- path = path.slice(0, -pathOffset);
358
-
359
- /** @type {HTMLStyleElement} */
360
- let style = Path.resolve(root, path);
361
- if (rootEl.nodeType === 1) {
362
- Util.bindStyles(style, rootEl);
363
- this.styles.set(style, style.textContent);
364
- }
365
- }
366
-
367
- }
368
- // scripts
369
- if (options?.scripts !== false) {
370
- for (let path of shell.scripts) {
371
- if (pathOffset)
372
- path = path.slice(0, -pathOffset);
373
- let script = Path.resolve(root, path);
374
- eval(script.textContent)
375
- }
376
- }
377
- }
378
- }
379
-
380
- //#IFDEV
381
- getParentNode() {
382
- return this.startNode?.parentNode
383
- }
384
-
385
- get debug() {
386
- return [
387
- `parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
388
- 'nodes:',
389
- ...setIndent(this.getNodes().map(item => {
390
- if (item?.nodeType) {
391
-
392
- let tree = nodeToArrayTree(item, nextNode => {
393
-
394
- let path = this.paths.find(path=>(path instanceof PathToNodes) && path.getNodes().includes(nextNode));
395
- if (path)
396
- return [`Path.nodes:`]
397
-
398
- return [];
399
- })
400
-
401
- // TODO: How to indend nodes belonging to a path vs those that just occur after the path?
402
- return flattenAndIndent(tree)
403
- }
404
- else if (item instanceof Path)
405
- return setIndent(item.debug, 1)
406
- }).flat(), 1)
407
- ]
408
- }
409
-
410
- get debugNodes() { return this.getNodes() }
411
-
412
-
413
- get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
414
-
415
- verify() {
416
- if (!window.verify)
417
- return;
418
-
419
- assert(this.startNode)
420
- assert(this.endNode)
421
- //assert(this.startNode !== this.endNode) // This can be true.
422
- assert(this.startNode.parentNode === this.endNode.parentNode)
423
-
424
- // Only if connected:
425
- assert(!this.startNode.parentNode || this.startNode === this.endNode || this.startNode.compareDocumentPosition(this.endNode) === Node.DOCUMENT_POSITION_FOLLOWING)
426
-
427
- // if (this.parentPath)
428
- // assert(this.parentPath.nodeGroups.includes(this));
429
-
430
- for (let path of this.paths) {
431
- assert(path.parentNg === this)
432
-
433
- // Fails for detached NodeGroups.
434
- // NodeGroups get detached when their nodes are removed by udomdiff()
435
- let parentNode = this.getParentNode();
436
- if (parentNode)
437
- assert(this.getParentNode().contains(path.getParentNode()))
438
- path.verify();
439
- // TODO: Make sure path nodes are all within our own node range.
440
- }
441
- return true;
442
- }
443
- //#ENDIF
444
- }
445
-
446
-
447
-
448
-
449
- function getSingleEl(fragment) {
450
- let nonempty = [];
451
- for (let n of fragment.childNodes) {
452
- if (n.nodeType === 1 || n.nodeType === 3 && n.textContent.trim().length) {
453
- if (nonempty.length)
454
- return null;
455
- nonempty.push(n);
456
- }
457
- }
458
- return nonempty[0];
459
- }
460
-
461
- /**
462
- * Does the fragment have one child that's an element matching the tagname of el?
463
- * @param fragment {DocumentFragment}
464
- * @param tagName {string}
465
- * @returns {boolean} */
466
- function isReplaceEl(fragment, tagName) {
467
- return fragment.children.length===1
468
- && tagName.includes('-')
469
- && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === tagName;
470
- }
1
+ import assert from "./assert.js";
2
+ import Util, {flattenAndIndent, nodeToArrayTree, setIndent} from "./Util.js";
3
+ import {exprSame} from "./Template.js";
4
+ import Shell from "./Shell.js";
5
+ import Path from "./Path.js";
6
+ import Globals from './Globals.js';
7
+ import PathToComponent from "./PathToComponent.js";
8
+ import PathToNodes from "./PathToNodes.js";
9
+
10
+ /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
11
+
12
+ /**
13
+ * A group of Nodes instantiated from a Shell, with Expr's filled in.
14
+ *
15
+ * The range is determined by startNode and nodeMarker.
16
+ * startNode - never null. An empty text node is created before the first path if none exists.
17
+ * nodeMarker - null if this Nodegroup is at the end of its parents' nodes.*/
18
+ export default class NodeGroup {
19
+
20
+ /**
21
+ * @Type {RootNodeGroup} */
22
+ rootNg;
23
+
24
+ /** @type {Path} */
25
+ parentPath;
26
+
27
+ /** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
28
+ startNode;
29
+
30
+ /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
31
+ * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
32
+ * TODO: But sometimes startNode and endNode point to the same node. Document this inconsistency. */
33
+ endNode;
34
+
35
+ /** @type {?Path[]} Null for text NodeGroups; created by setPathsFromFragment(). */
36
+ paths = null;
37
+
38
+ /** @type {string} Key that only matches the template. */
39
+ closeKey;
40
+
41
+ /** @type {*} List key from the template's key=${} expression; written by PathToKey,
42
+ * matched by PathToNodes.applyKeyed(). Undefined for unkeyed NodeGroups. */
43
+ key;
44
+
45
+ /** @type {boolean} True if any of this NodeGroup's own paths is a PathToComponent. */
46
+ hasComponentPaths = false;
47
+
48
+ /** @type {boolean} True if every path consumes exactly one expression and none are components. */
49
+ pathsSingleExpr = false;
50
+
51
+ /** @type {boolean} True until applyExprs() finishes the first time.
52
+ * While true, ancestor node caches can't reference this NodeGroup's nodes, so they don't need invalidation. */
53
+ firstApply = true;
54
+
55
+ /**
56
+ * @internal
57
+ * @type {Node[]} Cached result of getNodes() used only for improving performance.*/
58
+ nodesCache;
59
+
60
+ /**
61
+ * A map between <style> Elements and their text content.
62
+ * This lets NodeGroup.updateStyles() see when the style text has changed.
63
+ * @type {?Map<HTMLStyleElement, string>} */
64
+ styles;
65
+
66
+ /** @type {Template} */
67
+ template;
68
+
69
+
70
+ /**
71
+ * Create an "instantiated" NodeGroup from a Template and add it to an element.
72
+ * Don't call applyExprs() yet to apply expressions or instantiate components yet.
73
+ * @param template {Template} Create it from the html strings and expressions in this template.
74
+ * @param parentPath {?Path}
75
+ * @param el {?HTMLElement} Optional, pre-existing htmlElement that will be the root.
76
+ * @param options {?object} Only used for RootNodeGroup */
77
+ constructor(template, parentPath=null, el=null, options=null) {
78
+ this.rootNg = parentPath?.parentNg?.rootNg || this;
79
+ this.parentPath = parentPath;
80
+
81
+ /*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
82
+ this.template = template;
83
+
84
+ // JSX templates carry their list key on the Template (tagged templates instead set it via
85
+ // a PathToKey during applyExprs). Adopt it so the keyed reconciler sees ng.key uniformly.
86
+ if (template.key !== undefined)
87
+ this.key = template.key;
88
+
89
+ // If it's just a text node, skip a bunch of unnecessary steps.
90
+ // el can be an existing Text node to adopt, from PathToNodes' bare-text fast path.
91
+ if (template.isText) {
92
+ this.closeKey = template.getCloseKey();
93
+ this.startNode = this.endNode = el || Globals.doc.createTextNode(template.html[0]);
94
+ }
95
+
96
+ else {
97
+ // Get a cached version of the parsed and instantiated html, and Paths:
98
+ const shell = Shell.get(template.html, template.svgMode);
99
+
100
+ // The shell caches the close key so each new template doesn't repeat the WeakMap lookup.
101
+ this.closeKey = shell.closeKey ??= template.getCloseKey();
102
+
103
+ this.hasComponentPaths = shell.hasComponentPaths;
104
+ this.pathsSingleExpr = shell.pathsSingleExpr;
105
+
106
+ // A lone root element is cloned directly, skipping a throwaway fragment wrapper.
107
+ // Only for child NodeGroups; RootNodeGroup's grafting expects a fragment.
108
+ if (shell.singleRoot && parentPath !== null) {
109
+ const clone = shell.fragment.firstChild.cloneNode(true);
110
+ this.startNode = this.endNode = clone;
111
+
112
+ // Stampable shells skip path creation entirely; the first applyExprs() routes
113
+ // to applyStamp(), and paths are materialized only if the group is rewritten.
114
+ if (shell.stampable !== true)
115
+ this.setPathsFromFragment(clone, shell, 0, true);
116
+ }
117
+ else {
118
+ const shellFragment = shell.fragment.cloneNode(true);
119
+
120
+ if (shellFragment.nodeType === 11) { // DocumentFragment
121
+ this.startNode = shellFragment.firstChild;
122
+ this.endNode = shellFragment.lastChild;
123
+ } else
124
+ this.startNode = this.endNode = shellFragment;
125
+
126
+ this.instantiate(shell, shellFragment, el, options);
127
+ }
128
+ }
129
+
130
+ //#IFDEV
131
+ this.verify();
132
+ //#ENDIF
133
+ }
134
+
135
+ /**
136
+ * Set up paths and embeds from the cloned fragment.
137
+ * RootNodeGroup overrides this with its more involved setup.
138
+ * @param shell {Shell}
139
+ * @param shellFragment {DocumentFragment|HTMLElement|Text}
140
+ * @param el {?HTMLElement} Unused here; used by RootNodeGroup.
141
+ * @param options {?object} Unused here; used by RootNodeGroup. */
142
+ instantiate(shell, shellFragment, el, options) {
143
+ // A non-stampable group must keep a non-null paths array; null is the stamped/text
144
+ // sentinel, and reuse would otherwise route a path-less group through rewriteStamp(),
145
+ // which only exists for stampable shells. Zero-expression shells have no paths to build.
146
+ if (shell.paths.length)
147
+ this.setPathsFromFragment(shellFragment, shell);
148
+ else
149
+ this.paths = [];
150
+
151
+ if (shell.hasEmbeds)
152
+ this.activateEmbeds(shellFragment, shell);
153
+ }
154
+
155
+
156
+ /**
157
+ * Use the paths to insert the given expressions.
158
+ * Dispatches expression handling to other functions depending on the path type.
159
+ * @param exprs {(*|*[]|function|Template)[]}
160
+ * @param includeNonComponents {boolean} False to only apply component paths,
161
+ * used when the non-component exprs are known to be unchanged. */
162
+ applyExprs(exprs, includeNonComponents=true) {
163
+
164
+ /*#IFDEV*/
165
+ this.verify();
166
+ /*#ENDIF*/
167
+
168
+ let paths = this.paths;
169
+
170
+ // Fast path: every path consumes exactly one expression and none are components,
171
+ // so skip the bookkeeping that maps expressions to paths.
172
+ if (this.pathsSingleExpr) {
173
+ if (includeNonComponents) {
174
+ if (paths === null) { // Created from a stampable shell; no paths yet.
175
+ this.applyStamp(exprs);
176
+ return;
177
+ }
178
+ for (let i = paths.length - 1; i >= 0; i--)
179
+ paths[i].applySingle(exprs[i]);
180
+
181
+ if (this.styles)
182
+ this.updateStyles();
183
+
184
+ // Invalidate the nodes cache because we just changed it.
185
+ this.nodesCache = null;
186
+ }
187
+ this.firstApply = false;
188
+ return;
189
+ }
190
+
191
+ if (!paths) { // Text NodeGroups have no paths.
192
+ this.firstApply = false;
193
+ return;
194
+ }
195
+
196
+ // Things to consider:
197
+ // 1. Paths consume a varying number of expressions.
198
+ // An PathToAttribs may use multipe expressions. E.g. <div class="${1} ${2}">
199
+ // While an PathToComponent uses zero.
200
+ // 2. An PathToComponent references other Paths that set its attribute values.
201
+ // 3. We apply them in reverse order so that a <select> box has its children created from an expression
202
+ // before its instantiated and its value attribute is set via an expression.
203
+ let exprIndex = exprs.length; // Update exprs at paths.
204
+ let pathExprs = new Array(paths.length); // Store all the expressions that map to a single path. Only paths to attribute values can have more than one.
205
+ for (let i = paths.length - 1, path; path = paths[i]; i--) {
206
+ if (i===0 && path instanceof PathToComponent && path.nodeMarker === this.getRootNode())
207
+ continue;
208
+
209
+ // Get the expressions associated with this path.
210
+ let exprCount = path.getExpressionCount();
211
+ pathExprs[i] = exprs.slice(exprIndex-exprCount, exprIndex); // slice() probably doesn't allocate if the JS vm implements copy on write.
212
+ exprIndex -= exprCount;
213
+
214
+ // Component expressions don't have a corresponding user-provided expression.
215
+ // They use expressions from the paths that provide their attributes.
216
+ if (path instanceof PathToComponent) {
217
+ let attribExprs = pathExprs.slice(i+1, i+1 + path.attribPaths.length); // +1 b/c we move forward from the component path.
218
+ path.apply(attribExprs);
219
+ }
220
+ else if (includeNonComponents)
221
+ path.apply(pathExprs[i]);
222
+ }
223
+
224
+ // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
225
+ // and the number of paths not matching.
226
+ /*#IFDEV*/
227
+ assert(exprIndex === 0);
228
+ /*#ENDIF*/
229
+
230
+
231
+ if (includeNonComponents) {
232
+
233
+ // TODO: Only do this if we have Paths within styles?
234
+ this.updateStyles();
235
+
236
+ // Invalidate the nodes cache because we just changed it.
237
+ this.nodesCache = null;
238
+
239
+ }
240
+ this.firstApply = false;
241
+
242
+ /*#IFDEV*/
243
+ this.verify();
244
+ /*#ENDIF*/
245
+ }
246
+
247
+ /**
248
+ * Write expressions into a freshly stamped (or pooled path-less) NodeGroup through the
249
+ * shell's shared stamper paths, allocating no per-instance Path objects.
250
+ * Child-node expressions must be primitives (one text write each); anything else
251
+ * falls back to materializing real paths and applying normally.
252
+ * @param exprs {Expr[]} */
253
+ applyStamp(exprs) {
254
+ let template = this.template;
255
+ let shell = Shell.get(template.html, template.svgMode);
256
+
257
+ // 1. Bail to real paths when any child-node expression isn't a primitive.
258
+ let nodesIdx = shell.nodesPathIdx;
259
+ for (let i=0; i<nodesIdx.length; i++) {
260
+ let t = typeof exprs[nodesIdx[i]];
261
+ if (t !== 'string' && t !== 'number') {
262
+ let paths = this.materializePaths(shell);
263
+ for (let i = paths.length - 1; i >= 0; i--)
264
+ paths[i].applySingle(exprs[i]);
265
+ this.nodesCache = null;
266
+ this.firstApply = false;
267
+ return;
268
+ }
269
+ }
270
+
271
+ // 2. Resolve target nodes, then write each expression.
272
+ let slots = this.resolveStampSlots(shell);
273
+ let paths = shell.paths, stampers = shell.stampPaths;
274
+ for (let i = paths.length - 1; i >= 0; i--) {
275
+ let stamper = stampers[i];
276
+ let marker = slots[paths[i].markerSlot];
277
+
278
+ // A wholeParent text path's marker is the (freshly cloned, empty) only-child slot:
279
+ // write its text directly, skipping applySingle's branching and the shared-stamper
280
+ // bookkeeping. Child exprs are primitive here (step 1 bailed otherwise).
281
+ if (stamper.wholeParent) {
282
+ let v = exprs[i];
283
+ if (typeof v === 'number')
284
+ v += '';
285
+ marker.textContent = v;
286
+ continue;
287
+ }
288
+
289
+ stamper.nodeMarker = marker;
290
+ stamper.parentNg = this;
291
+ stamper.applySingle(exprs[i]);
292
+ }
293
+
294
+ this.nodesCache = null;
295
+ this.firstApply = false;
296
+ }
297
+
298
+ /**
299
+ * In-place rewrite of a stamped (path-less) NodeGroup through the shared stampers,
300
+ * comparing expressions and writing only the changed ones. The group stays path-less.
301
+ * @param template {Template} The new template; the caller assigns it to this.template.
302
+ * @return {boolean} False when a child-node expression isn't primitive; the caller
303
+ * must then materialize paths and apply normally. */
304
+ rewriteStamp(template) {
305
+ let shell = Shell.get(template.html, template.svgMode);
306
+ let newExprs = template.exprs;
307
+ let nodesIdx = shell.nodesPathIdx;
308
+ for (let i=0; i<nodesIdx.length; i++) {
309
+ let t = typeof newExprs[nodesIdx[i]];
310
+ if (t !== 'string' && t !== 'number')
311
+ return false;
312
+ }
313
+
314
+ let oldExprs = this.template.exprs;
315
+ let paths = shell.paths, stampers = shell.stampPaths;
316
+ let slots = null; // Nodes are resolved only if something actually changed.
317
+ for (let i = paths.length - 1; i >= 0; i--) {
318
+ if (!exprSame(oldExprs[i], newExprs[i])) {
319
+ if (slots === null)
320
+ slots = this.resolveStampSlots(shell);
321
+ let stamper = stampers[i];
322
+ let marker = slots[paths[i].markerSlot];
323
+
324
+ // Fast path for a wholeParent text path whose child already exists (the common
325
+ // rewrite case): set its value directly, skipping applySingle's branching and
326
+ // textNode bookkeeping. exprSame above already proved it changed.
327
+ if (stamper.wholeParent) {
328
+ let v = newExprs[i], tn = marker.firstChild;
329
+ if (typeof v === 'number')
330
+ v += '';
331
+ if (tn !== null && tn.nodeType === 3 && tn === marker.lastChild)
332
+ tn.nodeValue = v;
333
+ else {
334
+ // Empty/absent text child: fall back to the stamper, then clear its per-row
335
+ // state immediately so the shared stamper doesn't carry into the next row.
336
+ stamper.nodeMarker = marker;
337
+ stamper.parentNg = this;
338
+ stamper.applySingle(newExprs[i]);
339
+ stamper.textNode = null;
340
+ stamper.textValue = null;
341
+ stamper.nodesCache = null;
342
+ }
343
+ continue;
344
+ }
345
+
346
+ stamper.nodeMarker = marker;
347
+ stamper.parentNg = this;
348
+ stamper.applySingle(newExprs[i]);
349
+ }
350
+ }
351
+
352
+ return true;
353
+ }
354
+
355
+ /**
356
+ * Run the shell's resolve program from this NodeGroup's root element.
357
+ * Only valid for singleRoot shells, whose ops always start with the root's own pair.
358
+ * @param shell {Shell}
359
+ * @return {Node[]} The shell's shared scratch slots array. */
360
+ resolveStampSlots(shell) {
361
+ let slots = shell.resolveSlots;
362
+ slots[1] = this.startNode;
363
+ let ops = shell.resolveOps;
364
+ // firstChild/nextSibling pointer walk; see setPathsFromFragment for why not childNodes[i].
365
+ for (let i=2, s=2; i<ops.length; i+=2, s++) {
366
+ let node = slots[ops[i]].firstChild;
367
+ for (let k=ops[i+1]; k>0; k--)
368
+ node = node.nextSibling;
369
+ slots[s] = node;
370
+ }
371
+ return slots;
372
+ }
373
+
374
+ /**
375
+ * Create the real Path objects for a NodeGroup that was created by applyStamp().
376
+ * Called lazily, the first time the group is rewritten in place.
377
+ * Recovers the bare-text state of child-node paths that stamped a primitive.
378
+ * @param shell {?Shell}
379
+ * @return {Path[]} */
380
+ materializePaths(shell=null) {
381
+ shell ??= Shell.get(this.template.html, this.template.svgMode);
382
+ let slots = this.resolveStampSlots(shell);
383
+ let paths = shell.paths;
384
+ let pathLength = paths.length;
385
+ let result = this.paths = new Array(pathLength);
386
+ for (let i=0; i<pathLength; i++) {
387
+ let p = paths[i];
388
+ let path = p.cloneWithNodes(p.beforeSlot >= 0 ? slots[p.beforeSlot] : null, slots[p.markerSlot]);
389
+ path.parentNg = this;
390
+ result[i] = path;
391
+ }
392
+
393
+ // A wholeParent child-node path that stamped a primitive left exactly one Text child.
394
+ for (let idx of shell.nodesPathIdx) {
395
+ let path = result[idx];
396
+ let tn = path.nodeMarker.firstChild;
397
+ if (tn !== null && tn.nodeType === 3 && tn === path.nodeMarker.lastChild) {
398
+ path.textNode = tn;
399
+ path.textValue = tn.nodeValue;
400
+ }
401
+ }
402
+ return result;
403
+ }
404
+
405
+ /**
406
+ * Get all the nodes inclusive between startNode and endNode.
407
+ * TODO: when not using nodesCache, could this use less memory with yield?
408
+ * But we'd need to save the reference to the next Node in case it's removed.
409
+ * @return {(Node|HTMLElement)[]} */
410
+ getNodes() {
411
+ // applyExprs() invalidates this cache.
412
+ let result = this.nodesCache;
413
+ if (result) // This does speed up the partialUpdate benchmark by 10-15%.
414
+ return result;
415
+
416
+ result = [];
417
+ let current = this.startNode
418
+ let afterLast = this.endNode?.nextSibling
419
+ while (current && current !== afterLast) {
420
+ result.push(current)
421
+ current = current.nextSibling
422
+ }
423
+
424
+ this.nodesCache = result;
425
+ return result;
426
+ }
427
+
428
+ /**
429
+ * Get the root element of the NodeGroup's RootNodeGroup.
430
+ * @returns {HTMLElement|DocumentFragment} */
431
+ getRootNode() {
432
+ return this.rootNg.root;
433
+ }
434
+
435
+ /**
436
+ * @returns {RootNodeGroup} */
437
+ getRootNodeGroup() {
438
+ return this.rootNg;
439
+ }
440
+
441
+ /**
442
+ * Copy paths in fragment to this.paths.
443
+ * @param fragment {DocumentFragment|HTMLElement}
444
+ * @param shell {Shell}
445
+ * @param startingPathDepth {int}
446
+ * @param isRootClone {boolean} True when fragment is a direct clone of a singleRoot
447
+ * shell's root element: it fills slot 1 itself and the first op pair is skipped. */
448
+ setPathsFromFragment(fragment, shell, startingPathDepth=0, isRootClone=false) {
449
+ let paths = shell.paths;
450
+ let pathLength = paths.length; // For faster iteration
451
+ let result = this.paths = new Array(pathLength);
452
+
453
+ // Fast path: run the shell's precomputed resolve program (see Shell.buildResolveProgram).
454
+ // Each Path.clone() would walk childNodes from the fragment root to its target node,
455
+ // re-traversing the same ancestors for every path. The program instead resolves each
456
+ // unique node exactly once into the slots array: ops is flat [parentSlot, childIndex]
457
+ // pairs in dependency order, pair i filling slot i+1, with slot 0 being the fragment.
458
+ // Paths then copy themselves via cloneWithNodes() using their precomputed slot indexes.
459
+ // Only built for component-free shells, since PathToComponent.clone() has special
460
+ // attribPaths behavior; pathOffset!==0 (root grafting) also uses the fallback.
461
+ let ops = shell.resolveOps;
462
+ if (ops && startingPathDepth === 0) {
463
+ let slots = shell.resolveSlots;
464
+ let i = 0, s = 1;
465
+ if (isRootClone) { // Slot 1 is the root element itself; skip its op pair.
466
+ slots[1] = fragment;
467
+ i = 2;
468
+ s = 2;
469
+ }
470
+ else
471
+ slots[0] = fragment;
472
+ // Resolve each node via firstChild/nextSibling pointer walks instead of
473
+ // childNodes[index]; the live NodeList indexing is markedly slower, and indices
474
+ // are small (markers are elements, often the first child after whitespace stripping).
475
+ for (; i<ops.length; i+=2, s++) {
476
+ let node = slots[ops[i]].firstChild;
477
+ for (let k=ops[i+1]; k>0; k--)
478
+ node = node.nextSibling;
479
+ slots[s] = node;
480
+ }
481
+ for (let i=0; i<pathLength; i++) {
482
+ let p = paths[i];
483
+ let path = p.cloneWithNodes(p.beforeSlot >= 0 ? slots[p.beforeSlot] : null, slots[p.markerSlot]);
484
+ path.parentNg = this;
485
+ result[i] = path;
486
+ }
487
+ }
488
+ else
489
+ for (let i=0; i<pathLength; i++) {
490
+ let path = paths[i].clone(fragment, startingPathDepth)
491
+ path.parentNg = this;
492
+ result[i] = path;
493
+ }
494
+ }
495
+
496
+ updateStyles() {
497
+ if (this.styles)
498
+ for (let [style, oldText] of this.styles) {
499
+ let newText = style.textContent;
500
+ if (oldText !== newText)
501
+ Util.bindStyles(style, this.getRootNodeGroup().root);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * @param root {HTMLElement|DocumentFragment}
507
+ * @param shell {Shell}
508
+ * @param pathOffset {int} */
509
+ activateEmbeds(root, shell, pathOffset=0) {
510
+
511
+ let rootEl = this.rootNg.root;
512
+ if (rootEl) {
513
+ let options = this.rootNg.options;
514
+
515
+ // ids
516
+ if (options?.ids !== false) {
517
+ for (let path of shell.ids) {
518
+ if (pathOffset)
519
+ path = path.slice(0, -pathOffset);
520
+ let el = Path.resolve(root, path);
521
+ Util.bindId(rootEl, el);
522
+ }
523
+ }
524
+
525
+ // styles
526
+ if (options?.styles !== false) {
527
+ if (shell.styles.length)
528
+ this.styles = new Map();
529
+ for (let path of shell.styles) {
530
+ if (pathOffset)
531
+ path = path.slice(0, -pathOffset);
532
+
533
+ /** @type {HTMLStyleElement} */
534
+ let style = Path.resolve(root, path);
535
+ if (rootEl.nodeType === 1) {
536
+ Util.bindStyles(style, rootEl);
537
+ this.styles.set(style, style.textContent);
538
+ }
539
+ }
540
+
541
+ }
542
+ // scripts
543
+ if (options?.scripts !== false) {
544
+ for (let path of shell.scripts) {
545
+ if (pathOffset)
546
+ path = path.slice(0, -pathOffset);
547
+ let script = Path.resolve(root, path);
548
+ // Indirect eval runs in global scope (correct for a <script> tag) and, unlike a direct
549
+ // eval, doesn't force terser to keep every top-level name in the bundle unmangled.
550
+ (0, eval)(script.textContent)
551
+ }
552
+ }
553
+ }
554
+ }
555
+
556
+ //#IFDEV
557
+ getParentNode() {
558
+ return this.startNode?.parentNode
559
+ }
560
+
561
+ get debug() {
562
+ return [
563
+ `parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
564
+ 'nodes:',
565
+ ...setIndent(this.getNodes().map(item => {
566
+ if (item?.nodeType) {
567
+
568
+ let tree = nodeToArrayTree(item, nextNode => {
569
+
570
+ let path = this.paths.find(path=>(path instanceof PathToNodes) && path.getNodes().includes(nextNode));
571
+ if (path)
572
+ return [`Path.nodes:`]
573
+
574
+ return [];
575
+ })
576
+
577
+ // TODO: How to indend nodes belonging to a path vs those that just occur after the path?
578
+ return flattenAndIndent(tree)
579
+ }
580
+ else if (item instanceof Path)
581
+ return setIndent(item.debug, 1)
582
+ }).flat(), 1)
583
+ ]
584
+ }
585
+
586
+ get debugNodes() { return this.getNodes() }
587
+
588
+
589
+ get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
590
+
591
+ verify() {
592
+ if (!window.verify)
593
+ return;
594
+
595
+ assert(this.startNode)
596
+ assert(this.endNode)
597
+ //assert(this.startNode !== this.endNode) // This can be true.
598
+ assert(this.startNode.parentNode === this.endNode.parentNode)
599
+
600
+ // Only if connected:
601
+ assert(!this.startNode.parentNode || this.startNode === this.endNode || this.startNode.compareDocumentPosition(this.endNode) === Node.DOCUMENT_POSITION_FOLLOWING)
602
+
603
+ // if (this.parentPath)
604
+ // assert(this.parentPath.nodeGroups.includes(this));
605
+
606
+ for (let path of this.paths || []) {
607
+ assert(path.parentNg === this)
608
+
609
+ // Fails for detached NodeGroups.
610
+ // NodeGroups get detached when their nodes are removed by reconcileNodes()
611
+ let parentNode = this.getParentNode();
612
+ if (parentNode)
613
+ assert(this.getParentNode().contains(path.getParentNode()))
614
+ path.verify();
615
+ // TODO: Make sure path nodes are all within our own node range.
616
+ }
617
+ return true;
618
+ }
619
+ //#ENDIF
620
+ }
621
+
622
+
623
+
624
+