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/Template.js CHANGED
@@ -1,274 +1,195 @@
1
- import assert from "./assert.js";
2
- import {getObjectHash, getObjectId} from "./hash.js";
3
- import Globals from "./Globals.js";
4
- import RootNodeGroup from "./RootNodeGroup.js";
5
-
6
- /**
7
- * The html strings and evaluated expressions from an html tagged template.
8
- * A unique Template is created for each item in a loop.
9
- * Although the reference to the html strings is shared among templates. */
10
- export default class Template {
11
-
12
- /** @type {Expr[]} Evaulated expressions. */
13
- 'exprs' = []
14
-
15
- /** @type {string[]} */
16
- 'html' = [];
17
-
18
- /** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
19
- hashedFields;
20
-
21
- closeKey;
22
- exactKey;
23
-
24
- isText;
25
-
26
- /**
27
- *
28
- * @param htmlStrings {string[]}
29
- * @param exprs {*[]} */
30
- constructor(htmlStrings=[''], exprs=[]) {
31
- this.html = htmlStrings;
32
-
33
- this.exprs = exprs;
34
-
35
- //this.trace = new Error().stack.split(/\n/g)
36
-
37
- // Multiple templates can share the same htmlStrings array.
38
- //this.hashedFields = [getObjectId(htmlStrings), exprs]
39
-
40
- //#IFDEV
41
- assert(Array.isArray(htmlStrings))
42
- assert(Array.isArray(exprs))
43
-
44
- Object.defineProperty(this, 'debug', {
45
- get() {
46
- return JSON.stringify([this.html, this.exprs]);
47
- }
48
- })
49
- //#ENDIF
50
- }
51
-
52
- /**
53
- * Called by JSON.serialize when it encounters a Template.
54
- * This prevents the hashed version from being too large. */
55
- toJSON() {
56
- if (this.hashedFields===undefined)
57
- this.hashedFields = [getObjectId(this.html), this.exprs];
58
-
59
- return this.hashedFields
60
- }
61
-
62
- /**
63
- * Render the main (root) template.
64
- * @param el {?HTMLElement} Null if we're rendering to a standalone element.
65
- * @param options {RenderOptions}
66
- * @return {?DocumentFragment|HTMLElement} */
67
- 'render'(el=null, options={}) {
68
-
69
-
70
-
71
- let ng = el && Globals.rootNodeGroups.get(el);
72
- if (!ng) {
73
- ng = new RootNodeGroup(this, null, el, options);
74
- if (!el) // null if it's a standalone elment.
75
- el = ng.getRootNode();
76
- Globals.rootNodeGroups.set(el, ng); // All tests still pass if this is commented out!
77
- }
78
-
79
- // Make sure the expresion count matches match the Path "hole" count.
80
- // This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
81
- // These don't always have the same length, for example if one attribute has multiple expressions.
82
- // if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
83
- // throw new Error(
84
- // `Solarite Error: Parent HTMLElement ${ng.template.html.join('${...}')} and ${ng.paths.length} \${value} ` +
85
- // `placeholders can't accomodate a Template with ${this.exprs.length} values.`);
86
-
87
- // Creating the root nodegroup also renders it.
88
- // If we didn't just create it, we need to render it.
89
- if (this.html?.length === 1 && !this.html[0]) // An empty string.
90
- el.innerHTML = ''; // Fast path for empty component.
91
- else {
92
-
93
- let oldKey = ng.exactKey;
94
- let newKey = this.getExactKey();
95
- ng.applyExprs(this.exprs, oldKey !== newKey);
96
- ng.exactKey = newKey;
97
-
98
- //if (firstTime)
99
- // ng.instantiateStaticComponents(ng.staticComponents);
100
- }
101
-
102
- ng.exprsToRender = new Map();
103
- return el;
104
- }
105
-
106
- getExactKey() {
107
- if (this.exactKey===undefined) {
108
- if (this.exprs.length)
109
- this.exactKey = getObjectHash(this);// calls this.toJSON().
110
- else // Don't hash plain html.
111
- this.exactKey = this.html[0];
112
- }
113
- return this.exactKey;
114
- }
115
-
116
- getCloseKey() {
117
- //console.log(this.exprs.length)
118
- if (this.closeKey===undefined) {
119
- if (this.exprs.length)
120
- this.closeKey = /*'@' + */this.toJSON()[0];
121
- else
122
- this.closeKey = this.html[0];
123
- }
124
- // Use the joined html when debugging? But it breaks some tests.
125
- //return '@'+this.html.join('|')
126
-
127
- return this.closeKey;
128
- }
129
-
130
- /**
131
- * @param tag {string}
132
- * @param props {?Record<string, any>}
133
- * @param children
134
- * @returns {Template} */
135
- static fromJsx(tag, props, children) {
136
-
137
- // HTML void elements that must not have closing tags
138
- const isVoid = selfClosingTags.has(tag.toLowerCase());
139
-
140
- // Build htmlStrings/exprs so Shell can place placeholders in attribute values and child content.
141
- let htmlStrings = [];
142
- let templateExprs = [];
143
-
144
- // Opening tag
145
- let open = `<${tag}`;
146
-
147
- // Attributes
148
- if (props && typeof props === 'object') {
149
- for (let name in props) {
150
- let value = props[name];
151
-
152
- // id and data-id are static in templates — never expressions
153
- if (name === 'id' || name === 'data-id') {
154
- // Write directly into the opening string with quotes
155
- open += ` ${name}="${value}"`;
156
- continue;
157
- }
158
-
159
- // Dynamic attribute value: functions are unquoted (e.g., onclick=${fn}), others quoted
160
- if (typeof value === 'function') {
161
- open += ` ${name}=`;
162
- htmlStrings.push(open);
163
- templateExprs.push(value);
164
- // reset so subsequent attributes start fresh (e.g., ' title=')
165
- open = ``;
166
- }
167
- else {
168
- open += ` ${name}=`;
169
- htmlStrings.push(open);
170
- templateExprs.push(value);
171
- // reset so subsequent attributes start fresh (e.g., ' title=')
172
- open = ``;
173
- }
174
- }
175
- }
176
-
177
- // Finalize opening tag precisely to match tagged template splitting
178
- if (!isVoid) {
179
- const pushedAny = htmlStrings.length > 0;
180
- // If nothing pushed yet (no dynamic attrs), push the entire open + '>'
181
- if (!pushedAny)
182
- htmlStrings.push(open + '>');
183
- else {
184
- // If we were in a quoted attr (open === '"'), then the string after expr is '">' ;
185
- // Otherwise (function-valued attr), the string after expr is just '>'
186
- htmlStrings.push(open === '"' ? '">' : '>');
187
- }
188
-
189
- for (let child of children)
190
- addChild(child, htmlStrings, templateExprs);
191
- }
192
-
193
- // Closing tag (not for void tags)
194
- if (!isVoid) {
195
- // If we never emitted the '>' for the open tag (no children were added),
196
- // then it was appended above before children. Now just add the closing tag to the last html segment.
197
- let lastIdx = htmlStrings.length - 1;
198
- htmlStrings[lastIdx] += `</${tag}>`;
199
- }
200
- else {
201
- // Void element: ensure we emitted a trailing '>' segment
202
- const pushedAny = htmlStrings.length > 0;
203
- if (!pushedAny)
204
- htmlStrings.push(open + '>');
205
- else
206
- htmlStrings.push('>');
207
- }
208
-
209
- // Ensure invariant
210
- //assert(htmlStrings.length === templateExprs.length + 1);
211
- //console.log([htmlStrings, templateExprs])
212
- return new Template(htmlStrings, templateExprs);
213
- }
214
- }
215
-
216
-
217
- const selfClosingTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
218
-
219
-
220
- /**
221
- * Add child Templates that were already created via h() and Template.fromJsx()
222
- * @param template {Template}
223
- * @param html {string[]}
224
- * @param exprs {any[]} */
225
- const addChild = (template, html, exprs) => {
226
-
227
- if (Array.isArray(template)) {
228
- for (let c of template)
229
- addChild(c, html, exprs);
230
- }
231
- else {
232
- let flatten = false;
233
- if (template instanceof Template) {
234
- // Heuristic to match tagged-template splitting:
235
- // - Flatten if the child has expressions (so JSX can inline attribute/value placeholders like tagged literals would).
236
- // - Also flatten void elements (e.g., <img>) so they inline like literals.
237
- // - Otherwise, keep as a dynamic child placeholder to match cases where the tagged template used an expression child.
238
- const childHasExprs = template.exprs.length > 0;
239
- if (childHasExprs)
240
- flatten = true;
241
- else {
242
- const m = (template.html[0] || '').match(/^<([a-zA-Z][\w:-]*)/);
243
- const childTag = m ? m[1].toLowerCase() : '';
244
- flatten = selfClosingTags.has(childTag);
245
- }
246
- }
247
-
248
- if (flatten) {
249
- // Flatten/interleave into current segment to match tagged template splitting
250
- html[html.length - 1] += template.html[0];
251
- for (let i = 0; i < template.exprs.length; i++) {
252
- exprs.push(template.exprs[i]);
253
- html.push(template.html[i + 1] ?? '');
254
- }
255
- } else {
256
- // Keep as dynamic child
257
- exprs.push(template);
258
- html.push('');
259
- }
260
- }
261
- }
262
-
263
-
264
- /**
265
- * @typedef {Object} RenderOptions
266
- * @property {boolean=} styles - Replace :host in style tags to scope them locally.
267
- * @property {boolean=} scripts - Execute script tags.
268
- * @property {boolean=} ids - Create references to elements with id or data-id attributes.
269
- * @property {?boolean} render - Deprecated.
270
- * Used only when options are given to a class super constructor inheriting from Solarite.
271
- * True to call render() immediately in super constructor.
272
- * False to automatically call render() at all.
273
- * Undefined (default) to call render() when added to the DOM, unless already rendered.
274
- */
1
+ import assert from "./assert.js";
2
+ import Globals from "./Globals.js";
3
+ import RootNodeGroup from "./RootNodeGroup.js";
4
+
5
+ let lastObjectId = 1;
6
+ let objectIds = new WeakMap();
7
+
8
+ /**
9
+ * Get a short string id unique to the given object, for use as a map key.
10
+ * @param obj {Object}
11
+ * @returns {string} */
12
+ function getObjectId(obj) {
13
+ let result = objectIds.get(obj);
14
+ if (result === undefined) {
15
+ result = '~@' + (lastObjectId++); // Unique 2-byte prefix so it can't collide with html-string keys.
16
+ objectIds.set(obj, result);
17
+ }
18
+ return result;
19
+ }
20
+
21
+ /**
22
+ * The html strings and evaluated expressions from an html tagged template.
23
+ * A unique Template is created for each item in a loop.
24
+ * Although the reference to the html strings is shared among templates. */
25
+ export default class Template {
26
+
27
+ /** @type {Expr[]} Evaulated expressions. Assigned by the constructor. */
28
+ 'exprs' = undefined;
29
+
30
+ /** @type {string[]} Assigned by the constructor. */
31
+ 'html' = undefined;
32
+
33
+ closeKey;
34
+
35
+ isText;
36
+
37
+ /** @type {*} List key for keyed diffing, set by JSX jsxTemplate()/jsxToTemplate() from a
38
+ * `key` prop. Tagged templates instead carry their key as an expr at Shell.keyIndex; the
39
+ * keyed reconciler (PathToNodes) reads whichever is present. */
40
+ key;
41
+
42
+ /** @type {boolean} True if created by the svg`` tag; the Shell parses the html in the SVG namespace. */
43
+ svgMode = false;
44
+
45
+ /**
46
+ *
47
+ * @param htmlStrings {string[]}
48
+ * @param exprs {*[]} */
49
+ constructor(htmlStrings=[''], exprs=[]) {
50
+ this.html = htmlStrings;
51
+
52
+ this.exprs = exprs;
53
+
54
+ //this.trace = new Error().stack.split(/\n/g)
55
+
56
+ //#IFDEV
57
+ assert(Array.isArray(htmlStrings))
58
+ assert(Array.isArray(exprs))
59
+
60
+ Object.defineProperty(this, 'debug', {
61
+ get() {
62
+ return JSON.stringify([this.html, this.exprs]);
63
+ }
64
+ })
65
+ //#ENDIF
66
+ }
67
+
68
+ /**
69
+ * Render the main (root) template.
70
+ * @param el {?HTMLElement} Null if we're rendering to a standalone element.
71
+ * @param options {RenderOptions}
72
+ * @return {?DocumentFragment|HTMLElement} */
73
+ 'render'(el=null, options={}) {
74
+
75
+
76
+
77
+ let ng = el && Globals.rootNodeGroups.get(el);
78
+ if (!ng) {
79
+ ng = new RootNodeGroup(this, null, el, options);
80
+ if (!el) // null if it's a standalone elment.
81
+ el = ng.getRootNode();
82
+ Globals.rootNodeGroups.set(el, ng); // All tests still pass if this is commented out!
83
+ }
84
+
85
+ // Make sure the expresion count matches match the Path "hole" count.
86
+ // This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
87
+ // These don't always have the same length, for example if one attribute has multiple expressions.
88
+ // if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
89
+ // throw new Error(
90
+ // `Solarite Error: Parent HTMLElement ${ng.template.html.join('${...}')} and ${ng.paths.length} \${value} ` +
91
+ // `placeholders can't accomodate a Template with ${this.exprs.length} values.`);
92
+
93
+ // Creating the root nodegroup also renders it.
94
+ // If we didn't just create it, we need to render it.
95
+ if (this.html?.length === 1 && !this.html[0]) // An empty string.
96
+ el.innerHTML = ''; // Fast path for empty component.
97
+ else
98
+ ng.applyExprs(this.exprs);
99
+
100
+ return el;
101
+ }
102
+
103
+ getCloseKey() {
104
+ if (this.closeKey===undefined) {
105
+ if (this.exprs.length)
106
+ this.closeKey = getObjectId(this.html);
107
+ else
108
+ this.closeKey = this.html[0];
109
+ }
110
+ // Use the joined html when debugging? But it breaks some tests.
111
+ //return '@'+this.html.join('|')
112
+
113
+ return this.closeKey;
114
+ }
115
+
116
+ }
117
+
118
+
119
+ /**
120
+ * Do two templates produce identical content?
121
+ * Compares expression values by identity, so no hashing or stringification is needed.
122
+ * @param a {Template}
123
+ * @param b {Template}
124
+ * @return {boolean} */
125
+ export function templatesSame(a, b) {
126
+ if (a.html === b.html && a.svgMode === b.svgMode) {
127
+ let ae = a.exprs, be = b.exprs;
128
+ for (let i=0; i<ae.length; i++)
129
+ if (!exprSame(ae[i], be[i]))
130
+ return false;
131
+ return true;
132
+ }
133
+
134
+ // Text and other single-string templates get a new html array each time, so compare by content.
135
+ if (a.isText === b.isText && !a.exprs.length && !b.exprs.length
136
+ && a.html.length === 1 && b.html.length === 1 && a.svgMode === b.svgMode)
137
+ return a.html[0] === b.html[0];
138
+
139
+ return false;
140
+ }
141
+
142
+ /**
143
+ * Get a string that changes when any value inside obj changes, including deep mutations.
144
+ * Used by PathToComponent to compute the `changed` argument to component render() calls.
145
+ * Functions, Nodes, and repeated/circular objects are represented by identity ids.
146
+ * @param obj {*}
147
+ * @returns {string} */
148
+ export function getObjectHash(obj) {
149
+ const seen = new Set();
150
+ return JSON.stringify(obj, (key, value) => {
151
+ if (typeof value === 'function')
152
+ return getObjectId(value);
153
+ if (typeof value === 'object' && value !== null) {
154
+ if (value instanceof Node)
155
+ return getObjectId(value);
156
+ if (seen.has(value))
157
+ return getObjectId(value);
158
+ seen.add(value);
159
+ if (value instanceof Template)
160
+ return {html: getObjectId(value.html), exprs: value.exprs}; // Don't hash long html strings.
161
+ }
162
+ return value;
163
+ });
164
+ }
165
+
166
+ /**
167
+ * @return {boolean} */
168
+ export function exprSame(a, b) {
169
+ if (a === b)
170
+ return true;
171
+ if (Array.isArray(a)) {
172
+ if (!Array.isArray(b) || a.length !== b.length)
173
+ return false;
174
+ for (let i=0; i<a.length; i++)
175
+ if (!exprSame(a[i], b[i]))
176
+ return false;
177
+ return true;
178
+ }
179
+ if (a instanceof Template && b instanceof Template)
180
+ return templatesSame(a, b);
181
+ return false;
182
+ }
183
+
184
+
185
+ /**
186
+ * @typedef {Object} RenderOptions
187
+ * @property {boolean=} styles - Replace :host in style tags to scope them locally.
188
+ * @property {boolean=} scripts - Execute script tags. Requires a CSP that allows unsafe-eval.
189
+ * @property {boolean=} ids - Create references to elements with id or data-id attributes.
190
+ * @property {?boolean} render - Deprecated.
191
+ * Used only when options are given to a class super constructor inheriting from Solarite.
192
+ * True to call render() immediately in super constructor.
193
+ * False to automatically call render() at all.
194
+ * Undefined (default) to call render() when added to the DOM, unless already rendered.
195
+ */