chocola 2.0.0-next.6 → 2.0.0-next.8

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.
@@ -1,121 +1,120 @@
1
- import { throwError } from "../compiler/utils.js";
2
- import { compileExpression } from "./utils.js";
3
- import { hasDelIfAttr, getDelIfAttr, removeDelIfAttr } from "./context.js";
4
-
5
- export const reservedAttrs = ["if", "del:if", "elif", "else"];
6
-
7
- export function getLineNumber(sourceContent, parentContent, idxInParent) {
8
- if (sourceContent === parentContent) {
9
- return parentContent.substring(0, idxInParent).split("\n").length;
10
- }
11
- const templateStartMatch = sourceContent.match(/<template[^>]*>/);
12
- if (templateStartMatch) {
13
- const contentStart = templateStartMatch.index + templateStartMatch[0].length;
14
- const beforeContent = sourceContent.substring(0, contentStart);
15
- const linesInBefore = beforeContent.split("\n").length;
16
- const linesInTemplate = parentContent.substring(0, idxInParent).split("\n").length;
17
- return linesInBefore + linesInTemplate - 1;
18
- }
19
- return null;
20
- }
21
-
22
- export function validateChainStructure(parent, sourceFile, sourceContent, parentContent) {
23
- const children = [...parent.children];
24
- let chainActive = false;
25
-
26
- for (const child of children) {
27
- const hasIf = child.hasAttribute("if");
28
- const hasDelIf = hasDelIfAttr(child);
29
- const hasElif = child.hasAttribute("elif");
30
- const hasElse = child.hasAttribute("else");
31
-
32
- if (hasElif || hasElse) {
33
- if (!chainActive) {
34
- const tag = child.tagName.toLowerCase();
35
- const attr = hasElif ? "elif" : "else";
36
- let loc = sourceFile;
37
- if (sourceContent && parentContent) {
38
- const idx = parentContent.indexOf(child.outerHTML);
39
- if (idx !== -1) {
40
- const lineNum = getLineNumber(sourceContent, parentContent, idx);
41
- if (lineNum !== null) loc = `${sourceFile}:${lineNum}`;
42
- }
43
- }
44
- throwError(`${loc}\n <${tag}> has ${attr} without a preceding if/del-if sibling`);
45
- }
46
- if (hasElse) {
47
- chainActive = false;
48
- }
49
- }
50
-
51
- if (hasIf || hasDelIf) {
52
- chainActive = true;
53
- } else if (!hasElif && !hasElse) {
54
- chainActive = false;
55
- }
56
-
57
- validateChainStructure(child, sourceFile, sourceContent, parentContent);
58
- }
59
- }
60
-
61
- export function applyConditionalToElement(child, ctxProxy, condChain, hasIf, hasDelIf, hasElif, hasElse) {
62
- if (hasIf) {
63
- const expr = child.getAttribute("if").slice(1, -1);
64
- const fn = compileExpression(expr, true);
65
- condChain.active = true;
66
- if (fn(ctxProxy)) {
67
- condChain.rendered = true;
68
- } else {
69
- child.style.display = "none";
70
- condChain.rendered = false;
71
- }
72
- child.removeAttribute("if");
73
- } else if (hasDelIf) {
74
- const expr = getDelIfAttr(child).slice(1, -1);
75
- const fn = compileExpression(expr, true);
76
- condChain.active = true;
77
- if (fn(ctxProxy)) {
78
- condChain.rendered = true;
79
- } else {
80
- child.remove();
81
- condChain.rendered = false;
82
- }
83
- removeDelIfAttr(child);
84
- } else if (hasElif) {
85
- const expr = child.getAttribute("elif").slice(1, -1);
86
- const fn = compileExpression(expr, true);
87
- if (fn(ctxProxy)) {
88
- condChain.rendered = true;
89
- } else {
90
- child.remove();
91
- }
92
- child.removeAttribute("elif");
93
- } else if (hasElse) {
94
- condChain.rendered = true;
95
- condChain.active = false;
96
- child.removeAttribute("else");
97
- } else {
98
- condChain.active = false;
99
- condChain.rendered = false;
100
- }
101
- }
102
-
103
- export function interpolateNode(root, ctxProxy) {
104
- const stack = [root];
105
- while (stack.length) {
106
- const node = stack.pop();
107
- if (node.nodeType === 3) {
108
- node.textContent = node.textContent.replace(/\{([^}]+)\}/g, (_, expr) => {
109
- try {
110
- return compileExpression(expr, true)(ctxProxy);
111
- } catch {
112
- return "";
113
- }
114
- });
115
- } else {
116
- for (let i = node.childNodes.length - 1; i >= 0; i--) {
117
- stack.push(node.childNodes[i]);
118
- }
119
- }
120
- }
121
- }
1
+ import { compileExpression } from "./utils.js";
2
+ import { hasDelIfAttr, getDelIfAttr, removeDelIfAttr } from "./context.js";
3
+
4
+ export const reservedAttrs = ["if", "mount:if", "elif", "else"];
5
+
6
+ export function getLineNumber(sourceContent, parentContent, idxInParent) {
7
+ if (sourceContent === parentContent) {
8
+ return parentContent.substring(0, idxInParent).split("\n").length;
9
+ }
10
+ const templateStartMatch = sourceContent.match(/<template[^>]*>/);
11
+ if (templateStartMatch) {
12
+ const contentStart = templateStartMatch.index + templateStartMatch[0].length;
13
+ const beforeContent = sourceContent.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 = hasDelIfAttr(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/del-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 = compileExpression(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 = getDelIfAttr(child).slice(1, -1);
74
+ const fn = compileExpression(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
+ removeDelIfAttr(child);
83
+ } else if (hasElif) {
84
+ const expr = child.getAttribute("elif").slice(1, -1);
85
+ const fn = compileExpression(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 compileExpression(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
+ }
package/parser/utils.js CHANGED
@@ -1,13 +1,16 @@
1
- const compiledCache = new Map();
2
-
3
- export function compileExpression(expr, useCtx) {
4
- const key = useCtx ? `ctx:${expr}` : `raw:${expr}`;
5
- let fn = compiledCache.get(key);
6
- if (!fn) {
7
- fn = useCtx
8
- ? new Function("ctx", `with(ctx) { return (${expr}); }`)
9
- : new Function(`"use strict"; return (${expr})`);
10
- compiledCache.set(key, fn);
11
- }
12
- return fn;
13
- }
1
+ import { restoreTemplateChars } from "../utils.js";
2
+
3
+ const compiledCache = new Map();
4
+
5
+ export function compileExpression(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
+ }
@@ -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 compileExpression(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(compileExpression(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 applyConditionalToElement(child, ctx, chain, hasIf, hasDelIf, hasElif, hasElse) {
45
+ if (hasIf) {
46
+ const expr = child.getAttribute("if").slice(1, -1);
47
+ chain.active = true;
48
+ if (compileExpression(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 (hasDelIf) {
56
+ const expr = child.getAttribute("mount:if").slice(1, -1);
57
+ chain.active = true;
58
+ if (compileExpression(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 (compileExpression(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(compileExpression(expr, true)(ctx));
94
+ } catch {
95
+ return "";
96
+ }
97
+ });
98
+ }
99
+ }
100
+ }
101
+
102
+ function processConditionals(element, ctx, chain) {
103
+ for (const child of [...element.children]) {
104
+ const hasIf = child.hasAttribute("if");
105
+ const hasDelIf = 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
+ applyConditionalToElement(child, ctx, chain, hasIf, hasDelIf, hasElif, hasElse);
116
+
117
+ if (child.parentNode) {
118
+ processConditionals(child, ctx, chain);
119
+ }
120
+ }
121
+ }
122
+
123
+ function processSlots(element, projectedChildren) {
124
+ const slots = element.querySelectorAll("slot");
125
+ for (const slot of slots) {
126
+ const clone = projectedChildren.map(c => c.cloneNode(true));
127
+ slot.replaceWith(...clone);
128
+ }
129
+ }
130
+
131
+ let bindCounter = 0;
132
+
133
+ function processBindAttributes(element) {
134
+ const bindings = [];
135
+ for (const attr of [...element.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
+ element.setAttribute("data-chbind-" + bindId, "");
141
+ bindings.push({ prop, varName, bindId });
142
+ element.removeAttribute(attr.name);
143
+ }
144
+ }
145
+ return bindings;
146
+ }
147
+
148
+ function resolveDataBindings(element) {
149
+ const ids = [];
150
+ for (const attr of [...element.attributes]) {
151
+ const match = attr.name.match(/^data-chbind-(b\d+)$/);
152
+ if (match) {
153
+ ids.push(match[1]);
154
+ element.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(...processBindAttributes(el));
166
+ for (const id of resolveDataBindings(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
+ processConditionals(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 };
package/utils.js CHANGED
@@ -13,15 +13,40 @@ export async function getChocolaConfig(__rootdir) {
13
13
 
14
14
  const LBRACE_PH = "_%%CHOCOLA-LBRACE%%_";
15
15
  const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
16
+ const PROTECTED_REGEX = /_%%CHOCOLA-(?:AMP|LT|GT)\d+%%_/g;
17
+
18
+ let protectSeq = 0;
16
19
 
17
20
  export function protectCurlyBraces(html) {
18
21
  return html
19
22
  .replace(/&(?:lbrace|#123|#x7B);/gi, LBRACE_PH)
20
- .replace(/&(?:rcub|#125|#x7D);/gi, RBRACE_PH);
23
+ .replace(/&(?:rcub|#125|#x7D);/gi, RBRACE_PH)
24
+ .split(/(<script\b[^>]*>[\s\S]*?<\/script>|<style\b[^>]*>[\s\S]*?<\/style>)/gi)
25
+ .map((part, i) => i % 2 === 0
26
+ ? part.replace(/\{([^{}]*)\}/g, (m, inner) =>
27
+ `{${inner
28
+ .replace(/&/g, () => `_%%CHOCOLA-AMP${protectSeq++}%%_`)
29
+ .replace(/</g, () => `_%%CHOCOLA-LT${protectSeq++}%%_`)
30
+ .replace(/>/g, () => `_%%CHOCOLA-GT${protectSeq++}%%_`)}}`)
31
+ : part)
32
+ .join("");
21
33
  }
22
34
 
23
35
  export function restoreCurlyBraces(html) {
24
36
  return html
25
37
  .replace(/_%%CHOCOLA-LBRACE%%_/g, "{")
26
- .replace(/_%%CHOCOLA-RBRACE%%_/g, "}");
38
+ .replace(/_%%CHOCOLA-RBRACE%%_/g, "}")
39
+ .replace(PROTECTED_REGEX, char => {
40
+ if (char.includes("AMP")) return "&";
41
+ if (char.includes("LT")) return "<";
42
+ return ">";
43
+ });
44
+ }
45
+
46
+ export function restoreTemplateChars(str) {
47
+ return str.replace(PROTECTED_REGEX, char => {
48
+ if (char.includes("AMP")) return "&";
49
+ if (char.includes("LT")) return "<";
50
+ return ">";
51
+ });
27
52
  }