@zenithbuild/core 1.1.0 → 1.2.1
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.
|
@@ -57,11 +57,6 @@ function lowerNode(
|
|
|
57
57
|
return lowerExpressionNode(node, filePath, expressions)
|
|
58
58
|
|
|
59
59
|
case 'element': {
|
|
60
|
-
// Check if this is a <for> element directive
|
|
61
|
-
if (node.tag === 'for') {
|
|
62
|
-
return lowerForElement(node, filePath, expressions)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
60
|
// Check if this is an <html-content> element directive
|
|
66
61
|
if (node.tag === 'html-content') {
|
|
67
62
|
return lowerHtmlContentElement(node, filePath, expressions)
|
|
@@ -287,95 +282,6 @@ function lowerInlineFragment(
|
|
|
287
282
|
return node
|
|
288
283
|
}
|
|
289
284
|
|
|
290
|
-
/**
|
|
291
|
-
* Lower <for> element directive to LoopFragmentNode
|
|
292
|
-
*
|
|
293
|
-
* Syntax: <for each="item" in="items">...body...</for>
|
|
294
|
-
* Or: <for each="item, index" in="items">...body...</for>
|
|
295
|
-
*
|
|
296
|
-
* This is compile-time sugar for {items.map(item => ...)}
|
|
297
|
-
*/
|
|
298
|
-
function lowerForElement(
|
|
299
|
-
node: import('../ir/types').ElementNode,
|
|
300
|
-
filePath: string,
|
|
301
|
-
expressions: ExpressionIR[]
|
|
302
|
-
): LoopFragmentNode {
|
|
303
|
-
// Extract 'each' and 'in' attributes
|
|
304
|
-
const eachAttr = node.attributes.find(a => a.name === 'each')
|
|
305
|
-
const inAttr = node.attributes.find(a => a.name === 'in')
|
|
306
|
-
|
|
307
|
-
if (!eachAttr || typeof eachAttr.value !== 'string') {
|
|
308
|
-
throw new InvariantError(
|
|
309
|
-
'ZEN001',
|
|
310
|
-
`<for> element requires an 'each' attribute specifying the item variable`,
|
|
311
|
-
'Usage: <for each="item" in="items">...body...</for>',
|
|
312
|
-
filePath,
|
|
313
|
-
node.location.line,
|
|
314
|
-
node.location.column
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (!inAttr || typeof inAttr.value !== 'string') {
|
|
319
|
-
throw new InvariantError(
|
|
320
|
-
'ZEN001',
|
|
321
|
-
`<for> element requires an 'in' attribute specifying the source array`,
|
|
322
|
-
'Usage: <for each="item" in="items">...body...</for>',
|
|
323
|
-
filePath,
|
|
324
|
-
node.location.line,
|
|
325
|
-
node.location.column
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Parse item variable (may include index: "item, index" or "item, i")
|
|
330
|
-
const eachValue = eachAttr.value.trim()
|
|
331
|
-
let itemVar: string
|
|
332
|
-
let indexVar: string | undefined
|
|
333
|
-
|
|
334
|
-
if (eachValue.includes(',')) {
|
|
335
|
-
const parts = eachValue.split(',').map(p => p.trim())
|
|
336
|
-
itemVar = parts[0]!
|
|
337
|
-
indexVar = parts[1]
|
|
338
|
-
} else {
|
|
339
|
-
itemVar = eachValue
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const source = inAttr.value.trim()
|
|
343
|
-
|
|
344
|
-
// Create loop context for the body
|
|
345
|
-
const loopVariables = [itemVar]
|
|
346
|
-
if (indexVar) {
|
|
347
|
-
loopVariables.push(indexVar)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const bodyLoopContext: LoopContext = {
|
|
351
|
-
variables: node.loopContext
|
|
352
|
-
? [...node.loopContext.variables, ...loopVariables]
|
|
353
|
-
: loopVariables,
|
|
354
|
-
mapSource: source
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Lower children with loop context
|
|
358
|
-
const body = node.children.map(child => {
|
|
359
|
-
// Recursively lower children
|
|
360
|
-
const lowered = lowerNode(child, filePath, expressions)
|
|
361
|
-
// Attach loop context to children that need it
|
|
362
|
-
if ('loopContext' in lowered) {
|
|
363
|
-
return { ...lowered, loopContext: bodyLoopContext }
|
|
364
|
-
}
|
|
365
|
-
return lowered
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
return {
|
|
369
|
-
type: 'loop-fragment',
|
|
370
|
-
source,
|
|
371
|
-
itemVar,
|
|
372
|
-
indexVar,
|
|
373
|
-
body,
|
|
374
|
-
location: node.location,
|
|
375
|
-
loopContext: bodyLoopContext
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
285
|
/**
|
|
380
286
|
* Lower <html-content> element directive
|
|
381
287
|
*
|
|
@@ -126,7 +126,8 @@ export function transformNode(
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
case 'loop-fragment': {
|
|
129
|
-
// Loop fragment: {items.map(item => <li>...</li>)}
|
|
129
|
+
// Loop fragment: {items.map(item => <li>...</li>)}
|
|
130
|
+
// .map() is compile-time sugar, lowered to LoopFragmentNode
|
|
130
131
|
// For SSR/SSG, we render one instance of the body as a template
|
|
131
132
|
// The runtime will hydrate and expand this for each actual item
|
|
132
133
|
const loopNode = node as LoopFragmentNode
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/core",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Core library for the Zenith framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -77,4 +77,4 @@
|
|
|
77
77
|
"parse5": "^8.0.0",
|
|
78
78
|
"picocolors": "^1.1.1"
|
|
79
79
|
}
|
|
80
|
-
}
|
|
80
|
+
}
|
|
@@ -309,80 +309,461 @@ export function getExpression(id: string): ((state: any) => any) | undefined {
|
|
|
309
309
|
// Hydration Functions
|
|
310
310
|
// ============================================
|
|
311
311
|
|
|
312
|
-
|
|
312
|
+
// ============================================
|
|
313
|
+
// Hydration & Binding System
|
|
314
|
+
// ============================================
|
|
313
315
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
interface BaseBinding {
|
|
317
|
+
node: Node;
|
|
318
|
+
type: 'text' | 'attribute' | 'html' | 'conditional' | 'optional' | 'loop';
|
|
319
|
+
id: string; // Binding/Expression ID
|
|
320
|
+
}
|
|
316
321
|
|
|
317
|
-
|
|
318
|
-
|
|
322
|
+
interface TextBinding extends BaseBinding {
|
|
323
|
+
type: 'text';
|
|
324
|
+
node: Text;
|
|
325
|
+
}
|
|
319
326
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
327
|
+
interface AttributeBinding extends BaseBinding {
|
|
328
|
+
type: 'attribute';
|
|
329
|
+
node: Element;
|
|
330
|
+
attrName: string;
|
|
331
|
+
}
|
|
325
332
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
interface HtmlBinding extends BaseBinding {
|
|
334
|
+
type: 'html';
|
|
335
|
+
node: Element;
|
|
336
|
+
}
|
|
329
337
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
338
|
+
interface ConditionalBinding extends BaseBinding {
|
|
339
|
+
type: 'conditional';
|
|
340
|
+
node: Element; // The container div with data-zen-cond
|
|
341
|
+
trueBranch?: Node[]; // Cached true branch nodes
|
|
342
|
+
falseBranch?: Node[]; // Cached false branch nodes
|
|
343
|
+
currentBranch: boolean | null; // true, false, or null (uninitialized)
|
|
344
|
+
}
|
|
337
345
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
346
|
+
interface OptionalBinding extends BaseBinding {
|
|
347
|
+
type: 'optional';
|
|
348
|
+
node: Element; // The container div with data-zen-opt
|
|
349
|
+
content?: Node[]; // Cached content nodes
|
|
350
|
+
isVisible: boolean | null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
interface LoopBinding extends BaseBinding {
|
|
354
|
+
type: 'loop';
|
|
355
|
+
node: Element; // The container div with data-zen-loop
|
|
356
|
+
template: Node[]; // THe template nodes (cloned from initial content)
|
|
357
|
+
items: any[]; // Current items list
|
|
358
|
+
itemBindings: Binding[][]; // Bindings for each item
|
|
359
|
+
itemVar: string;
|
|
360
|
+
indexVar: string | null;
|
|
361
|
+
sourceExpr: string;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
type Binding = TextBinding | AttributeBinding | HtmlBinding | ConditionalBinding | OptionalBinding | LoopBinding;
|
|
365
|
+
|
|
366
|
+
// Root bindings for the application
|
|
367
|
+
const rootBindings: Binding[] = [];
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Recursive Hydration (Tree Walker)
|
|
371
|
+
*
|
|
372
|
+
* Scans the DOM tree for bindings, respecting scope boundaries.
|
|
373
|
+
* Returns a list of bindings found in this subtree (excluding those inside child scopes like loops).
|
|
374
|
+
*/
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Manual recursive traversal to better handle skipping subtrees
|
|
378
|
+
*/
|
|
379
|
+
function scanBindings(node: Node, bindings: Binding[]): void {
|
|
380
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
381
|
+
const element = node as Element;
|
|
382
|
+
|
|
383
|
+
// Check for Loop - Stop traversal into children if found
|
|
384
|
+
if (element.hasAttribute('data-zen-loop')) {
|
|
385
|
+
const id = element.getAttribute('data-zen-loop')!;
|
|
386
|
+
const source = element.getAttribute('data-zen-source')!;
|
|
387
|
+
const itemVar = element.getAttribute('data-zen-item')!;
|
|
388
|
+
const indexVar = element.getAttribute('data-zen-index');
|
|
389
|
+
|
|
390
|
+
// Capture template from initial content (SSR)
|
|
391
|
+
// Note: For client-side nav, this might modify `element` immediately?
|
|
392
|
+
// For now assume standard hydration of SSR content
|
|
393
|
+
const template = Array.from(element.childNodes).map(n => n.cloneNode(true));
|
|
394
|
+
|
|
395
|
+
// The loop binding itself manages the children.
|
|
396
|
+
// We do NOT scan children here. The loop update() will scan/hydrate instances.
|
|
397
|
+
|
|
398
|
+
// Clear initial SSR content so we can re-render fresh?
|
|
399
|
+
// Or try to hydrate existing? Hydrating lists is hard (keys etc).
|
|
400
|
+
// Simplest safe fix: Clear and re-render.
|
|
401
|
+
element.innerHTML = '';
|
|
402
|
+
|
|
403
|
+
bindings.push({
|
|
404
|
+
type: 'loop',
|
|
405
|
+
node: element,
|
|
406
|
+
id,
|
|
407
|
+
sourceExpr: source,
|
|
408
|
+
itemVar,
|
|
409
|
+
indexVar,
|
|
410
|
+
template,
|
|
411
|
+
items: [],
|
|
412
|
+
itemBindings: []
|
|
413
|
+
} as LoopBinding);
|
|
414
|
+
|
|
415
|
+
return; // STOP recursion into loop children
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Check for Conditional - Stop recursion?
|
|
419
|
+
// Actually, conditional blocks contain children we WANT to bind if they are currently visible.
|
|
420
|
+
// But if we toggle, we need to re-scan.
|
|
421
|
+
// So ConditionalBinding should manage its children.
|
|
422
|
+
if (element.hasAttribute('data-zen-cond')) {
|
|
423
|
+
const id = element.getAttribute('data-zen-cond')!;
|
|
424
|
+
// We assume the true/false branches are initially present as separate containers
|
|
425
|
+
// OR `data-zen-cond-true` / `data-zen-cond-false` markers?
|
|
426
|
+
// The compiler output:
|
|
427
|
+
// <div data_zen_cond="id" data_zen_cond_true style="display:contents">...</div>
|
|
428
|
+
// <div data_zen_cond="id" data_zen_cond_false style="display:none">...</div>
|
|
429
|
+
// These are siblings usually? Or nested?
|
|
430
|
+
// Wait, compiler generates TWO divs.
|
|
431
|
+
// <div data-zen-cond="id" ...true> and <div data-zen-cond="id" ...false>
|
|
432
|
+
// Each is a separate binding effectively?
|
|
433
|
+
// If they share the same ID, they are part of the same logic.
|
|
434
|
+
// Scanning will find both. We can treat them as independent toggle-able areas?
|
|
435
|
+
|
|
436
|
+
// Actually, simpler to treat them as ConditionalBinding.
|
|
437
|
+
// But we need to know which branch it is.
|
|
438
|
+
const isTrueBranch = element.hasAttribute('data-zen-cond-true');
|
|
439
|
+
|
|
440
|
+
// For now, let's treat them as distinct bindings that listen to the same expression
|
|
441
|
+
// and toggle visibility.
|
|
442
|
+
// We DO want to scan their children because they might be visible.
|
|
443
|
+
|
|
444
|
+
// Optimization: If display:none, maybe don't scan yet?
|
|
445
|
+
// But we want to be ready to show.
|
|
446
|
+
|
|
447
|
+
// Let's implement ConditionalBinding to manage visibility AND recurse.
|
|
448
|
+
bindings.push({
|
|
449
|
+
type: 'conditional',
|
|
450
|
+
node: element,
|
|
451
|
+
id,
|
|
452
|
+
currentBranch: null, // Force update
|
|
453
|
+
|
|
454
|
+
} as any); // Simplification: Just toggle display
|
|
455
|
+
// NO, wait. If we hide it, we shouldn't update its children's bindings if they depend on ephemeral state?
|
|
456
|
+
// Actually, simplest is just toggle binding.
|
|
457
|
+
// Let's rely on standard attribute binding?
|
|
458
|
+
// No, standard attribute binding doesn't handle children.
|
|
459
|
+
|
|
460
|
+
// Revisiting the compiler:
|
|
461
|
+
// It's just a div with an ID. The logic is "If cond is true, show TrueDiv, hide FalseDiv".
|
|
462
|
+
// So we can have a Binding that just toggles 'style.display'.
|
|
463
|
+
// And we CONTINUE scanning children.
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Optional? Same.
|
|
467
|
+
|
|
468
|
+
// Bind attributes
|
|
469
|
+
const attrSelectors = ['class', 'style', 'src', 'href', 'disabled', 'checked', 'value', 'placeholder'];
|
|
470
|
+
for (const attr of attrSelectors) {
|
|
471
|
+
const attrKey = `data-zen-attr-${attr}`;
|
|
472
|
+
if (element.hasAttribute(attrKey)) {
|
|
473
|
+
bindings.push({
|
|
474
|
+
type: 'attribute',
|
|
475
|
+
node: element,
|
|
476
|
+
id: element.getAttribute(attrKey)!,
|
|
477
|
+
attrName: attr
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Bind HTML
|
|
483
|
+
if (element.hasAttribute('data-zen-html')) {
|
|
484
|
+
bindings.push({
|
|
485
|
+
type: 'html',
|
|
486
|
+
node: element,
|
|
487
|
+
id: element.getAttribute('data-zen-html')!
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Bind Events
|
|
492
|
+
bindEventsHelpers(element);
|
|
341
493
|
}
|
|
342
494
|
|
|
343
|
-
//
|
|
344
|
-
|
|
495
|
+
// Check Text Nodes
|
|
496
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
497
|
+
// Text nodes can't have attributes, so we look at parent?
|
|
498
|
+
// No, Zenith compiler puts `data-zen-text="id"` on the PARENT element usually?
|
|
499
|
+
// Or strictly on the element wrapping the text?
|
|
500
|
+
// Compiler: <span data-zen-text="id"></span> or similar.
|
|
501
|
+
// Wait, `transformNode` for text binding:
|
|
502
|
+
// Returns `<!--binding:id-->`? No.
|
|
503
|
+
// It returns a TextNode in IR. `generateBindings` validates "Text binding must have target 'data-zen-text'".
|
|
504
|
+
// `transformNode`: if binding exists, it sets `data-zen-text` attribute on the element?
|
|
505
|
+
// Wait, text nodes don't have attributes.
|
|
506
|
+
// The compiler wraps text in a generic element or expects the parent?
|
|
507
|
+
// Usually framework puts it on the parent element.
|
|
508
|
+
// Let's check `parseTemplate.ts` or `transformNode.ts`...
|
|
509
|
+
// Assuming the element with `data-zen-text` owns the text content.
|
|
510
|
+
}
|
|
345
511
|
|
|
346
|
-
//
|
|
347
|
-
|
|
512
|
+
// Check parent for Text Binding (on Element)
|
|
513
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
514
|
+
const el = node as Element;
|
|
515
|
+
if (el.hasAttribute('data-zen-text')) {
|
|
516
|
+
bindings.push({
|
|
517
|
+
type: 'text',
|
|
518
|
+
node: el.firstChild as Text || el.appendChild(document.createTextNode('')), // Ensure text node exists
|
|
519
|
+
id: el.getAttribute('data-zen-text')!
|
|
520
|
+
} as TextBinding);
|
|
521
|
+
// We don't skip children here, but usually text binding replaces content.
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Recurse to children (unless stopped above)
|
|
526
|
+
let child = node.firstChild;
|
|
527
|
+
while (child) {
|
|
528
|
+
scanBindings(child, bindings);
|
|
529
|
+
child = child.nextSibling;
|
|
530
|
+
}
|
|
348
531
|
}
|
|
349
532
|
|
|
350
|
-
function
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
533
|
+
function bindEventsHelpers(element: Element) {
|
|
534
|
+
const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
|
|
535
|
+
for (const eventType of eventTypes) {
|
|
536
|
+
const attr = `data-zen-${eventType}`;
|
|
537
|
+
if (element.hasAttribute(attr)) {
|
|
538
|
+
// We attach handler immediately?
|
|
539
|
+
// Handler needs to access runtime state.
|
|
540
|
+
// We'll use a closure that calls the global/scoped handler at runtime.
|
|
541
|
+
const handlerName = element.getAttribute(attr)!;
|
|
542
|
+
|
|
543
|
+
// Remove old
|
|
544
|
+
const oldHandler = (element as any)[`__zen_${eventType}`];
|
|
545
|
+
if (oldHandler) element.removeEventListener(eventType, oldHandler);
|
|
546
|
+
|
|
547
|
+
// Add new
|
|
548
|
+
const handler = (e: Event) => dispatchEvent(e, handlerName, element);
|
|
549
|
+
(element as any)[`__zen_${eventType}`] = handler;
|
|
550
|
+
element.addEventListener(eventType, handler);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function dispatchEvent(event: Event, handlerName: string, element: Element) {
|
|
556
|
+
// We need to find the SCOPE.
|
|
557
|
+
// Event handler might be looking for `item` (loop variable).
|
|
558
|
+
// This is tricky with standard DOM events.
|
|
559
|
+
// We attach scope info to the DOM element?
|
|
560
|
+
// When LoopBinding creates nodes, it should attach `_zen_scope` property to the root of the item?
|
|
561
|
+
|
|
562
|
+
// Finding scope: traverse up to find an element with `_zen_scope`.
|
|
563
|
+
let target = element as any;
|
|
564
|
+
let scope = {};
|
|
565
|
+
|
|
566
|
+
while (target) {
|
|
567
|
+
if (target._zen_scope) {
|
|
568
|
+
scope = { ...target._zen_scope, ...scope }; // Merge scopes (inner wins? No, inner is more specific)
|
|
569
|
+
// Actually, prototype chain or Object.assign?
|
|
570
|
+
// List items usually have unique props.
|
|
571
|
+
// Let's accumulate.
|
|
572
|
+
// For a simple hierarchy: Global < LoopItem.
|
|
573
|
+
// We start matching from Global? No.
|
|
574
|
+
// The handler execution needs the collected scope.
|
|
575
|
+
break; // Assuming flat scope per loop item for now or merge?
|
|
576
|
+
}
|
|
577
|
+
target = target.parentNode;
|
|
355
578
|
}
|
|
356
579
|
|
|
580
|
+
// Global scopes
|
|
581
|
+
// We don't easily have access to the *current* global state passed to update() here,
|
|
582
|
+
// unless we store it globally.
|
|
583
|
+
// `expressionRegistry` handlers take `state`.
|
|
584
|
+
// We need to fetch the latest state?
|
|
585
|
+
// Zenith signals usually read current value directly.
|
|
586
|
+
// But `expression(state)` pattern implies pure function of state.
|
|
587
|
+
|
|
588
|
+
// For events: usually they call a function. `increment()`.
|
|
589
|
+
// That function access signals/state directly.
|
|
590
|
+
// BUT if it uses arguments like `doSomething(item)`, `item` comes from scope.
|
|
591
|
+
|
|
592
|
+
// Only expressions registered in registry need `state` passed?
|
|
593
|
+
// If `handlerName` is in `__ZENITH_EXPRESSIONS__`, calling it requires state.
|
|
594
|
+
// Which state? One with loop vars.
|
|
595
|
+
|
|
596
|
+
// HACK: for now, we rely on the fact that most handlers are simple fn calls.
|
|
597
|
+
// If they are expressions expecting args, we assume they are bound or don't need args?
|
|
598
|
+
// Wait, `<button onclick="remove(item)">`.
|
|
599
|
+
// Compiler lower this to an expression: `(state) => remove(state.item)`.
|
|
600
|
+
// So we DO need to pass the scope-enriched state to the handler if it's an expression.
|
|
601
|
+
|
|
602
|
+
const handlerFunc = (window as any)[handlerName] || (window as any).__ZENITH_EXPRESSIONS__?.get(handlerName);
|
|
603
|
+
|
|
604
|
+
if (typeof handlerFunc === 'function') {
|
|
605
|
+
// Determine effective state
|
|
606
|
+
// We might need a global `getLastState()` or similar if strictly functional.
|
|
607
|
+
// Or we pass `scope` (which has item) as the state?
|
|
608
|
+
// If generic state is required, we are in trouble without a global reference.
|
|
609
|
+
// Let's assume `window.__zenith_last_state` is available or similar?
|
|
610
|
+
// Added `window.__zenith_currentState` in update().
|
|
611
|
+
|
|
612
|
+
const globalState = (window as any).__zenith_currentState || {};
|
|
613
|
+
const effectiveState = { ...globalState, ...scope };
|
|
614
|
+
|
|
615
|
+
handlerFunc(event, element, effectiveState); // Pass state as 3rd arg? Or 1st?
|
|
616
|
+
// Expression signature: (state) => ...
|
|
617
|
+
// Event handler signature: (e, el) => ...
|
|
618
|
+
// Conflict.
|
|
619
|
+
// Zenith compiler usually generates `(state) => (e) => ...` or similar for events?
|
|
620
|
+
// Or the expression IS the handler body?
|
|
621
|
+
// If it's `(state) => ...`, we call it to get the result.
|
|
622
|
+
// If result is function, call it with event?
|
|
623
|
+
|
|
624
|
+
// Let's try calling with state. If it returns function, call that.
|
|
625
|
+
try {
|
|
626
|
+
const result = handlerFunc(effectiveState);
|
|
627
|
+
if (typeof result === 'function') {
|
|
628
|
+
result(event, element);
|
|
629
|
+
}
|
|
630
|
+
} catch (e) {
|
|
631
|
+
// It might be a direct event handler (e) => ...
|
|
632
|
+
handlerFunc(event, element);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function hydrate(state: any, container?: Element | Document): void {
|
|
638
|
+
const root = container || document;
|
|
639
|
+
// Clear global bindings
|
|
640
|
+
rootBindings.length = 0;
|
|
641
|
+
|
|
642
|
+
// Store initial state for events
|
|
643
|
+
(window as any).__zenith_currentState = state;
|
|
644
|
+
|
|
645
|
+
scanBindings(root, rootBindings);
|
|
646
|
+
|
|
647
|
+
// Initial update
|
|
648
|
+
updateBindings(rootBindings, state);
|
|
649
|
+
triggerMount();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Update Loop Logic
|
|
654
|
+
*/
|
|
655
|
+
function updateLoop(binding: LoopBinding, state: any) {
|
|
656
|
+
const { node, id, sourceExpr, itemVar, indexVar, template } = binding;
|
|
657
|
+
|
|
658
|
+
// 1. Evaluate Source
|
|
659
|
+
const expr = expressionRegistry.get(sourceExpr);
|
|
660
|
+
if (!expr) return;
|
|
661
|
+
|
|
662
|
+
let list = [];
|
|
357
663
|
try {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
664
|
+
list = expr(state) || [];
|
|
665
|
+
} catch (e) { console.error('Loop source error', e); }
|
|
666
|
+
|
|
667
|
+
if (!Array.isArray(list)) list = [];
|
|
668
|
+
|
|
669
|
+
// 2. Reconcile (Naive: Clear and Re-render)
|
|
670
|
+
// Optimization: Reuse existing items?
|
|
671
|
+
// For Patch 1.1.0, let's keep it simple and safe: Full Re-render.
|
|
672
|
+
// Ideally we diff.
|
|
673
|
+
|
|
674
|
+
// Detect if nothing changed? (Deep equals or ref check)
|
|
675
|
+
if (binding.items === list) {
|
|
676
|
+
// Same reference, assumes no change?
|
|
677
|
+
// In mutable state (Proxy), the array might be same ref but mutated.
|
|
678
|
+
// We probably should re-render or at least re-update children.
|
|
679
|
+
// Let's fall through.
|
|
680
|
+
}
|
|
681
|
+
binding.items = list;
|
|
682
|
+
|
|
683
|
+
node.innerHTML = '';
|
|
684
|
+
binding.itemBindings = []; // Clear child bindings
|
|
685
|
+
|
|
686
|
+
list.forEach((item, index) => {
|
|
687
|
+
// Create Scope
|
|
688
|
+
const scope = { [itemVar]: item };
|
|
689
|
+
if (indexVar) scope[indexVar] = index;
|
|
690
|
+
|
|
691
|
+
// Clone Template
|
|
692
|
+
const fragment = document.createDocumentFragment();
|
|
693
|
+
template.forEach(n => fragment.appendChild(n.cloneNode(true)));
|
|
694
|
+
|
|
695
|
+
// Mark Scope on Root Elements of Item
|
|
696
|
+
// (Used for event delegation)
|
|
697
|
+
Array.from(fragment.childNodes).forEach((child: any) => {
|
|
698
|
+
if (child.nodeType === 1) child._zen_scope = scope;
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Hydrate this instance (collect bindings)
|
|
702
|
+
const instanceBindings: Binding[] = [];
|
|
703
|
+
// Scan fragment BEFORE appending? Or after?
|
|
704
|
+
// Appending first is easier for traversal, but we want to bind to the specific nodes.
|
|
705
|
+
scanBindings(fragment, instanceBindings);
|
|
706
|
+
binding.itemBindings.push(instanceBindings);
|
|
707
|
+
|
|
708
|
+
node.appendChild(fragment);
|
|
709
|
+
|
|
710
|
+
// Initial Update for this instance
|
|
711
|
+
updateBindings(instanceBindings, { ...state, ...scope });
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function updateBindings(bindingsList: Binding[], state: any) {
|
|
716
|
+
for (const binding of bindingsList) {
|
|
717
|
+
if (binding.type === 'text') {
|
|
718
|
+
const expr = expressionRegistry.get(binding.id);
|
|
719
|
+
if (expr) {
|
|
720
|
+
try { binding.node.textContent = String(expr(state) ?? ''); }
|
|
721
|
+
catch (e) { }
|
|
366
722
|
}
|
|
367
|
-
} else if (
|
|
368
|
-
node.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
723
|
+
} else if (binding.type === 'attribute') {
|
|
724
|
+
updateAttributeBinding(binding.node, binding.attrName, binding.id, state);
|
|
725
|
+
} else if (binding.type === 'html') {
|
|
726
|
+
const expr = expressionRegistry.get(binding.id);
|
|
727
|
+
if (expr) {
|
|
728
|
+
try {
|
|
729
|
+
const val = expr(state);
|
|
730
|
+
binding.node.innerHTML = String(val ?? '');
|
|
731
|
+
} catch (e) { }
|
|
732
|
+
}
|
|
733
|
+
} else if (binding.type === 'conditional' || binding.type === 'optional') {
|
|
734
|
+
// Toggle display based on truthiness
|
|
735
|
+
const expr = expressionRegistry.get(binding.id);
|
|
736
|
+
if (expr) {
|
|
737
|
+
const val = !!expr(state);
|
|
738
|
+
// Check if inverted (data-zen-cond-false)
|
|
739
|
+
const isInverse = (binding.node as Element).hasAttribute('data-zen-cond-false');
|
|
740
|
+
const shouldShow = isInverse ? !val : val;
|
|
741
|
+
|
|
742
|
+
(binding.node as HTMLElement).style.display = shouldShow ? 'contents' : 'none';
|
|
743
|
+
}
|
|
744
|
+
} else if (binding.type === 'loop') {
|
|
745
|
+
updateLoop(binding as LoopBinding, state);
|
|
380
746
|
}
|
|
381
|
-
} catch (error) {
|
|
382
|
-
console.error(`[Zenith] Error evaluating expression ${expressionId}:`, error);
|
|
383
747
|
}
|
|
384
748
|
}
|
|
385
749
|
|
|
750
|
+
export function update(state: any): void {
|
|
751
|
+
(window as any).__zenith_currentState = state;
|
|
752
|
+
updateBindings(rootBindings, state);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export function cleanup(container?: Element | Document): void {
|
|
756
|
+
rootBindings.length = 0;
|
|
757
|
+
rootBindings.length = 0;
|
|
758
|
+
triggerUnmount();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ... existing helper functions (updateAttributeBinding, bindEvents) need slight adjustments or imports?
|
|
762
|
+
// We need to keep updateAttributeBinding available.
|
|
763
|
+
|
|
764
|
+
// REUSING EXISTING updateAttributeBinding from previous implementation
|
|
765
|
+
// (Need to make sure it's defined or moved)
|
|
766
|
+
|
|
386
767
|
function updateAttributeBinding(element: Element, attrName: string, expressionId: string, state: any): void {
|
|
387
768
|
const expression = expressionRegistry.get(expressionId);
|
|
388
769
|
if (!expression) return;
|
|
@@ -413,77 +794,18 @@ function updateAttributeBinding(element: Element, attrName: string, expressionId
|
|
|
413
794
|
}
|
|
414
795
|
}
|
|
415
796
|
|
|
416
|
-
export function update(state: any): void {
|
|
417
|
-
for (const binding of bindings) {
|
|
418
|
-
if (binding.type === 'text') {
|
|
419
|
-
updateTextBinding(binding.node, binding.expressionId, state);
|
|
420
|
-
} else if (binding.type === 'attribute' && binding.attributeName) {
|
|
421
|
-
updateAttributeBinding(binding.node, binding.attributeName, binding.expressionId, state);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
797
|
export function bindEvents(container: Element | Document): void {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
// Remove existing handler if any
|
|
437
|
-
const handlerKey = `__zen_${eventType}_handler`;
|
|
438
|
-
const existingHandler = (element as any)[handlerKey];
|
|
439
|
-
if (existingHandler) {
|
|
440
|
-
element.removeEventListener(eventType, existingHandler);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Create new handler
|
|
444
|
-
const handler = (event: Event) => {
|
|
445
|
-
try {
|
|
446
|
-
// Try window first, then expression registry
|
|
447
|
-
let handlerFunc = (window as any)[handlerName];
|
|
448
|
-
if (typeof handlerFunc !== 'function') {
|
|
449
|
-
handlerFunc = (window as any).__ZENITH_EXPRESSIONS__?.get(handlerName);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (typeof handlerFunc === 'function') {
|
|
453
|
-
handlerFunc(event, element);
|
|
454
|
-
} else {
|
|
455
|
-
console.warn(`[Zenith] Event handler "${handlerName}" not found`);
|
|
456
|
-
}
|
|
457
|
-
} catch (error) {
|
|
458
|
-
console.error(`[Zenith] Error executing handler "${handlerName}":`, error);
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
(element as any)[handlerKey] = handler;
|
|
463
|
-
element.addEventListener(eventType, handler);
|
|
464
|
-
});
|
|
798
|
+
// Legacy support or external call?
|
|
799
|
+
// Our scanBindings() handles events.
|
|
800
|
+
// If called manually, we traverse?
|
|
801
|
+
if (container.nodeType === Node.ELEMENT_NODE) {
|
|
802
|
+
bindEventsHelpers(container as Element);
|
|
803
|
+
// And children...
|
|
804
|
+
const all = (container as Element).querySelectorAll('*');
|
|
805
|
+
all.forEach(el => bindEventsHelpers(el));
|
|
465
806
|
}
|
|
466
807
|
}
|
|
467
808
|
|
|
468
|
-
export function cleanup(container?: Element | Document): void {
|
|
469
|
-
const root = container || document;
|
|
470
|
-
const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
|
|
471
|
-
|
|
472
|
-
for (const eventType of eventTypes) {
|
|
473
|
-
const elements = root.querySelectorAll(`[data-zen-${eventType}]`);
|
|
474
|
-
elements.forEach((element) => {
|
|
475
|
-
const handlerKey = `__zen_${eventType}_handler`;
|
|
476
|
-
const handler = (element as any)[handlerKey];
|
|
477
|
-
if (handler) {
|
|
478
|
-
element.removeEventListener(eventType, handler);
|
|
479
|
-
delete (element as any)[handlerKey];
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
bindings.length = 0;
|
|
485
|
-
triggerUnmount();
|
|
486
|
-
}
|
|
487
809
|
|
|
488
810
|
// ============================================
|
|
489
811
|
// Plugin Runtime Data Access
|