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,307 @@
1
+ import {assert} from "../util/Errors.js";
2
+ import ExprPath, {PathType, getNodePath} from "./ExprPath.js";
3
+
4
+ import {div, htmlContext, isEvent} from "./Util.js";
5
+
6
+ /**
7
+ * A Shell is created from a tagged template expression instantiated as Nodes,
8
+ * but without any expressions filled in. */
9
+ export default class Shell {
10
+
11
+ /**
12
+ * @type {DocumentFragment} Parent of the shell nodes. */
13
+ fragment;
14
+
15
+ /** @type {ExprPath[]} Paths to where expressions should go. */
16
+ paths = [];
17
+
18
+ /** @type {?Template} Template that created this element. */
19
+ template;
20
+
21
+ // Embeds and ids
22
+ events = [];
23
+
24
+ /** @type {int[][]} Array of paths */
25
+ ids = [];
26
+ scripts = [];
27
+ styles = [];
28
+
29
+ staticComponents = [];
30
+
31
+
32
+ /**
33
+ * Create the nodes but without filling in the expressions.
34
+ * This is useful because the expression-less nodes created by a template can be cached.
35
+ * @param html {string[]} */
36
+ constructor(html=null) {
37
+ if (!html)
38
+ return;
39
+
40
+ //#IFDEV
41
+ this.html = html.join('');
42
+ //#ENDIF
43
+
44
+ // 1. Add placeholders
45
+ // We increment the placeholder char as we go because nodes can't have the same attribute more than once.
46
+ let placeholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
47
+
48
+ let buffer = [];
49
+ let commentPlaceholder = `<!--!✨!-->`;
50
+ let componentNames = {};
51
+
52
+ htmlContext(null); // Reset the context.
53
+ for (let i=0; i<html.length; i++) {
54
+ let lastHtml = html[i];
55
+ let context = htmlContext(lastHtml);
56
+
57
+ // Swap out Embedded Solarite Components with ${} attributes.
58
+ // Later, NodeGroup.render() will search for these and replace them with the real components.
59
+ // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
60
+ if (context === htmlContext.Attribute) {
61
+
62
+ let lastIndex, lastMatch;
63
+ lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
64
+ lastIndex = index+1; // +1 for after opening <
65
+ lastMatch = match.slice(1);
66
+ })
67
+
68
+ if (lastMatch) {
69
+ let newTagName = lastMatch + '-redcomponent-placeholder';
70
+ lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
71
+ componentNames[lastMatch] = newTagName
72
+ }
73
+ }
74
+
75
+ buffer.push(lastHtml);
76
+ //console.log(lastHtml, context)
77
+ if (i < html.length-1)
78
+ if (context === htmlContext.Text)
79
+ buffer.push(commentPlaceholder) // Comment Placeholder. because we can't put text in between <tr> tags for example.
80
+ else
81
+ buffer.push(String.fromCharCode(placeholder+i));
82
+ }
83
+
84
+ // 2. Create elements from html with placeholders.
85
+ let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
86
+ let joinedHtml = buffer.join('');
87
+
88
+ // Replace '-redcomponent-placeholder' close tags.
89
+ // TODO: is there a better way? What if the close tag is inside a comment?
90
+ for (let name in componentNames)
91
+ joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
92
+
93
+ if (joinedHtml)
94
+ template.innerHTML = joinedHtml;
95
+ else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
96
+ template.content.append(document.createTextNode(''))
97
+ this.fragment = template.content;
98
+
99
+ // 3. Find placeholders
100
+ let node;
101
+ let toRemove = [];
102
+ const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
103
+ while (node = walker.nextNode()) {
104
+
105
+ // Remove previous after each iteration, so paths will still be calculated correctly.
106
+ toRemove.map(el => el.remove());
107
+ toRemove = [];
108
+
109
+ // Replace attributes
110
+ if (node.nodeType === 1) {
111
+ for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
112
+
113
+ // Whole attribute
114
+ let matches = attr.name.match(/^[\ue000-\uf8ff]$/)
115
+ if (matches) {
116
+ this.paths.push(new ExprPath(null, node, PathType.Multiple));
117
+ node.removeAttribute(matches[0]);
118
+ }
119
+
120
+ // Just the attribute value.
121
+ else {
122
+ let parts = attr.value.split(/[\ue000-\uf8ff]/g);
123
+ if (parts.length > 1) {
124
+ let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
125
+ this.paths.push(new ExprPath(null, node, PathType.Value, attr.name, nonEmptyParts));
126
+ node.setAttribute(attr.name, parts.join(''));
127
+ }
128
+ }
129
+ }
130
+ }
131
+ // Replace comment placeholders
132
+ else if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === '!✨!') {
133
+
134
+ // Get or create nodeBefore.
135
+ let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
136
+ if (!nodeBefore) {
137
+ nodeBefore = document.createComment('PathStart:'+this.paths.length);
138
+ node.parentNode.insertBefore(nodeBefore, node)
139
+ }
140
+ /*#IFDEV*/assert(nodeBefore);/*#ENDIF*/
141
+
142
+ // Get the next node.
143
+ let nodeMarker;
144
+
145
+ // A subsequent node is available to be a nodeMarker.
146
+ if (node.nextSibling && (node.nextSibling.nodeType !== 8 || node.nextSibling.textContent !== '!✨!')) {
147
+ nodeMarker = node.nextSibling;
148
+ toRemove.push(node); // Removing them here will mess up the treeWalker.
149
+ }
150
+ // Re-use existing comment placeholder.
151
+ else {
152
+ nodeMarker = node;
153
+ nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
154
+ }
155
+ /*#IFDEV*/assert(nodeMarker);/*#ENDIF*/
156
+
157
+
158
+
159
+ let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
160
+ //#IFDEV
161
+ path.parentIndex = this.paths.length; // For debugging.
162
+ //#ENDIF
163
+ this.paths.push(path);
164
+ }
165
+
166
+
167
+ // Sometimes users will comment out a block of html code that has expressions.
168
+ // Here we look for expressions in comments.
169
+ // We don't actually update them dynamically, but we still add paths for them.
170
+ // That way the expression count still matches.
171
+ else if (node.nodeType === Node.COMMENT_NODE) {
172
+ let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
173
+ for (let i=0; i<parts.length-1; i++) {
174
+ let path = new ExprPath(node.previousSibling, node)
175
+ path.type = PathType.Comment;
176
+ //#IFDEV
177
+ path.parentIndex = i; // For debugging.
178
+ //#ENDIF
179
+ this.paths.push(path);
180
+ }
181
+ }
182
+
183
+ // Replace comment placeholders inside script and style tags, which have become text nodes.
184
+ else if (node.nodeType === Node.TEXT_NODE && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) {
185
+ let parts = node.textContent.split(commentPlaceholder);
186
+ if (parts.length > 1) {
187
+
188
+ let placeholders = [];
189
+ for (let i = 0; i<parts.length; i++) {
190
+ let current = document.createTextNode(parts[i]);
191
+ node.parentNode.insertBefore(current, node);
192
+ if (i > 0)
193
+ placeholders.push(current)
194
+ }
195
+
196
+ for (let i=0, node; node=placeholders[i]; i++) {
197
+ let path = new ExprPath(node.previousSibling, node, PathType.Content)
198
+ //#IFDEV
199
+ path.parentIndex = i; // For debugging.
200
+ //#ENDIF
201
+ this.paths.push(path);
202
+
203
+ /*#IFDEV*/path.verify();/*#ENDIF*/
204
+ }
205
+
206
+ // Removing them here will mess up the treeWalker.
207
+ toRemove.push(node);
208
+ }
209
+ }
210
+ }
211
+ toRemove.map(el => el.remove());
212
+
213
+ // Handle redcomponent-placeholder's.
214
+ // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
215
+ //if (componentNames.size)
216
+ // this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
217
+
218
+ // Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
219
+ // that happens in NodeGroup.applyComponentExprs()
220
+ for (let el of this.fragment.querySelectorAll('[is]')) {
221
+ el.setAttribute('_is', el.getAttribute('is'))
222
+ // this.components.push(el);
223
+ }
224
+
225
+ for (let path of this.paths) {
226
+ if (path.nodeBefore)
227
+ path.nodeBeforeIndex = Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore)
228
+ path.nodeMarkerPath = getNodePath(path.nodeMarker)
229
+
230
+ // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
231
+ if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 &&
232
+ (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
233
+ path.type = PathType.Component;
234
+ }
235
+ }
236
+
237
+
238
+ this.findEmbeds();
239
+
240
+
241
+ /*#IFDEV*/this.verify();/*#ENDIF*/
242
+ } // end constructor
243
+
244
+ /**
245
+ * We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
246
+ * When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths. */
247
+ findEmbeds() {
248
+ this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el))
249
+ this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el))
250
+
251
+ let idEls = this.fragment.querySelectorAll('[id],[data-id]');
252
+
253
+
254
+ // Check for valid id names.
255
+ for (let el of idEls) {
256
+ let id = el.getAttribute('data-id') || el.getAttribute('id')
257
+ if (div.hasOwnProperty(id))
258
+ throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement property.`)
259
+ }
260
+
261
+
262
+ this.ids = Array.prototype.map.call(idEls, el => getNodePath(el))
263
+
264
+ // Events (not yet used)
265
+ for (let el of this.fragment.querySelectorAll('*')) {
266
+ for (let attrib of el.attributes)
267
+ if (isEvent(attrib.name))
268
+ this.events.push([attrib.name, getNodePath(el)])
269
+
270
+ if (el.tagName.includes('-') || el.hasAttribute('_is'))
271
+
272
+ // Dynamic components have attributes with expression values.
273
+ // They are created from applyExprs()
274
+ // But static components are created in a separate path inside the NodeGroup constructor.
275
+ if (!this.paths.find(path => path.nodeMarker === el))
276
+ this.staticComponents.push(getNodePath(el));
277
+ }
278
+
279
+ }
280
+
281
+ /**
282
+ * Get the shell for the html strings.
283
+ * @param htmlStrings {string[]}
284
+ * @returns {Shell} */
285
+ static get(htmlStrings) {
286
+ let result = shells.get(htmlStrings);
287
+ if (!result) {
288
+ result = new Shell(htmlStrings);
289
+ shells.set(htmlStrings, result); // cache
290
+ }
291
+
292
+ /*#IFDEV*/result.verify();/*#ENDIF*/
293
+ return result;
294
+ }
295
+
296
+ //#IFDEV
297
+ // For debugging only:
298
+ verify() {
299
+ for (let path of this.paths) {
300
+ assert(this.fragment.contains(path.getParentNode()))
301
+ path.verify();
302
+ }
303
+ }
304
+ //#ENDIF
305
+ }
306
+
307
+ let shells = new WeakMap();
@@ -0,0 +1,19 @@
1
+ import createSolarite from "./createSolarite.js";
2
+
3
+ /**
4
+ * TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
5
+ * @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
6
+ let Solarite = new Proxy(createSolarite(), {
7
+ apply(self, _, args) {
8
+ return createSolarite(...args)
9
+ }
10
+ });
11
+
12
+
13
+ /** @type {HTMLElement|Class} */
14
+ export {Solarite}
15
+ export {default as r} from './r.js';
16
+ export {getArg, ArgType} from './getArg.js';
17
+
18
+ export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
19
+ export {watch} from './watch2.js'; // unfinished
@@ -0,0 +1,85 @@
1
+ import {assert} from "../util/Errors.js";
2
+ import {getObjectId} from "./hash.js";
3
+ import NodeGroupManager from "./NodeGroupManager.js";
4
+
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 {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
13
+ exprs = []
14
+
15
+ /** @type {string[]} */
16
+ html = [];
17
+
18
+ /**
19
+ * If true, use this template to replace an existing element, instead of appending children to it.
20
+ * @type {?boolean} */
21
+ replaceMode;
22
+
23
+ /** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
24
+ hashedFields;
25
+
26
+ /**
27
+ * @deprecated
28
+ * @type {ExprPath} Used with forEach() from watch.js
29
+ * Set in NodeGroup.applyOneExpr() */
30
+ parentPath;
31
+
32
+ /** @type {NodeGroup} */
33
+ nodeGroup;
34
+
35
+ /**
36
+ * @type {string[][]} */
37
+ paths = [];
38
+
39
+ /**
40
+ *
41
+ * @param htmlStrings {string[]}
42
+ * @param exprs {*[]} */
43
+ constructor(htmlStrings, exprs) {
44
+ this.html = htmlStrings;
45
+ this.exprs = exprs;
46
+
47
+ //this.trace = new Error().stack.split(/\n/g)
48
+
49
+ // Multiple templates can share the same htmlStrings array.
50
+ //this.hashedFields = [getObjectId(htmlStrings), exprs]
51
+
52
+ //#IFDEV
53
+ assert(Array.isArray(htmlStrings))
54
+ assert(Array.isArray(exprs))
55
+
56
+ Object.defineProperty(this, 'debug', {
57
+ get() {
58
+ return JSON.stringify([this.html, this.exprs]);
59
+ }
60
+ })
61
+ //#ENDIF
62
+ }
63
+
64
+ /**
65
+ * Called by JSON.serialize when it encounters a Template.
66
+ * This prevents the hashed version from being too large. */
67
+ toJSON() {
68
+ if (!this.hashedFields)
69
+ this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
70
+
71
+ return this.hashedFields
72
+ }
73
+
74
+ toNode() {
75
+ let ngm = new NodeGroupManager();
76
+ return ngm.render(this);
77
+ }
78
+
79
+ getCloseKey() {
80
+ // Use the joined html when debugging?
81
+ //return '@'+this.html.join('|')
82
+
83
+ return '@'+this.hashedFields[0];
84
+ }
85
+ }
@@ -0,0 +1,264 @@
1
+ let Util = {
2
+
3
+ bindStyles(style, root) {
4
+ let styleId = root.getAttribute('data-style');
5
+ if (!styleId) {
6
+ // Keep track of one style id for each class.
7
+ // TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
8
+ if (!root.constructor.styleId)
9
+ root.constructor.styleId = 1;
10
+ styleId = root.constructor.styleId++;
11
+
12
+ root.setAttribute('data-style', styleId)
13
+ }
14
+
15
+ let tagName = root.tagName.toLowerCase();
16
+ for (let child of style.childNodes) {
17
+ if (child.nodeType === 3) {
18
+ let oldText = child.textContent;
19
+ let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, tagName + '[data-style="' + styleId + '"]')
20
+ if (oldText !== newText)
21
+ child.textContent = newText;
22
+ }
23
+ }
24
+ }
25
+
26
+
27
+ };
28
+
29
+ export default Util;
30
+
31
+
32
+
33
+ let div = document.createElement('div');
34
+ export {div}
35
+
36
+ let isEvent = attrName => attrName.startsWith('on') && attrName in div;
37
+ export {isEvent};
38
+
39
+
40
+ /**
41
+ * Convert a Proper Case name to a name with dashes.
42
+ * Dashes will be placed between letters and numbers.
43
+ * If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
44
+ * @param str {string}
45
+ * @return {string}
46
+ *
47
+ * @example
48
+ * 'ProperName' => 'proper-name'
49
+ * 'HTMLElement' => 'html-element'
50
+ * 'BigUI' => 'big-ui'
51
+ * 'UIForm' => 'ui-form'
52
+ * 'A100' => 'a-100' */
53
+ export function camelToDashes(str) {
54
+ // Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
55
+ str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
56
+
57
+ // Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
58
+ str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
59
+
60
+ // Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
61
+ str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
62
+
63
+ // Convert all the remaining capital letters to lowercase.
64
+ return str.toLowerCase();
65
+ }
66
+
67
+
68
+
69
+
70
+
71
+ /**
72
+ * Returns false if they're the same. Or the first index where they differ.
73
+ * @param a
74
+ * @param b
75
+ * @returns {int|false} */
76
+ export function findArrayDiff(a, b) {
77
+ if (a.length !== b.length)
78
+ return -1;
79
+ let aLength = a.length;
80
+ for (let i=0; i<aLength; i++)
81
+ if (a[i] !== b[i])
82
+ return i;
83
+ return false; // the same.
84
+ }
85
+
86
+
87
+ /**
88
+ * TODO: Turn this into a class because it has internal state.
89
+ * TODO: Don't break on 3<a inside a <script> or <style> tag.
90
+ * @param html {?string} Pass null to reset context.
91
+ * @returns {string} */
92
+ export function htmlContext(html) {
93
+ if (html === null) {
94
+ state = {...defaultState};
95
+ return state.context;
96
+ }
97
+ for (let i = 0; i < html.length; i++) {
98
+ const char = html[i];
99
+ switch (state.context) {
100
+ case htmlContext.Text:
101
+ if (char === '<' && html[i+1].match(/[a-z!]/i)) { // Start of a tag or comment.
102
+ // if (html.slice(i, i+4) === '<!--')
103
+ // state.context = htmlContext.Comment;
104
+ // else
105
+ state.context = htmlContext.Tag;
106
+ state.buffer = '';
107
+ }
108
+ break;
109
+ case htmlContext.Tag:
110
+ if (char === '>') {
111
+ state.context = htmlContext.Text;
112
+ state.quote = null;
113
+ state.buffer = '';
114
+ } else if (char === ' ' && !state.buffer) {
115
+ // No attribute name is present. Skipping the space.
116
+ continue;
117
+ } else if (char === ' ' || char === '/' || char === '?') {
118
+ state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
119
+ } else if (char === '"' || char === "'" || char === '=') {
120
+ state.context = htmlContext.Attribute;
121
+ state.quote = char === '=' ? null : char;
122
+ state.buffer = '';
123
+ } else {
124
+ state.buffer += char;
125
+ }
126
+ break;
127
+ case htmlContext.Attribute:
128
+ if (!state.quote && !state.buffer.length && (char === '"' || char === "'"))
129
+ state.quote = char;
130
+
131
+ else if (char === state.quote || (!state.quote && state.buffer.length)) {
132
+ state.context = htmlContext.Tag;
133
+ state.quote = null;
134
+ state.buffer = '';
135
+ } else if (!state.quote && char === '>') {
136
+ state.context = htmlContext.Text;
137
+ state.quote = null;
138
+ state.buffer = '';
139
+ } else if (char !== ' ') {
140
+ state.buffer += char;
141
+ }
142
+ break;
143
+ }
144
+
145
+ }
146
+ return state.context;
147
+ }
148
+
149
+
150
+ htmlContext.Attribute = 'Attribute';
151
+ htmlContext.Text = 'Text';
152
+ htmlContext.Tag = 'Tag';
153
+ //htmlContext.Comment = 'Comment';
154
+ let defaultState = {
155
+ context: htmlContext.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
156
+ quote: null, // possible values: null, '"', "'"
157
+ buffer: '',
158
+ lastChar: null
159
+ };
160
+ let state = {...defaultState};
161
+
162
+
163
+
164
+
165
+
166
+
167
+ let cacheItems = {};
168
+
169
+ /**
170
+ * @param item {string}
171
+ * @param initial {*}
172
+ * @returns {*} */
173
+ export function cache(item, initial) {
174
+ let result = cacheItems[item];
175
+ if (!result) {
176
+ cacheItems[item] = initial
177
+ result = initial;
178
+ }
179
+ return result;
180
+ }
181
+
182
+
183
+
184
+ export class WeakCache {
185
+
186
+ items = new WeakMap();
187
+
188
+ constructor(initial) {
189
+ this.initial = initial;
190
+ }
191
+
192
+ get(item) {
193
+ let result = this.items.get(item);
194
+ if (!result) {
195
+ let value = typeof this.initial === 'function' ? this.initial() : this.initial;
196
+ this.items.set(item, value)
197
+ result = this.initial;
198
+ }
199
+ return result;
200
+ }
201
+ }
202
+
203
+
204
+ // For debugging only
205
+ //#IFDEV
206
+ export function setIndent(items, level=1) {
207
+ if (typeof items === 'string')
208
+ items = items.split(/\r?\n/g)
209
+
210
+ return items.map(str => {
211
+ if (level > 0)
212
+ return ' '.repeat(level) + str;
213
+ else if (level < 0)
214
+ return str.replace(new RegExp(`^ {0,${Math.abs(level)}}`), '');
215
+ return str;
216
+ })
217
+ }
218
+
219
+ export function nodeToArrayTree(node, callback=null) {
220
+ if (!node) return [];
221
+
222
+ let result = [];
223
+
224
+ if (callback)
225
+ result.push(...callback(node))
226
+
227
+ if (node.nodeType === 1) {
228
+ let attrs = Array.from(node.attributes).map(attr => `${attr.name}="${attr.value}"`).join(' ');
229
+ let openingTag = `<${node.nodeName.toLowerCase()}${attrs ? ' ' + attrs : ''}>`;
230
+
231
+ let childrenArray = [];
232
+ for (let child of node.childNodes) {
233
+ let childResult = nodeToArrayTree(child, callback);
234
+ if (childResult.length > 0) {
235
+ childrenArray.push(childResult);
236
+ }
237
+ }
238
+
239
+ //let closingTag = `</${node.nodeName.toLowerCase()}>`;
240
+
241
+ result.push(openingTag, ...childrenArray);
242
+ } else if (node.nodeType === 3) {
243
+ result.push("'"+node.nodeValue+"'");
244
+ }
245
+
246
+ return result;
247
+ }
248
+
249
+
250
+ export function flattenAndIndent(inputArray, indent = "") {
251
+ let result = [];
252
+
253
+ for (let item of inputArray) {
254
+ if (Array.isArray(item)) {
255
+ // Recursively handle nested arrays with increased indentation
256
+ result = result.concat(flattenAndIndent(item, indent + " "));
257
+ } else {
258
+ result.push(indent + item);
259
+ }
260
+ }
261
+
262
+ return result;
263
+ }
264
+ //#ENDIF