chocola 2.0.0-next.1 → 2.0.0-next.11

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