chocola 2.0.0-next.5 → 2.0.0-next.7

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.
@@ -0,0 +1,309 @@
1
+ const exprCache = new Map();
2
+
3
+ function compileExpression(expr, useCtx) {
4
+ const key = useCtx ? "ctx:" + expr : "raw:" + expr;
5
+ let fn = exprCache.get(key);
6
+ if (!fn) {
7
+ fn = useCtx
8
+ ? new Function("ctx", "with(ctx) { return (" + expr + "); }")
9
+ : new Function('"use strict"; return (' + expr + ")");
10
+ exprCache.set(key, fn);
11
+ }
12
+ return fn;
13
+ }
14
+
15
+ function interpolateNode(root, ctx) {
16
+ const stack = [root];
17
+ while (stack.length) {
18
+ const node = stack.pop();
19
+ if (node.nodeType === 3) {
20
+ const text = node.textContent
21
+ .replace(new RegExp(LBRACE_PH, "g"), "{")
22
+ .replace(new RegExp(RBRACE_PH, "g"), "}");
23
+ node.textContent = text.replace(/\{([^}]+)\}/g, (_, expr) => {
24
+ try {
25
+ return String(compileExpression(expr, true)(ctx));
26
+ } catch {
27
+ return "";
28
+ }
29
+ });
30
+ } else {
31
+ for (let i = node.childNodes.length - 1; i >= 0; i--) {
32
+ stack.push(node.childNodes[i]);
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ function applyConditionalToElement(child, ctx, chain, hasIf, hasDelIf, hasElif, hasElse) {
39
+ if (hasIf) {
40
+ const expr = child.getAttribute("if").slice(1, -1);
41
+ chain.active = true;
42
+ if (compileExpression(expr, true)(ctx)) {
43
+ chain.rendered = true;
44
+ } else {
45
+ child.style.display = "none";
46
+ chain.rendered = false;
47
+ }
48
+ child.removeAttribute("if");
49
+ } else if (hasDelIf) {
50
+ const expr = child.getAttribute("del:if").slice(1, -1);
51
+ chain.active = true;
52
+ if (compileExpression(expr, true)(ctx)) {
53
+ chain.rendered = true;
54
+ } else {
55
+ child.remove();
56
+ chain.rendered = false;
57
+ }
58
+ child.removeAttribute("del:if");
59
+ } else if (hasElif) {
60
+ const expr = child.getAttribute("elif").slice(1, -1);
61
+ if (compileExpression(expr, true)(ctx)) {
62
+ chain.rendered = true;
63
+ } else {
64
+ child.remove();
65
+ }
66
+ child.removeAttribute("elif");
67
+ } else if (hasElse) {
68
+ chain.rendered = true;
69
+ chain.active = false;
70
+ child.removeAttribute("else");
71
+ } else {
72
+ chain.active = false;
73
+ chain.rendered = false;
74
+ }
75
+ }
76
+
77
+ const LBRACE_PH = "_%%CHOCOLA-LBRACE%%_";
78
+ const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
79
+
80
+ function interpolateAttributes(element, ctx) {
81
+ for (const el of [element, ...element.querySelectorAll("*")]) {
82
+ for (const attr of [...el.attributes]) {
83
+ const val = attr.value;
84
+ if (!val.includes("{") && !val.includes(LBRACE_PH)) continue;
85
+ const raw = val
86
+ .replace(new RegExp(LBRACE_PH, "g"), "{")
87
+ .replace(new RegExp(RBRACE_PH, "g"), "}");
88
+ attr.value = raw.replace(/\{([^}]+)\}/g, (_, expr) => {
89
+ try {
90
+ return String(compileExpression(expr, true)(ctx));
91
+ } catch {
92
+ return "";
93
+ }
94
+ });
95
+ }
96
+ }
97
+ }
98
+
99
+ function processConditionals(element, ctx, chain) {
100
+ for (const child of [...element.children]) {
101
+ const hasIf = child.hasAttribute("if");
102
+ const hasDelIf = child.hasAttribute("del:if");
103
+ const hasElif = child.hasAttribute("elif");
104
+ const hasElse = child.hasAttribute("else");
105
+
106
+ if ((hasElif || hasElse) && chain.rendered) {
107
+ child.remove();
108
+ if (hasElse) chain.active = false;
109
+ continue;
110
+ }
111
+
112
+ applyConditionalToElement(child, ctx, chain, hasIf, hasDelIf, hasElif, hasElse);
113
+
114
+ if (child.parentNode) {
115
+ processConditionals(child, ctx, chain);
116
+ }
117
+ }
118
+ }
119
+
120
+ function processSlots(element, projectedChildren) {
121
+ const slots = element.querySelectorAll("slot");
122
+ for (const slot of slots) {
123
+ const clone = projectedChildren.map(c => c.cloneNode(true));
124
+ slot.replaceWith(...clone);
125
+ }
126
+ }
127
+
128
+ let bindCounter = 0;
129
+
130
+ function processBindAttributes(element) {
131
+ const bindings = [];
132
+ for (const attr of [...element.attributes]) {
133
+ if (attr.name.startsWith("bind:")) {
134
+ const prop = attr.name.slice(5);
135
+ const varName = attr.value;
136
+ const bindId = "b" + (bindCounter++);
137
+ element.setAttribute("data-chbind-" + bindId, "");
138
+ bindings.push({ prop, varName, bindId });
139
+ element.removeAttribute(attr.name);
140
+ }
141
+ }
142
+ return bindings;
143
+ }
144
+
145
+ function resolveDataBindings(element) {
146
+ const ids = [];
147
+ for (const attr of [...element.attributes]) {
148
+ const match = attr.name.match(/^data-chbind-(b\d+)$/);
149
+ if (match) {
150
+ ids.push(match[1]);
151
+ element.removeAttribute(attr.name);
152
+ }
153
+ }
154
+ return ids;
155
+ }
156
+
157
+ function collectBindings(root) {
158
+ const bindingDefs = [];
159
+ const idToEl = {};
160
+
161
+ for (const el of [root, ...root.querySelectorAll("*")]) {
162
+ bindingDefs.push(...processBindAttributes(el));
163
+ for (const id of resolveDataBindings(el)) {
164
+ idToEl[id] = el;
165
+ }
166
+ }
167
+ return { bindingDefs, idToEl };
168
+ }
169
+
170
+ class ChocolaComponent {
171
+ #template;
172
+ #hash;
173
+ #runtime;
174
+ #defaultProps;
175
+ #bakedComponent;
176
+ #ctx;
177
+ #eventListeners = [];
178
+ #children;
179
+ #childComponents;
180
+
181
+ constructor(config = {}) {
182
+ this.#template = config.template || "";
183
+ this.#hash = config.hash || "";
184
+ this.#runtime = config.runtime || null;
185
+ this.#defaultProps = config.props || {};
186
+ this.#childComponents = config.children || [];
187
+ }
188
+
189
+ #collectListeners(root, fn) {
190
+ const allEls = [root, ...root.querySelectorAll("*")];
191
+ const patches = [];
192
+
193
+ for (const el of allEls) {
194
+ const original = el.addEventListener;
195
+ el.addEventListener = (event, handler, options) => {
196
+ this.#eventListeners.push({ el, event, handler, options });
197
+ original.call(el, event, handler, options);
198
+ };
199
+ patches.push({ el, original });
200
+ }
201
+
202
+ try {
203
+ fn(root, this.#ctx);
204
+ } finally {
205
+ for (const { el, original } of patches) {
206
+ el.addEventListener = original;
207
+ }
208
+ }
209
+ }
210
+
211
+ #init(ctx) {
212
+ const mergedCtx = { ...this.#defaultProps, ...ctx };
213
+
214
+ const container = document.createElement("template");
215
+ container.innerHTML = this.#template;
216
+ const fragment = container.content;
217
+ const root = fragment.firstElementChild;
218
+
219
+ if (!root) {
220
+ console.error(`${this.#hash}: component template must have a single root element`);
221
+ return null;
222
+ }
223
+
224
+ processSlots(root, this.#children || []);
225
+ processConditionals(root, mergedCtx, { active: false, rendered: false });
226
+ interpolateAttributes(root, mergedCtx);
227
+ interpolateNode(root, mergedCtx);
228
+ root.classList.add(this.#hash);
229
+
230
+ const { bindingDefs, idToEl } = collectBindings(root);
231
+ this.#ctx = { ...mergedCtx };
232
+ for (const { prop, varName, bindId } of bindingDefs) {
233
+ const el = idToEl[bindId];
234
+ if (el) {
235
+ this.#ctx[varName] = prop === "self" ? el : el[prop];
236
+ }
237
+ }
238
+
239
+ if (this.#runtime) {
240
+ this.#collectListeners(root, (self, ctx) => {
241
+ this.#runtime(self, ctx);
242
+ });
243
+ }
244
+
245
+ this.#bakedComponent = root;
246
+ container.remove();
247
+ return root;
248
+ }
249
+
250
+ #childInstances = [];
251
+
252
+ #mountChildren(root) {
253
+ for (const { tag, compClass } of this.#childComponents) {
254
+ const els = root.querySelectorAll(tag);
255
+ for (const el of els) {
256
+ const props = {};
257
+ for (const attr of [...el.attributes]) {
258
+ if (attr.name.startsWith("data-ch") || attr.name === "if" || attr.name === "del:if" || attr.name === "elif" || attr.name === "else") continue;
259
+ props[attr.name] = attr.value;
260
+ }
261
+ const instance = new compClass();
262
+ instance.mount(el.parentNode, props);
263
+ this.#childInstances.push(instance);
264
+ el.remove();
265
+ }
266
+ }
267
+ }
268
+
269
+ mount(target, ctx = {}) {
270
+ if (target.nodeType !== 1) {
271
+ console.error(`${this.#hash}: mount target is not a valid DOM node`);
272
+ return;
273
+ }
274
+ this.#children = [...target.children];
275
+ this.#init(ctx);
276
+ if (this.#bakedComponent) {
277
+ target.appendChild(this.#bakedComponent);
278
+ this.#mountChildren(this.#bakedComponent);
279
+ }
280
+ }
281
+
282
+ remove() {
283
+ for (const { el, event, handler, options } of this.#eventListeners) {
284
+ el.removeEventListener(event, handler, options);
285
+ }
286
+ this.#eventListeners = [];
287
+ for (const child of this.#childInstances) {
288
+ child.remove();
289
+ }
290
+ this.#childInstances = [];
291
+ if (this.#bakedComponent) {
292
+ this.#bakedComponent.remove();
293
+ this.#bakedComponent = null;
294
+ }
295
+ }
296
+
297
+ update(ctx = {}) {
298
+ const mergedCtx = { ...this.#defaultProps, ...ctx };
299
+ const parent = this.#bakedComponent?.parentNode;
300
+ parent && this.remove();
301
+ this.#init(mergedCtx);
302
+ if (parent && this.#bakedComponent) {
303
+ parent.appendChild(this.#bakedComponent);
304
+ this.#mountChildren(this.#bakedComponent);
305
+ }
306
+ }
307
+ }
308
+
309
+ export { ChocolaComponent };
package/utils.js CHANGED
@@ -9,4 +9,19 @@ export async function getChocolaConfig(__rootdir) {
9
9
  } catch(err) {
10
10
  throwError("An error occurred while fetching the Chocola config file:\n" + err);
11
11
  }
12
+ }
13
+
14
+ const LBRACE_PH = "_%%CHOCOLA-LBRACE%%_";
15
+ const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
16
+
17
+ export function protectCurlyBraces(html) {
18
+ return html
19
+ .replace(/&(?:lbrace|#123|#x7B);/gi, LBRACE_PH)
20
+ .replace(/&(?:rcub|#125|#x7D);/gi, RBRACE_PH);
21
+ }
22
+
23
+ export function restoreCurlyBraces(html) {
24
+ return html
25
+ .replace(/_%%CHOCOLA-LBRACE%%_/g, "{")
26
+ .replace(/_%%CHOCOLA-RBRACE%%_/g, "}");
12
27
  }