solarite 0.1.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 (63) hide show
  1. package/build/build.bat +3 -0
  2. package/build/build.js +139 -0
  3. package/build/lib/rollup.min.js +11 -0
  4. package/build/lib/source-map.min.js +1 -0
  5. package/build/lib/terser.min.js +1 -0
  6. package/dist/Solarite-debug.js +4143 -0
  7. package/dist/Solarite.js +3740 -0
  8. package/dist/Solarite.min.js +4 -0
  9. package/docs/index.md +423 -0
  10. package/docs/js/Playground.js +184 -0
  11. package/docs/js/codemirror/codemirror6.js +32036 -0
  12. package/docs/js/codemirror/themeSolarIce.js +312 -0
  13. package/docs/js/documentation.js +32 -0
  14. package/docs/js/ui/CodeEditor.js +840 -0
  15. package/docs/js/ui/DarkToggle.js +52 -0
  16. package/docs/js/ui/FlexResizer.js +142 -0
  17. package/docs/js/util/Draggable2.js +151 -0
  18. package/docs/js/util/Errors.js +9 -0
  19. package/docs/js/util/Html.js +147 -0
  20. package/docs/js/util/Icons.js +623 -0
  21. package/docs/js/util/Input.js +253 -0
  22. package/docs/js/util/Util.js +88 -0
  23. package/docs/js/util/delve.js +43 -0
  24. package/docs/media/FiraCode400.woff2 +0 -0
  25. package/docs/media/cabin-latin-700.woff2 +0 -0
  26. package/docs/media/documentation.css +93 -0
  27. package/docs/media/eternium.css +1123 -0
  28. package/docs/media/solarite-machine.webp +0 -0
  29. package/index.html +325 -0
  30. package/package.json +33 -0
  31. package/readme.md +3 -0
  32. package/src/solarite/ExprPath.js +554 -0
  33. package/src/solarite/MultiValueMap.js +65 -0
  34. package/src/solarite/NodeGroup.js +706 -0
  35. package/src/solarite/NodeGroupManager.js +582 -0
  36. package/src/solarite/Shell.js +307 -0
  37. package/src/solarite/Solarite.js +19 -0
  38. package/src/solarite/Template.js +85 -0
  39. package/src/solarite/Util.js +264 -0
  40. package/src/solarite/createSolarite.js +267 -0
  41. package/src/solarite/getArg.js +99 -0
  42. package/src/solarite/hash.js +101 -0
  43. package/src/solarite/r.js +143 -0
  44. package/src/solarite/udomdiff.js +233 -0
  45. package/src/solarite/watch.js +302 -0
  46. package/src/solarite/watch2.js +439 -0
  47. package/src/unused/FastLookupArray.js +54 -0
  48. package/src/unused/Hashes.js +339 -0
  49. package/src/unused/InUse.test.js +92 -0
  50. package/src/unused/InUseMap.js +98 -0
  51. package/src/unused/LinkedList.js +117 -0
  52. package/src/unused/LinkedList.test.js +115 -0
  53. package/src/unused/Perf.js +47 -0
  54. package/src/unused/Template.js +108 -0
  55. package/src/util/Errors.js +9 -0
  56. package/src/util/Util.js +88 -0
  57. package/src/util/delve.js +43 -0
  58. package/tests/Benchmark.test.js +319 -0
  59. package/tests/NodeGroup.test.js +115 -0
  60. package/tests/Shell.test.js +75 -0
  61. package/tests/Solarite.test.js +2896 -0
  62. package/tests/Testimony.js +602 -0
  63. package/tests/index.html +75 -0
@@ -0,0 +1,233 @@
1
+ /**
2
+ * ISC License
3
+ *
4
+ * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
5
+ *
6
+ * Permission to use, copy, modify, and/or distribute this software for any
7
+ * purpose with or without fee is hereby granted, provided that the above
8
+ * copyright notice and this permission notice appear in all copies.
9
+ *
10
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
11
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
12
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
13
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
14
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
15
+ * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16
+ * PERFORMANCE OF THIS SOFTWARE.
17
+ */
18
+
19
+ import NodeGroup from "./NodeGroup.js";
20
+
21
+ /**
22
+ * @param {Node} parentNode The container where children live
23
+ * @param {Node[]} a The list of current/live children
24
+ * @param {Node[]} b The list of future children
25
+ * @param {(entry: Node, action: number) => Node} get
26
+ * The callback invoked per each entry related DOM operation.
27
+ * @param {Node} [before] The optional node used as anchor to insert before.
28
+ * @returns {Node[]} The same list of future children.
29
+ */
30
+ const udomdiff = (parentNode, a, b, before) => {
31
+ //#IFDEV
32
+ // if (parentNode instanceof ExprPath)
33
+ // parentNode.verify();
34
+ //#ENDIF
35
+
36
+ const bLength = b.length;
37
+ let aEnd = a.length;
38
+ let bEnd = bLength;
39
+ let aStart = 0;
40
+ let bStart = 0;
41
+ let map = null;
42
+ while (aStart < aEnd || bStart < bEnd) {
43
+ // append head, tail, or nodes in between: fast path
44
+ if (aEnd === aStart) {
45
+ // we could be in a situation where the rest of nodes that
46
+ // need to be added are not at the end, and in such case
47
+ // the node to `insertBefore`, if the index is more than 0
48
+ // must be retrieved, otherwise it's gonna be the first item.
49
+ const node = bEnd < bLength
50
+ ? (bStart
51
+ ? (b[bStart - 1].nextSibling)
52
+ : b[bEnd - bStart])
53
+ : before;
54
+ while (bStart < bEnd) {
55
+ let bNode = b[bStart++];
56
+ parentNode.insertBefore(bNode, node);
57
+
58
+ //#IFDEV
59
+ if (bNode instanceof NodeGroup)
60
+ bNode.verify();
61
+ // if (parentNode instanceof ExprPath)
62
+ // parentNode.verify();
63
+ //#ENDIF
64
+ }
65
+ }
66
+ // remove head or tail: fast path
67
+ else if (bEnd === bStart) {
68
+ while (aStart < aEnd) {
69
+ // remove the node only if it's unknown or not live
70
+ let aNode = a[aStart];
71
+ if (!map || !map.has(aNode)) {
72
+ parentNode.removeChild(aNode);
73
+
74
+ //#IFDEV
75
+ if (aNode instanceof NodeGroup)
76
+ aNode.verify();
77
+ // if (parentNode instanceof ExprPath)
78
+ // parentNode.verify();
79
+ //#ENDIF
80
+ }
81
+ aStart++;
82
+ }
83
+ }
84
+ // same node: fast path
85
+ else if (a[aStart] === b[bStart]) {
86
+ aStart++;
87
+ bStart++;
88
+ }
89
+ // same tail: fast path
90
+ else if (a[aEnd - 1] === b[bEnd - 1]) {
91
+ aEnd--;
92
+ bEnd--;
93
+ }
94
+ // The once here single last swap "fast path" has been removed in v1.1.0
95
+ // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
96
+ // reverse swap: also fast path
97
+ else if (
98
+ a[aStart] === b[bEnd - 1] &&
99
+ b[bStart] === a[aEnd - 1]
100
+ ) {
101
+ // this is a "shrink" operation that could happen in these cases:
102
+ // [1, 2, 3, 4, 5]
103
+ // [1, 4, 3, 2, 5]
104
+ // or asymmetric too
105
+ // [1, 2, 3, 4, 5]
106
+ // [1, 2, 3, 5, 6, 4]
107
+ const node = a[--aEnd].nextSibling;
108
+
109
+
110
+ let a2 = b[bStart++];
111
+ let b2 = a[aStart++];
112
+ parentNode.insertBefore(
113
+ a2,
114
+ b2.nextSibling
115
+ );
116
+ //#IFDEV
117
+ if (a2 instanceof NodeGroup)
118
+ a2.verify();
119
+ // if (parentNode instanceof ExprPath)
120
+ // parentNode.verify();
121
+ //#ENDIF
122
+
123
+ let bNode = b[--bEnd];
124
+ parentNode.insertBefore(bNode, node);
125
+
126
+ //#IFDEV
127
+ if (bNode instanceof NodeGroup)
128
+ bNode.verify();
129
+ // if (parentNode instanceof ExprPath)
130
+ // parentNode.verify();
131
+
132
+ //#ENDIF
133
+
134
+ // mark the future index as identical (yeah, it's dirty, but cheap 👍)
135
+ // The main reason to do this, is that when a[aEnd] will be reached,
136
+ // the loop will likely be on the fast path, as identical to b[bEnd].
137
+ // In the best case scenario, the next loop will skip the tail,
138
+ // but in the worst one, this node will be considered as already
139
+ // processed, bailing out pretty quickly from the map index check
140
+ a[aEnd] = b[bEnd];
141
+ }
142
+ // map based fallback, "slow" path
143
+ else {
144
+ // the map requires an O(bEnd - bStart) operation once
145
+ // to store all future nodes indexes for later purposes.
146
+ // In the worst case scenario, this is a full O(N) cost,
147
+ // and such scenario happens at least when all nodes are different,
148
+ // but also if both first and last items of the lists are different
149
+ if (!map) {
150
+ map = new Map;
151
+ let i = bStart;
152
+ while (i < bEnd)
153
+ map.set(b[i], i++);
154
+ }
155
+ // if it's a future node, hence it needs some handling
156
+ if (map.has(a[aStart])) {
157
+ // grab the index of such node, 'cause it might have been processed
158
+ const index = map.get(a[aStart]);
159
+ // if it's not already processed, look on demand for the next LCS
160
+ if (bStart < index && index < bEnd) {
161
+ let i = aStart;
162
+ // counts the amount of nodes that are the same in the future
163
+ let sequence = 1;
164
+ while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
165
+ sequence++;
166
+ // effort decision here: if the sequence is longer than replaces
167
+ // needed to reach such sequence, which would brings again this loop
168
+ // to the fast path, prepend the difference before a sequence,
169
+ // and move only the future list index forward, so that aStart
170
+ // and bStart will be aligned again, hence on the fast path.
171
+ // An example considering aStart and bStart are both 0:
172
+ // a: [1, 2, 3, 4]
173
+ // b: [7, 1, 2, 3, 6]
174
+ // this would place 7 before 1 and, from that time on, 1, 2, and 3
175
+ // will be processed at zero cost
176
+ if (sequence > (index - bStart)) {
177
+ const node = a[aStart];
178
+ while (bStart < index) {
179
+ let bNode = b[bStart++];
180
+ parentNode.insertBefore(bNode, node);
181
+
182
+ //#IFDEV
183
+ if (bNode instanceof NodeGroup)
184
+ bNode.verify();
185
+ // if (parentNode instanceof ExprPath)
186
+ // parentNode.verify();
187
+
188
+ //#ENDIF
189
+ }
190
+ }
191
+ // if the effort wasn't good enough, fallback to a replace,
192
+ // moving both source and target indexes forward, hoping that some
193
+ // similar node will be found later on, to go back to the fast path
194
+ else {
195
+ let aNode = a[aStart++]
196
+ let bNode = b[bStart++]
197
+ parentNode.replaceChild(
198
+ bNode,
199
+ aNode
200
+ );
201
+
202
+ //#IFDEV
203
+ if (aNode instanceof NodeGroup)
204
+ aNode.verify();
205
+ // if (parentNode instanceof ExprPath)
206
+ // parentNode.verify();
207
+ //#ENDIF
208
+ }
209
+ }
210
+ // otherwise move the source forward, 'cause there's nothing to do
211
+ else
212
+ aStart++;
213
+ }
214
+ // this node has no meaning in the future list, so it's more than safe
215
+ // to remove it, and check the next live node out instead, meaning
216
+ // that only the live list index should be forwarded
217
+ else {
218
+ let aNode = a[aStart++];
219
+ parentNode.removeChild(aNode);
220
+
221
+ //#IFDEV
222
+ if (aNode instanceof NodeGroup)
223
+ aNode.verify();
224
+ // if (parentNode instanceof ExprPath)
225
+ // parentNode.verify();
226
+ //#ENDIF
227
+ }
228
+ }
229
+ }
230
+ return b;
231
+ };
232
+
233
+ export default udomdiff;
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Tools for watch variables and performing precise renders.
3
+ */
4
+ import {assert} from "../util/Errors.js";
5
+ import delve from "../util/delve.js";
6
+ import {getObjectHash, getObjectId} from "./hash.js";
7
+ import MultiValueMap from "./MultiValueMap.js";
8
+ import NodeGroupManager, {LoopInfo} from "./NodeGroupManager.js";
9
+ import Template from "./Template.js";
10
+
11
+
12
+ /**
13
+ * Stores info how to transform a path to a template. */
14
+ export class TransformerInfo {
15
+ constructor(path, transformer, hash) {
16
+ this.path = path;
17
+ this.transformer = transformer;
18
+ this.hash = hash;
19
+ }
20
+ }
21
+
22
+
23
+ /**
24
+ * Maps an object path to the function that converts it to a Template.
25
+ * Once it's convert to a template, we can get the hash of that Tempate.
26
+ * Then that hash tells us what NodeGroups are affected by the object.
27
+ *
28
+ * We store the function to get the Template, instead of the Template itself,
29
+ * so we can call that function again when the object has a new value.
30
+ * @type {MultiValueMap<Object, function(...Object):Template>} */
31
+ let pathToTransformer = new MultiValueMap(); // uses a Set() for each value.
32
+ export {pathToTransformer}
33
+
34
+
35
+
36
+ /**
37
+ * Watch the given properties on obj and call obj.render() when any of them are changed.
38
+ * @param obj {Object}
39
+ * @param props {string[]} */
40
+ export function renderWhenChanged(obj, ...props) {
41
+
42
+ }
43
+
44
+ /**
45
+ *
46
+ * @param objectPaths {(*|function)[]}
47
+ * @returns {Template} */
48
+ export function watchGet(...objectPaths) {
49
+
50
+ /** @type {function} */
51
+ let transformer = objectPaths.at(-1);
52
+ let paths;
53
+ if (typeof transformer === 'function') {
54
+ paths = objectPaths.slice(0, -1);
55
+ }
56
+
57
+ // No transformer provided, so we create our own.
58
+ else if (objectPaths.length === 1) {
59
+ paths = [objectPaths[0].slice(0, -1)];
60
+ let prop = objectPaths[0].at(-1);
61
+ transformer = (...args) => (args[0][prop])
62
+ }
63
+
64
+
65
+ // Save arguments used to call the template, so we can call it again when those args have their values change.
66
+ let args = [];
67
+ for (let path of paths) {
68
+
69
+
70
+ let obj = delve(watchSet(path[0]), path.slice(1));
71
+ args.push(obj);
72
+ }
73
+
74
+ let template = transformer(...args);
75
+
76
+ // If the result isn't a Template, convert the function to return a Template that wraps the result.
77
+ // This way NodeGroupManager.findAndDelete() can find a NodeGroup that matches this Template's hash.
78
+ if (!(template instanceof Template)) {
79
+ let oldToTemplate = transformer;
80
+ transformer = function() {
81
+ return new Template(['', ''], [oldToTemplate(...arguments)]);
82
+ }
83
+ template = transformer(...args);
84
+ }
85
+
86
+ // Map the object paths to the function that creates a template.
87
+ let hash = getObjectHash(template);
88
+ for (let path of paths) {
89
+ let serializedPath = serializePath(path);
90
+ pathToTransformer.add(serializedPath, new TransformerInfo(path, transformer, hash)); // Uses a Set() to ensure no duplicates.
91
+ }
92
+
93
+ return template;
94
+ }
95
+
96
+ //let proxyCache = new WeakMap();
97
+
98
+
99
+ /**
100
+ * Set the value of a variable in a way that's watched, so later when we call .renderWatched()
101
+ * We can find what NodeGroups to update.*/
102
+ export function watchSet(obj) {
103
+ if (obj?.$isProxy===true)
104
+ return obj; // It's already a Proxy.
105
+
106
+ // This cache doesn't make things faster.
107
+ // let result = proxyCache.get(obj);
108
+ // if (!result) {
109
+ // result = new Proxy(obj, new ProxyHandler(obj));
110
+ // proxyCache.set(obj, result);
111
+ // }
112
+ // return result;
113
+ return new Proxy(obj, new ProxyHandler(obj));
114
+ }
115
+
116
+ /**
117
+ * Loop over each item and apply watchGet() to each item.
118
+ * @param arrayPath {*[]}
119
+ * @param callback {function(obj:Object, index:int):Template}
120
+ * @returns {Template} */
121
+ export function forEach(arrayPath, callback) {
122
+ let array = delve(arrayPath[0], arrayPath.slice(1));
123
+
124
+ // This is retrieved on the 'insert' path inside renderWatched()
125
+ let ngm = NodeGroupManager.get(arrayPath[0]);
126
+ if (ngm.clearSubscribers) {
127
+ ngm.clearSubscribers = false;
128
+ ngm.pathToLoopInfo = new MultiValueMap();
129
+ } // TODO: Move tis into NodeGroupMAnager.get() without breaking things?
130
+
131
+
132
+ let newItems = [...array.map((item, i) => {
133
+ // TODO: This needs to wrap callback so we can pass it the index also.
134
+ return watchGet([...arrayPath, i], callback); // calls callback(array[i], i)
135
+ })
136
+ ];
137
+
138
+ // We return a template that wraps the array
139
+ // So that NodeGroup.applyOneExpr can set the ExprPath and nextSibling on the template.
140
+ // Then the 'insert' path in renderWatched() uses that data fora dding more nodes.
141
+ let result = new Template(['', ''], [newItems])
142
+
143
+
144
+ // We get a unique hash for each foreach template because the [''] array is unique each time.
145
+ let loopInfo = new LoopInfo(result, callback);
146
+ ngm.pathToLoopInfo.add(serializePath(arrayPath), loopInfo);
147
+ return result;
148
+ }
149
+
150
+ export function serializePath(path) {
151
+ // Convert any array indices to strings, so serialized comparisons work.
152
+ return JSON.stringify([getObjectId(path[0]), ...path.slice(1).map(item => item+'')])
153
+
154
+ }
155
+
156
+
157
+ /**
158
+ * When an object property is accessed, a new Proxy with a new instance of this handler class is created,
159
+ * but it tracks the path from the root to the property.
160
+ * That way when a property is set, it can report the changed path. */
161
+ class ProxyHandler {
162
+
163
+ /**
164
+ * @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
165
+ * @param path {string[]} Used internally. */
166
+ constructor(root, path=[]) {
167
+ /*#IFDEV*/assert(NodeGroupManager.get(root))/*#ENDIF*/
168
+ this.root = root;
169
+ this.path = path; // path from root to this Proxy.
170
+ }
171
+
172
+ /**
173
+ * @param obj {Object}
174
+ * @param prop {string} */
175
+ get(obj, prop) {
176
+
177
+ // Special props. Currently unused.
178
+ // if (prop === '$path')
179
+ // return this.path;
180
+ // if (prop === '$root')
181
+ // return this.root;
182
+ if (prop === '$isProxy')
183
+ return true;
184
+
185
+
186
+ // 1. Array.splice()
187
+ if (prop === 'splice' && Array.isArray(obj)) {
188
+ return (index, deleteCount, ...items) => {
189
+ let ngm = NodeGroupManager.get(this.root)
190
+
191
+ if (deleteCount) {
192
+
193
+ // Get the hash of each object along the delete range. The process to get the hash is:
194
+ // Serialized Path -> transformer -> Template -> hash.
195
+ let hashes = [];
196
+ for (let i=index; i<index+deleteCount; i++) {
197
+ let serializedPath = serializePath([this.root, ...this.path, i+'']);
198
+
199
+ let obj = delve(this.root, [...this.path, i])
200
+ for (let transformerInfo of pathToTransformer.getAll(serializedPath)) {
201
+ let template = transformerInfo.transformer(obj);
202
+ let hash = getObjectHash(template);
203
+ hashes.push(hash); // Hashes may go to nodes in more than one loop.
204
+ }
205
+ }
206
+
207
+ ngm.changes.push(new Change('delete', this.root, [...this.path, index+''], hashes));
208
+ }
209
+
210
+ //let oldArray = [...obj];
211
+ let result = obj.splice(index, deleteCount);
212
+
213
+ // Inserting
214
+ if (items.length) {
215
+
216
+ let beforeNgs;
217
+ for (let loopInfo of ngm.getLoopInfo([this.root, ...this.path])) {
218
+ let beforeObj = delve(this.root, [...this.path, index]);
219
+
220
+ // Find where to insert before.
221
+ if (beforeObj) {
222
+ let beforeTemplate = loopInfo.itemTransformer(beforeObj);
223
+ let beforeHash = getObjectHash(beforeTemplate);
224
+ beforeNgs = ngm.nodeGroupsAvailable.data[beforeHash];
225
+
226
+ if (beforeNgs) {
227
+ let hash = getObjectHash(loopInfo.template)
228
+ let loopNgs = ngm.nodeGroupsAvailable.data[hash] || [];
229
+ for (let loopNg of loopNgs)
230
+ for (let beforeNg of beforeNgs)
231
+ if (beforeNg.startNode.parentNode === loopNg.startNode.parentNode)
232
+ ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items, beforeTemplate));
233
+ }
234
+ }
235
+ if (!beforeNgs)
236
+ ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items));
237
+ }
238
+ obj.splice(index, 0, ...items);
239
+ }
240
+ return result;
241
+ }
242
+ }
243
+
244
+
245
+
246
+ // 2. Get property
247
+ // If we're getting an object or array property, apply watch() to it recursively.
248
+ let result = Reflect.get(obj, prop)
249
+ if (result && typeof result === 'object') {
250
+ let handler = new ProxyHandler(this.root, [...this.path, prop]); // same root, one level deeper on the path.
251
+ return new Proxy(result, handler);
252
+ }
253
+
254
+ return result;
255
+ }
256
+
257
+
258
+ set(obj, prop, newValue) {
259
+ let ngm = NodeGroupManager.get(this.root);
260
+ ngm.changes.push(new Change('set', this.root, [...this.path, prop], newValue));
261
+ return Reflect.set(obj, prop, newValue)
262
+ }
263
+ }
264
+
265
+
266
+ /**
267
+ *
268
+ */
269
+ class Change {
270
+
271
+ /**
272
+ * @param action {string}
273
+ * @param root {Object|Array}
274
+ * @param path {string[]}
275
+ * @param value
276
+ * If setting a value, this is the new value.
277
+ * If deleting from an array, this is an array of all the NodeGroups to delete.
278
+ *
279
+ * @param beforeTemplate
280
+ * */
281
+ constructor(action, root, path, value, beforeTemplate=null) {
282
+ this.action = action;
283
+
284
+ // TODO: Store root as first item of path, to be consistent with code elsewhere.
285
+ this.root = root;
286
+ this.path = path;
287
+ this.value = value;
288
+ this.beforeTemplate = beforeTemplate;
289
+
290
+ /** @type {TransformerInfo[]} */
291
+ this.transformerInfo = [];
292
+
293
+ // Traverse up the path.
294
+ for (let i=this.path.length; i>0; i--) {
295
+ let path = this.path.slice(0, i);
296
+ let fullPath = [this.root, ...path];
297
+
298
+ let serializedPath = getObjectHash(fullPath); // TODO: Why not serializedPath() ?
299
+ this.transformerInfo.push(...pathToTransformer.getAll(serializedPath))
300
+ }
301
+ }
302
+ }