chocola 2.0.0-next.2 → 2.0.0-next.5

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,489 +1,667 @@
1
- import { JSDOM } from "jsdom";
2
- import { extractContextFromElement } from "./dom-processor.js";
3
- import { genRandomId, incrementAlfabet, throwError, protectCurlyBraces, compileExpression } from "./utils.js";
4
- import chalk from "chalk";
5
- import beautify from "js-beautify";
6
-
7
- function hasCombinator(sel) {
8
- let depth = 0;
9
- for (const ch of sel) {
10
- if (ch === '[') depth++;
11
- else if (ch === ']') depth--;
12
- else if (depth === 0 && (ch === ' ' || ch === '>' || ch === '+' || ch === '~')) return true;
13
- }
14
- return false;
15
- }
16
-
17
- function scopeSelector(sel, cssId) {
18
- const s = sel.trim();
19
- if (s.startsWith("." + cssId)) return s;
20
- if (hasCombinator(s)) return `.${cssId} ${s}`;
21
- return `.${cssId}${s}, .${cssId} ${s}`;
22
- }
23
-
24
- function scopeCss(cssString, cssId) {
25
- const keyframeRegex = /@(-webkit-)?keyframes\s+(\S+)/gi;
26
- const animNames = new Set();
27
- let match;
28
- while ((match = keyframeRegex.exec(cssString))) {
29
- animNames.add(match[2]);
30
- }
31
-
32
- if (animNames.size > 0) {
33
- const sorted = [...animNames].sort((a, b) => b.length - a.length);
34
- const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35
- for (const name of sorted) {
36
- cssString = cssString.replace(
37
- new RegExp(`(@(?:-webkit-)?keyframes\\s+)${esc(name)}`, "gi"),
38
- `$1${name}-${cssId}`
39
- );
40
- cssString = cssString.replace(
41
- new RegExp(`((?:animation|animation-name)\\s*:\\s*[^;{]*?)\\b${esc(name)}\\b`, "gi"),
42
- `$1${name}-${cssId}`
43
- );
44
- }
45
- }
46
-
47
- function findMatchingBrace(s, openIndex) {
48
- let depth = 0;
49
- for (let i = openIndex; i < s.length; i++) {
50
- const ch = s[i];
51
- if (ch === '{') depth++;
52
- else if (ch === '}') {
53
- depth--;
54
- if (depth === 0) return i;
55
- }
56
- }
57
- return s.length - 1;
58
- }
59
-
60
- const scopeInnerAtRules = new Set(["@media", "@supports", "@container", "@layer"]);
61
-
62
- function processBlock(str) {
63
- str = str.replaceAll(":root", `.${cssId}`);
64
-
65
- let out = "";
66
- let i = 0;
67
- while (i < str.length) {
68
- const braceIndex = str.indexOf('{', i);
69
- if (braceIndex === -1) {
70
- out += str.slice(i);
71
- break;
72
- }
73
- const header = str.substring(i, braceIndex);
74
- const endBrace = findMatchingBrace(str, braceIndex);
75
- const inner = str.substring(braceIndex + 1, endBrace);
76
-
77
- if (header.trim().startsWith("@")) {
78
- const atKeyword = header.trim().split(/\s+/)[0].toLowerCase();
79
- if (scopeInnerAtRules.has(atKeyword)) {
80
- out += header + "{" + processBlock(inner) + "}";
81
- } else {
82
- out += header + "{" + inner + "}";
83
- }
84
- } else {
85
- const innerScoped = processBlock(inner);
86
- const selectors = header
87
- .split(",")
88
- .map(s => s.trim())
89
- .filter(Boolean);
90
- const scopedHeader = selectors.length > 0
91
- ? selectors.map(sel => scopeSelector(sel, cssId)).join(", ")
92
- : header;
93
- out += scopedHeader + "{" + innerScoped + "}";
94
- }
95
-
96
- i = endBrace + 1;
97
- }
98
- return out;
99
- }
100
-
101
- return processBlock(cssString);
102
- }
103
-
104
- const warnedDeprecated = new Set();
105
- function hasDelIfAttr(el, sourceLoc) {
106
- if (el.hasAttribute("del-if") && !el.hasAttribute("del:if") && !warnedDeprecated.has(sourceLoc)) {
107
- warnedDeprecated.add(sourceLoc);
108
- const name = sourceLoc ? sourceLoc.split(/[\\/]/).pop() : "unknown";
109
- console.warn(chalk.yellow(`[deprecation] "del-if" is deprecated in ${chalk.cyan(name)} and will be removed in Chocola 2. Use "del:if" instead.`));
110
- }
111
- return el.hasAttribute("del-if") || el.hasAttribute("del:if");
112
- }
113
- function getDelIfAttr(el) {
114
- return el.getAttribute("del:if") ?? el.getAttribute("del-if");
115
- }
116
- function removeDelIfAttr(el) {
117
- el.removeAttribute("del-if");
118
- el.removeAttribute("del:if");
119
- }
120
-
121
- function interpolateNode(root, ctxProxy) {
122
- const stack = [root];
123
- while (stack.length) {
124
- const node = stack.pop();
125
- if (node.nodeType === 3) {
126
- node.textContent = node.textContent.replace(/\{([^}]+)\}/g, (_, expr) => {
127
- try {
128
- return compileExpression(expr, true)(ctxProxy);
129
- } catch {
130
- return "";
131
- }
132
- });
133
- } else {
134
- for (let i = node.childNodes.length - 1; i >= 0; i--) {
135
- stack.push(node.childNodes[i]);
136
- }
137
- }
138
- }
139
- }
140
-
141
- function validateChainStructure(parent, sourceFile, sourceContent, parentContent) {
142
- const children = [...parent.children];
143
- let chainActive = false;
144
-
145
- for (const child of children) {
146
- const hasIf = child.hasAttribute("if");
147
- const hasDelIf = hasDelIfAttr(child, sourceFile);
148
- const hasElif = child.hasAttribute("elif");
149
- const hasElse = child.hasAttribute("else");
150
-
151
- if (hasElif || hasElse) {
152
- if (!chainActive) {
153
- const tag = child.tagName.toLowerCase();
154
- const attr = hasElif ? "elif" : "else";
155
- let loc = sourceFile;
156
- if (sourceContent && sourceContent === parentContent) {
157
- const idx = parentContent.indexOf(child.outerHTML);
158
- if (idx !== -1) {
159
- loc = `${sourceFile}:${parentContent.substring(0, idx).split("\n").length}`;
160
- }
161
- }
162
- throwError(`${loc}\n <${tag}> has ${attr} without a preceding if/del-if sibling`);
163
- }
164
- if (hasElse) {
165
- chainActive = false;
166
- }
167
- }
168
-
169
- if (hasIf || hasDelIf) {
170
- chainActive = true;
171
- } else if (!hasElif && !hasElse) {
172
- chainActive = false;
173
- }
174
-
175
- validateChainStructure(child, sourceFile, sourceContent, parentContent);
176
- }
177
- }
178
-
179
- /**
180
- * Processes a single component element and inserts it into the DOM
181
- * @param {Element} element
182
- * @param {Map} loadedComponents
183
- * @param {Array} runtimeChunks
184
- * @param {Array} compIdColl
185
- * @param {object} letterState - { value: string }
186
- * @returns {boolean} - true if component was processed, false if not found
187
- */
188
- export function processComponentElement(
189
- element,
190
- loadedComponents,
191
- runtimeChunks,
192
- compIdColl,
193
- letterState,
194
- runtimeMap,
195
- cssScopes,
196
- cssScopesMap,
197
- scopedStyles,
198
- renderChain = [],
199
- staticCtxRegistry,
200
- sourceFile,
201
- sourceContent
202
- ) {
203
- const tagName = element.tagName.toLowerCase();
204
- const compName = tagName + ".js";
205
- const instance = loadedComponents.get(compName);
206
-
207
- if (!instance || instance === undefined) return false;
208
- if (renderChain && renderChain.includes(compName)) return false;
209
-
210
- let ctx;
211
- if (staticCtxRegistry && staticCtxRegistry.has(element)) {
212
- ctx = staticCtxRegistry.get(element);
213
- } else {
214
- ctx = extractContextFromElement(element);
215
- staticCtxRegistry && staticCtxRegistry.set(element, ctx);
216
- }
217
- const srcInnerHtml = element.innerHTML;
218
-
219
- const ctxProxy = new Proxy(ctx, {
220
- has() { return true; },
221
- get(target, key) { return target[key]; }
222
- });
223
-
224
- if (instance.body) {
225
- let body = protectCurlyBraces(instance.body);
226
-
227
- const fragment = JSDOM.fragment(body);
228
-
229
- const slotFragment = JSDOM.fragment(srcInnerHtml);
230
- if (sourceFile) {
231
- validateChainStructure(slotFragment, sourceFile, sourceContent, srcInnerHtml);
232
- }
233
- validateChainStructure(fragment, instance.__sourceFile || compName, body, body);
234
- Array.from(fragment.querySelectorAll("slot")).forEach(slot => {
235
- slot.replaceWith(slotFragment);
236
- });
237
-
238
- const childEntries = Array.from(fragment.querySelectorAll("*")).map(el => ({
239
- el,
240
- parent: el.parentNode
241
- }));
242
- const condChains = new Map();
243
-
244
- childEntries.forEach(({ el: child, parent }) => {
245
- if (!condChains.has(parent)) {
246
- condChains.set(parent, { active: false, rendered: false });
247
- }
248
- const condChain = condChains.get(parent);
249
-
250
- const hasIf = child.hasAttribute("if");
251
- const hasDelIf = hasDelIfAttr(child, instance.__sourceFile || compName);
252
- const hasElif = child.hasAttribute("elif");
253
- const hasElse = child.hasAttribute("else");
254
-
255
- if (hasElif || hasElse) {
256
- if (!condChain.active) {
257
- throwError(`${instance.__sourceFile || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/del-if sibling`);
258
- }
259
- if (condChain.rendered) {
260
- child.remove();
261
- if (hasElse) {
262
- condChain.active = false;
263
- }
264
- return;
265
- }
266
- }
267
-
268
- if (child.tagName.toLowerCase() === "void") {
269
- if (hasElif || hasElse) {
270
- if (hasElif) {
271
- const expr = child.getAttribute("elif").slice(1, -1);
272
- const fn = compileExpression(expr, true);
273
- if (!fn(ctxProxy)) {
274
- child.remove();
275
- return;
276
- }
277
- }
278
- child.replaceWith(...child.children);
279
- condChain.rendered = true;
280
- if (hasElse) {
281
- condChain.active = false;
282
- }
283
- } else if (hasIf || hasDelIf) {
284
- const raw = hasIf ? child.getAttribute("if") : getDelIfAttr(child);
285
- const expr = raw.slice(1, -1);
286
- const fn = compileExpression(expr, true);
287
- condChain.active = true;
288
- if (fn(ctxProxy)) {
289
- child.replaceWith(...child.children);
290
- condChain.rendered = true;
291
- } else {
292
- child.remove();
293
- condChain.rendered = false;
294
- }
295
- } else {
296
- child.replaceWith(...child.children);
297
- condChain.active = false;
298
- condChain.rendered = false;
299
- }
300
- return;
301
- }
302
-
303
- const reservedAttrs = ["if", "del-if", "del:if", "elif", "else"];
304
-
305
- Array.from(child.attributes).forEach(attribute => {
306
- if (!attribute || attribute === undefined) return;
307
- if (reservedAttrs.includes(attribute.name)) return;
308
- attribute.value = attribute.value.replace(
309
- /\{([^}]+)\}/g,
310
- (_, expr) => {
311
- try {
312
- return compileExpression(expr, true)(ctxProxy);
313
- } catch {
314
- return "";
315
- }
316
- }
317
- );
318
- });
319
-
320
- processComponentElement(
321
- child,
322
- loadedComponents,
323
- runtimeChunks,
324
- compIdColl,
325
- letterState,
326
- runtimeMap,
327
- cssScopes,
328
- cssScopesMap,
329
- scopedStyles,
330
- renderChain.concat(compName),
331
- staticCtxRegistry,
332
- instance.__sourceFile || compName,
333
- body
334
- );
335
-
336
- if (hasIf) {
337
- const expr = child.getAttribute("if").slice(1, -1);
338
- const fn = compileExpression(expr, true);
339
- condChain.active = true;
340
- if (fn(ctxProxy)) {
341
- condChain.rendered = true;
342
- } else {
343
- child.style.display = "none";
344
- condChain.rendered = false;
345
- }
346
- child.removeAttribute("if");
347
- } else if (hasDelIf) {
348
- const expr = getDelIfAttr(child).slice(1, -1);
349
- const fn = compileExpression(expr, true);
350
- condChain.active = true;
351
- if (fn(ctxProxy)) {
352
- condChain.rendered = true;
353
- } else {
354
- child.remove();
355
- condChain.rendered = false;
356
- }
357
- removeDelIfAttr(child);
358
- } else if (hasElif) {
359
- const expr = child.getAttribute("elif").slice(1, -1);
360
- const fn = compileExpression(expr, true);
361
- if (fn(ctxProxy)) {
362
- condChain.rendered = true;
363
- } else {
364
- child.remove();
365
- }
366
- child.removeAttribute("elif");
367
- } else if (hasElse) {
368
- condChain.rendered = true;
369
- condChain.active = false;
370
- child.removeAttribute("else");
371
- } else {
372
- condChain.active = false;
373
- condChain.rendered = false;
374
- }
375
-
376
- interpolateNode(child, ctxProxy)
377
- });
378
-
379
- const firstChild = fragment.firstChild;
380
-
381
- if (firstChild && firstChild.nodeType === 1) {
382
- if (instance.script || instance.effects) {
383
- const compId = "chid-" + genRandomId(compIdColl);
384
- firstChild.setAttribute("chid", compId);
385
-
386
- let script = instance.script && instance.script.toString();
387
-
388
- const ctxRegex = /ctx\s*=\s*({.*?})/;
389
- const ctxMatch = script.match(ctxRegex);
390
- let runtimeCtx = {};
391
- if (ctxMatch) {
392
- try {
393
- runtimeCtx = JSON.parse(ctxMatch[1].replace(/(\w+):/g, '"$1":'));
394
- } catch (e) {
395
- runtimeCtx = {};
396
- }
397
- }
398
- let ctxDef = "";
399
- for (const [key, value] of Object.entries(runtimeCtx)) {
400
- ctxDef += `ctx.${key} = ctx.${key}||${JSON.stringify(value)};\n`;
401
- }
402
- script = script.replace(ctxRegex, "ctx");
403
- script = script.replace(/RUNTIME\([^)]*\)\s*{/, match => match + "\n" + ctxDef);
404
-
405
- let letterEntry = runtimeMap && runtimeMap.get(compName);
406
- let letter;
407
- if (!letterEntry) {
408
- letter = getNextLetter(letterState);
409
- script = script.replace(/RUNTIME/g, `${letter}RUNTIME`);
410
- runtimeChunks.push(script);
411
- runtimeMap && runtimeMap.set(compName, { letter });
412
- } else {
413
- letter = letterEntry.letter;
414
- }
415
-
416
- runtimeChunks.push(`${letter}RUNTIME(document.querySelector('[chid="${compId}"]'), ${JSON.stringify(ctx)});`);
417
- }
418
- }
419
-
420
- let style = instance.styles && instance.styles.toString();
421
- if (style) {
422
- let cssId = cssScopesMap && cssScopesMap.get(compName);
423
- if (!cssId) {
424
- cssId = genRandomId(cssScopes, 8, true);
425
- cssScopesMap.set(compName, cssId);
426
- }
427
- if (fragment.firstChild && fragment.firstChild.nodeType === 1) {
428
- fragment.firstChild.classList.add(cssId);
429
- }
430
- style = scopeCss(style, cssId);
431
- scopedStyles.push(style);
432
- }
433
-
434
- if (fragment.firstChild && fragment.firstChild.nodeType === 1) {
435
- fragment.firstChild.setAttribute("data-ch-source", instance.__sourceFile || compName);
436
- }
437
-
438
- element.replaceWith(fragment);
439
- return true;
440
- }
441
-
442
- console.warn(chalk.yellow(`${compName} — component could not be loaded (missing body or styles export)`));
443
- return false;
444
- }
445
-
446
- /**
447
- * Processes all components in the app container
448
- * @param {Element[]} appElements
449
- * @param {Map} loadedComponents
450
- * @returns {{
451
- * runtimeScript: string,
452
- * hasComponents: boolean
453
- * scopesCss: CSSString
454
- * }}
455
- */
456
- export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent) {
457
- let runtimeChunks = [];
458
- let compIdColl = [];
459
- let letterState = { value: null };
460
- let runtimeMap = new Map();
461
- let cssScopes = [];
462
- let cssScopesMap = new Map();
463
- let scopedStyles = [];
464
- let staticCtxRegistry = new Map();
465
-
466
- appElements.forEach(el => {
467
- if (!el.isConnected) return;
468
- processComponentElement(el, loadedComponents, runtimeChunks, compIdColl, letterState, runtimeMap, cssScopes, cssScopesMap, scopedStyles, [], staticCtxRegistry, pageSourceFile, pageSourceContent);
469
- });
470
- const runtimeScript = runtimeChunks.join("\n");
471
- const hasComponents = runtimeChunks.length > 0;
472
- const scopesCss = beautify.css(scopedStyles.join("\n"));
473
-
474
- return { runtimeScript, hasComponents, scopesCss };
475
- }
476
-
477
- /**
478
- * Gets the next letter in sequence or starts with 'a'
479
- * @param {object} letterState - { value: string }
480
- * @returns {string}
481
- */
482
- function getNextLetter(letterState) {
483
- if (!letterState.value) {
484
- letterState.value = "a";
485
- } else {
486
- letterState.value = incrementAlfabet(letterState.value);
487
- }
488
- return letterState.value;
489
- }
1
+ import { JSDOM } from "jsdom";
2
+ import { extractContextFromElement } from "./dom-processor.js";
3
+ import { genRandomId, incrementAlfabet, throwError, protectCurlyBraces, compileExpression, hasDelIfAttr, getDelIfAttr, removeDelIfAttr } from "./utils.js";
4
+ import chalk from "./chalk.js";
5
+
6
+ function hasCombinator(sel) {
7
+ let depth = 0;
8
+ for (const ch of sel) {
9
+ if (ch === '[') depth++;
10
+ else if (ch === ']') depth--;
11
+ else if (depth === 0 && (ch === ' ' || ch === '>' || ch === '+' || ch === '~')) return true;
12
+ }
13
+ return false;
14
+ }
15
+
16
+ function scopeSelector(sel, cssId) {
17
+ const s = sel.trim();
18
+ if (s.startsWith("." + cssId)) return s;
19
+ if (hasCombinator(s)) return `.${cssId} ${s}`;
20
+ return `.${cssId}${s}, .${cssId} ${s}`;
21
+ }
22
+
23
+ function scopeCss(cssString, cssId) {
24
+ const keyframeRegex = /@(-webkit-)?keyframes\s+(\S+)/gi;
25
+ const animNames = new Set();
26
+ let match;
27
+ while ((match = keyframeRegex.exec(cssString))) {
28
+ animNames.add(match[2]);
29
+ }
30
+
31
+ if (animNames.size > 0) {
32
+ const sorted = [...animNames].sort((a, b) => b.length - a.length);
33
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34
+ for (const name of sorted) {
35
+ cssString = cssString.replace(
36
+ new RegExp(`(@(?:-webkit-)?keyframes\\s+)${esc(name)}`, "gi"),
37
+ `$1${name}-${cssId}`
38
+ );
39
+ cssString = cssString.replace(
40
+ new RegExp(`((?:animation|animation-name)\\s*:\\s*[^;{]*?)\\b${esc(name)}\\b`, "gi"),
41
+ `$1${name}-${cssId}`
42
+ );
43
+ }
44
+ }
45
+
46
+ function findMatchingBrace(s, openIndex) {
47
+ let depth = 0;
48
+ for (let i = openIndex; i < s.length; i++) {
49
+ const ch = s[i];
50
+ if (ch === '{') depth++;
51
+ else if (ch === '}') {
52
+ depth--;
53
+ if (depth === 0) return i;
54
+ }
55
+ }
56
+ return s.length - 1;
57
+ }
58
+
59
+ const scopeInnerAtRules = new Set(["@media", "@supports", "@container", "@layer"]);
60
+
61
+ function processBlock(str) {
62
+ str = str.replaceAll(":root", `.${cssId}`);
63
+
64
+ let out = "";
65
+ let i = 0;
66
+ while (i < str.length) {
67
+ const braceIndex = str.indexOf('{', i);
68
+ if (braceIndex === -1) {
69
+ out += str.slice(i);
70
+ break;
71
+ }
72
+ const header = str.substring(i, braceIndex);
73
+ const endBrace = findMatchingBrace(str, braceIndex);
74
+ const inner = str.substring(braceIndex + 1, endBrace);
75
+
76
+ if (header.trim().startsWith("@")) {
77
+ const atKeyword = header.trim().split(/\s+/)[0].toLowerCase();
78
+ if (scopeInnerAtRules.has(atKeyword)) {
79
+ out += header + "{" + processBlock(inner) + "}";
80
+ } else {
81
+ out += header + "{" + inner + "}";
82
+ }
83
+ } else {
84
+ const innerScoped = processBlock(inner);
85
+ const selectors = header
86
+ .split(",")
87
+ .map(s => s.trim())
88
+ .filter(Boolean);
89
+ const scopedHeader = selectors.length > 0
90
+ ? selectors.map(sel => scopeSelector(sel, cssId)).join(", ")
91
+ : header;
92
+ out += scopedHeader + "{" + innerScoped + "}";
93
+ }
94
+
95
+ i = endBrace + 1;
96
+ }
97
+ return out;
98
+ }
99
+
100
+ return processBlock(cssString);
101
+ }
102
+
103
+ function extractPropsDefaults(script) {
104
+ if (!script) return [];
105
+ const propsRegex = /export\s+let\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*(?:=\s*([^;]+))?;/g;
106
+ let props = [];
107
+ let match;
108
+ while ((match = propsRegex.exec(script)) !== null) {
109
+ props.push({ name: match[1].trim(), defaultValue: match[2]?.trim() });
110
+ }
111
+ return props;
112
+ }
113
+
114
+ function interpolateNode(root, ctxProxy) {
115
+ const stack = [root];
116
+ while (stack.length) {
117
+ const node = stack.pop();
118
+ if (node.nodeType === 3) {
119
+ node.textContent = node.textContent.replace(/\{([^}]+)\}/g, (_, expr) => {
120
+ try {
121
+ return compileExpression(expr, true)(ctxProxy);
122
+ } catch {
123
+ return "";
124
+ }
125
+ });
126
+ } else {
127
+ for (let i = node.childNodes.length - 1; i >= 0; i--) {
128
+ stack.push(node.childNodes[i]);
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ function getLineNumber(sourceContent, parentContent, idxInParent) {
135
+ if (sourceContent === parentContent) {
136
+ return parentContent.substring(0, idxInParent).split("\n").length;
137
+ }
138
+ const templateStartMatch = sourceContent.match(/<template[^>]*>/);
139
+ if (templateStartMatch) {
140
+ const contentStart = templateStartMatch.index + templateStartMatch[0].length;
141
+ const beforeContent = sourceContent.substring(0, contentStart);
142
+ const linesInBefore = beforeContent.split("\n").length;
143
+ const linesInTemplate = parentContent.substring(0, idxInParent).split("\n").length;
144
+ return linesInBefore + linesInTemplate - 1;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ function validateChainStructure(parent, sourceFile, sourceContent, parentContent) {
150
+ const children = [...parent.children];
151
+ let chainActive = false;
152
+
153
+ for (const child of children) {
154
+ const hasIf = child.hasAttribute("if");
155
+ const hasDelIf = hasDelIfAttr(child);
156
+ const hasElif = child.hasAttribute("elif");
157
+ const hasElse = child.hasAttribute("else");
158
+
159
+ if (hasElif || hasElse) {
160
+ if (!chainActive) {
161
+ const tag = child.tagName.toLowerCase();
162
+ const attr = hasElif ? "elif" : "else";
163
+ let loc = sourceFile;
164
+ if (sourceContent && parentContent) {
165
+ const idx = parentContent.indexOf(child.outerHTML);
166
+ if (idx !== -1) {
167
+ const lineNum = getLineNumber(sourceContent, parentContent, idx);
168
+ if (lineNum !== null) loc = `${sourceFile}:${lineNum}`;
169
+ }
170
+ }
171
+ throwError(`${loc}\n <${tag}> has ${attr} without a preceding if/del-if sibling`);
172
+ }
173
+ if (hasElse) {
174
+ chainActive = false;
175
+ }
176
+ }
177
+
178
+ if (hasIf || hasDelIf) {
179
+ chainActive = true;
180
+ } else if (!hasElif && !hasElse) {
181
+ chainActive = false;
182
+ }
183
+
184
+ validateChainStructure(child, sourceFile, sourceContent, parentContent);
185
+ }
186
+ }
187
+
188
+ class ProcessContext {
189
+ constructor(loadedComponents, runtimeChunks, compIdColl, letterState, runtimeMap, cssScopes, cssScopesMap, scopedStyles, staticCtxRegistry) {
190
+ this.loadedComponents = loadedComponents;
191
+ this.runtimeChunks = runtimeChunks;
192
+ this.compIdColl = compIdColl;
193
+ this.letterState = letterState;
194
+ this.runtimeMap = runtimeMap;
195
+ this.cssScopes = cssScopes;
196
+ this.cssScopesMap = cssScopesMap;
197
+ this.scopedStyles = scopedStyles;
198
+ this.staticCtxRegistry = staticCtxRegistry;
199
+ }
200
+ }
201
+
202
+ const reservedAttrs = ["if", "del:if", "elif", "else"];
203
+
204
+ function extractTopLevelFunctions(script, excludeName) {
205
+ const funcs = [];
206
+ let i = 0;
207
+ let depth = 0;
208
+ let inStr = false;
209
+ let strChar = null;
210
+
211
+ while (i < script.length) {
212
+ const ch = script[i];
213
+
214
+ if (inStr) {
215
+ if (ch === '\\') i++;
216
+ else if (ch === strChar) inStr = false;
217
+ i++;
218
+ continue;
219
+ }
220
+
221
+ if (ch === '"' || ch === "'" || ch === '`') {
222
+ inStr = true;
223
+ strChar = ch;
224
+ i++;
225
+ continue;
226
+ }
227
+
228
+ if (ch === '{') { depth++; i++; continue; }
229
+ if (ch === '}') { depth--; i++; continue; }
230
+
231
+ if (depth === 0) {
232
+ const rest = script.slice(i);
233
+ const m = rest.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*\(/);
234
+ if (m) {
235
+ const name = m[1];
236
+ const pastParen = i + m[0].length;
237
+
238
+ let parenDepth = 1;
239
+ let p = pastParen;
240
+ while (p < script.length && parenDepth > 0) {
241
+ if (script[p] === '(') parenDepth++;
242
+ if (script[p] === ')') parenDepth--;
243
+ p++;
244
+ }
245
+
246
+ while (p < script.length && script[p] !== '{') p++;
247
+ if (p >= script.length) { i++; continue; }
248
+
249
+ let bDepth = 1;
250
+ let bodyStr = false;
251
+ let bodyStrChar = null;
252
+ let end = p + 1;
253
+ while (end < script.length && bDepth > 0) {
254
+ const c = script[end];
255
+ if (bodyStr) {
256
+ if (c === '\\') end++;
257
+ else if (c === bodyStrChar) bodyStr = false;
258
+ end++;
259
+ continue;
260
+ }
261
+ if (c === '"' || c === "'" || c === '`') {
262
+ bodyStr = true;
263
+ bodyStrChar = c;
264
+ end++;
265
+ continue;
266
+ }
267
+ if (c === '{') bDepth++;
268
+ if (c === '}') bDepth--;
269
+ end++;
270
+ }
271
+
272
+ if (name !== excludeName) {
273
+ funcs.push(script.slice(i, end));
274
+ }
275
+ i = end;
276
+ continue;
277
+ }
278
+ }
279
+ i++;
280
+ }
281
+
282
+ return funcs;
283
+ }
284
+
285
+ function extractRuntime(script, compName) {
286
+ const startRegex = /(?:async\s+)?function\s+\$runtime\(([^)]*)\)\s*\{/;
287
+ const match = script.match(startRegex);
288
+ if (!match) return null;
289
+
290
+ const startIndexBrace = match.index + match[0].length - 1;
291
+ let bracesCount = 0;
292
+ let inString = false;
293
+ let stringChar = null;
294
+
295
+ for (let i = startIndexBrace; i < script.length; i++) {
296
+ const ch = script[i];
297
+
298
+ if (inString) {
299
+ if (ch === '\\') { i++; continue; }
300
+ if (ch === stringChar) inString = false;
301
+ continue;
302
+ }
303
+
304
+ if (ch === "'" || ch === '"' || ch === '`') {
305
+ inString = true;
306
+ stringChar = ch;
307
+ continue;
308
+ }
309
+
310
+ if (ch === '{') {
311
+ bracesCount++;
312
+ } else if (ch === '}') {
313
+ bracesCount--;
314
+ if (bracesCount === 0) {
315
+ return script.substring(match.index, i + 1);
316
+ }
317
+ }
318
+ }
319
+
320
+ throw new Error(`${compName} $runtime function has unclosed curly braces.`);
321
+ }
322
+
323
+ function applyConditionalToElement(child, ctxProxy, condChain, hasIf, hasDelIf, hasElif, hasElse) {
324
+ if (hasIf) {
325
+ const expr = child.getAttribute("if").slice(1, -1);
326
+ const fn = compileExpression(expr, true);
327
+ condChain.active = true;
328
+ if (fn(ctxProxy)) {
329
+ condChain.rendered = true;
330
+ } else {
331
+ child.style.display = "none";
332
+ condChain.rendered = false;
333
+ }
334
+ child.removeAttribute("if");
335
+ } else if (hasDelIf) {
336
+ const expr = getDelIfAttr(child).slice(1, -1);
337
+ const fn = compileExpression(expr, true);
338
+ condChain.active = true;
339
+ if (fn(ctxProxy)) {
340
+ condChain.rendered = true;
341
+ } else {
342
+ child.remove();
343
+ condChain.rendered = false;
344
+ }
345
+ removeDelIfAttr(child);
346
+ } else if (hasElif) {
347
+ const expr = child.getAttribute("elif").slice(1, -1);
348
+ const fn = compileExpression(expr, true);
349
+ if (fn(ctxProxy)) {
350
+ condChain.rendered = true;
351
+ } else {
352
+ child.remove();
353
+ }
354
+ child.removeAttribute("elif");
355
+ } else if (hasElse) {
356
+ condChain.rendered = true;
357
+ condChain.active = false;
358
+ child.removeAttribute("else");
359
+ } else {
360
+ condChain.active = false;
361
+ condChain.rendered = false;
362
+ }
363
+ }
364
+
365
+ export function processComponentElement(
366
+ element,
367
+ cx,
368
+ renderChain = [],
369
+ sourceFile,
370
+ sourceContent
371
+ ) {
372
+ const tagName = element.tagName.toLowerCase();
373
+ const compName = tagName + ".html";
374
+ let instance = cx.loadedComponents.get(compName);
375
+
376
+ if (!instance || instance === undefined) return false;
377
+ if (renderChain && renderChain.includes(compName)) return false;
378
+
379
+ instance = protectCurlyBraces(instance);
380
+ const dom = new JSDOM(instance);
381
+ const doc = dom.window.document;
382
+ let script = doc.querySelector("script")?.innerHTML;
383
+ let template = doc.querySelector("template")?.innerHTML;
384
+ let styles = doc.querySelector("style")?.innerHTML;
385
+
386
+ if (!template) {
387
+ console.warn(chalk.yellow(`${compName} — component is missing a <template>`));
388
+ return false;
389
+ }
390
+
391
+ const compProps = extractPropsDefaults(script);
392
+
393
+ let ctx;
394
+ if (cx.staticCtxRegistry && cx.staticCtxRegistry.has(element)) {
395
+ ctx = cx.staticCtxRegistry.get(element);
396
+ } else {
397
+ ctx = extractContextFromElement(element);
398
+ cx.staticCtxRegistry && cx.staticCtxRegistry.set(element, ctx);
399
+ }
400
+
401
+ if (compProps.length > 0) {
402
+ compProps.forEach(({ name, defaultValue }) => {
403
+ if (defaultValue !== undefined && !(name in ctx)) {
404
+ try {
405
+ ctx[name] = compileExpression(defaultValue, false)();
406
+ } catch {
407
+ ctx[name] = defaultValue;
408
+ }
409
+ }
410
+ });
411
+ }
412
+
413
+ const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
414
+ for (const src of topFuncSrc) {
415
+ try {
416
+ const fn = (0, eval)("(" + src + ")");
417
+ const name = fn.name;
418
+ if (name && !(name in ctx)) ctx[name] = fn;
419
+ } catch {}
420
+ }
421
+
422
+ const elInnerHtml = element.innerHTML;
423
+
424
+ const ctxProxy = new Proxy(ctx, {
425
+ has() { return true; },
426
+ get(target, key) { return target[key]; }
427
+ });
428
+
429
+ const fragment = JSDOM.fragment(template);
430
+
431
+ const slotFragment = JSDOM.fragment(elInnerHtml);
432
+ if (sourceFile) {
433
+ validateChainStructure(slotFragment, sourceFile, sourceContent, elInnerHtml);
434
+ }
435
+ validateChainStructure(fragment, instance.__sourceFile || compName, instance, template);
436
+ Array.from(fragment.querySelectorAll("slot")).forEach(slot => {
437
+ slot.replaceWith(slotFragment);
438
+ });
439
+
440
+ const childEntries = Array.from(fragment.querySelectorAll("*")).map(el => ({
441
+ el,
442
+ parent: el.parentNode
443
+ }));
444
+ const condChains = new Map();
445
+ const bindings = [];
446
+ const elBindIds = new Map();
447
+ let bindCounter = 0;
448
+
449
+ childEntries.forEach(({ el: child, parent }) => {
450
+ if (!condChains.has(parent)) {
451
+ condChains.set(parent, { active: false, rendered: false });
452
+ }
453
+ const condChain = condChains.get(parent);
454
+
455
+ const hasIf = child.hasAttribute("if");
456
+ const hasDelIf = hasDelIfAttr(child);
457
+ const hasElif = child.hasAttribute("elif");
458
+ const hasElse = child.hasAttribute("else");
459
+
460
+ if (hasElif || hasElse) {
461
+ if (!condChain.active) {
462
+ throwError(`${instance.__sourceFile || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/del-if sibling`);
463
+ }
464
+ if (condChain.rendered) {
465
+ child.remove();
466
+ if (hasElse) {
467
+ condChain.active = false;
468
+ }
469
+ return;
470
+ }
471
+ }
472
+
473
+ if (child.tagName.toLowerCase() === "void") {
474
+ if (hasElif || hasElse) {
475
+ if (hasElif) {
476
+ const expr = child.getAttribute("elif").slice(1, -1);
477
+ const fn = compileExpression(expr, true);
478
+ if (!fn(ctxProxy)) {
479
+ child.remove();
480
+ return;
481
+ }
482
+ }
483
+ child.replaceWith(...child.children);
484
+ condChain.rendered = true;
485
+ if (hasElse) {
486
+ condChain.active = false;
487
+ }
488
+ } else if (hasIf || hasDelIf) {
489
+ const raw = hasIf ? child.getAttribute("if") : getDelIfAttr(child);
490
+ const expr = raw.slice(1, -1);
491
+ const fn = compileExpression(expr, true);
492
+ condChain.active = true;
493
+ if (fn(ctxProxy)) {
494
+ child.replaceWith(...child.children);
495
+ condChain.rendered = true;
496
+ } else {
497
+ child.remove();
498
+ condChain.rendered = false;
499
+ }
500
+ } else {
501
+ child.replaceWith(...child.children);
502
+ condChain.active = false;
503
+ condChain.rendered = false;
504
+ }
505
+ return;
506
+ }
507
+
508
+ Array.from(child.attributes).forEach(attribute => {
509
+ if (!attribute || attribute === undefined) return;
510
+ if (reservedAttrs.includes(attribute.name)) return;
511
+
512
+ if (attribute.name.startsWith("bind:")) {
513
+ const prop = attribute.name.slice(5);
514
+ const varName = attribute.value;
515
+ let bindId = elBindIds.get(child);
516
+ if (!bindId) {
517
+ bindId = "b" + (bindCounter++);
518
+ elBindIds.set(child, bindId);
519
+ child.setAttribute("data-chbind-" + bindId, "");
520
+ }
521
+ bindings.push({ prop, varName, bindId });
522
+ child.removeAttribute(attribute.name);
523
+ return;
524
+ }
525
+
526
+ attribute.value = attribute.value.replace(
527
+ /\{([^}]+)\}/g,
528
+ (_, expr) => {
529
+ try {
530
+ return compileExpression(expr, true)(ctxProxy);
531
+ } catch {
532
+ return "";
533
+ }
534
+ }
535
+ );
536
+ });
537
+
538
+ processComponentElement(
539
+ child,
540
+ cx,
541
+ renderChain.concat(compName),
542
+ compName,
543
+ template
544
+ );
545
+
546
+ applyConditionalToElement(child, ctxProxy, condChain, hasIf, hasDelIf, hasElif, hasElse);
547
+
548
+ interpolateNode(child, ctxProxy)
549
+ });
550
+
551
+ const firstChild = fragment.children[0];
552
+
553
+ if (firstChild && firstChild.nodeType === 1) {
554
+ if (script) {
555
+ const compId = "chid-" + genRandomId(cx.compIdColl);
556
+ firstChild.setAttribute("chid", compId);
557
+
558
+ const ctxRegex = /ctx\s*=\s*({.*?})/;
559
+ const ctxMatch = script.match(ctxRegex);
560
+ let runtimeCtx = {};
561
+ if (ctxMatch) {
562
+ try {
563
+ runtimeCtx = JSON.parse(ctxMatch[1].replace(/(\w+):/g, '"$1":'));
564
+ } catch (e) {
565
+ runtimeCtx = {};
566
+ }
567
+ }
568
+ const ctxDefParts = [];
569
+ const declared = new Set();
570
+ for (const [key, value] of Object.entries(runtimeCtx)) {
571
+ ctxDefParts.push(`let ${key} = ctx.${key}||${JSON.stringify(value)};\n`);
572
+ declared.add(key);
573
+ }
574
+
575
+ for (const { name, defaultValue } of compProps) {
576
+ if (declared.has(name)) continue;
577
+ if (defaultValue !== undefined) {
578
+ ctxDefParts.push(`let ${name} = ctx.${name}||${defaultValue};\n`);
579
+ } else {
580
+ ctxDefParts.push(`let ${name} = ctx.${name};\n`);
581
+ }
582
+ }
583
+
584
+ const ctxDef = ctxDefParts.join("");
585
+
586
+ script = script.replace(ctxRegex, "ctx");
587
+
588
+ let runtime = extractRuntime(script, compName);
589
+
590
+ if (runtime) {
591
+ let letterEntry = cx.runtimeMap && cx.runtimeMap.get(compName);
592
+ let letter;
593
+ if (!letterEntry) {
594
+ letter = getNextLetter(cx.letterState);
595
+ const topFuncs = topFuncSrc;
596
+
597
+ let injectCode = ctxDef;
598
+ if (bindings.length > 0) {
599
+ injectCode += "\n" + bindings.map(b => {
600
+ const accessor = b.prop === "self" ? "" : "." + b.prop;
601
+ return "let " + b.varName + " = self.querySelector('[data-chbind-" + b.bindId + "]')" + accessor + ";";
602
+ }).join("\n") + "\n";
603
+ }
604
+ if (topFuncs.length > 0) {
605
+ injectCode += "\n" + topFuncs.join("\n\n") + "\n";
606
+ }
607
+ runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
608
+
609
+ runtime = runtime.replace(`${RUNTIME_KW}()`, `${letter}r(self, ctx)`);
610
+ cx.runtimeChunks.push(runtime);
611
+ cx.runtimeMap && cx.runtimeMap.set(compName, { letter });
612
+ } else {
613
+ letter = letterEntry.letter;
614
+ }
615
+
616
+ cx.runtimeChunks.push(`${letter}r(document.querySelector('[chid="${compId}"]'), ${JSON.stringify(ctx)});`);
617
+ }
618
+ }
619
+ }
620
+
621
+ if (styles) {
622
+ let cssId = cx.cssScopesMap && cx.cssScopesMap.get(compName);
623
+ if (!cssId) {
624
+ cssId = genRandomId(cx.cssScopes, 8, true);
625
+ cx.cssScopesMap.set(compName, cssId);
626
+ }
627
+ if (fragment.children.length === 1 && firstChild.nodeType === 1) {
628
+ firstChild.classList.add(cssId);
629
+ }
630
+ styles = scopeCss(styles, cssId);
631
+ cx.scopedStyles.push(styles);
632
+ }
633
+
634
+ if (fragment.children.length === 1 && firstChild.nodeType === 1) {
635
+ firstChild.setAttribute("data-ch-source", compName);
636
+ }
637
+
638
+ element.replaceWith(fragment);
639
+ return true;
640
+ }
641
+
642
+ export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent) {
643
+ const cx = new ProcessContext(
644
+ loadedComponents, [], [], { value: null }, new Map(), [], new Map(), [], new Map()
645
+ );
646
+
647
+ appElements.forEach(el => {
648
+ if (!el.isConnected) return;
649
+ processComponentElement(el, cx, [], pageSourceFile, pageSourceContent);
650
+ });
651
+ const runtimeScript = cx.runtimeChunks.join("\n");
652
+ const hasComponents = cx.runtimeChunks.length > 0;
653
+ const scopesCss = cx.scopedStyles.join("\n");
654
+
655
+ return { runtimeScript, hasComponents, scopesCss };
656
+ }
657
+
658
+ function getNextLetter(letterState) {
659
+ if (!letterState.value) {
660
+ letterState.value = "a";
661
+ } else {
662
+ letterState.value = incrementAlfabet(letterState.value);
663
+ }
664
+ return letterState.value;
665
+ }
666
+
667
+ const RUNTIME_KW = "$runtime";