@zenithbuild/core 0.1.0

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.
Files changed (101) hide show
  1. package/.eslintignore +15 -0
  2. package/.gitattributes +2 -0
  3. package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
  4. package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
  5. package/.github/pull_request_template.md +15 -0
  6. package/.github/workflows/discord-changelog.yml +141 -0
  7. package/.github/workflows/discord-notify.yml +242 -0
  8. package/.github/workflows/discord-version.yml +195 -0
  9. package/.prettierignore +13 -0
  10. package/.prettierrc +21 -0
  11. package/.zen.d.ts +15 -0
  12. package/LICENSE +21 -0
  13. package/README.md +55 -0
  14. package/app/components/Button.zen +46 -0
  15. package/app/components/Link.zen +11 -0
  16. package/app/favicon.ico +0 -0
  17. package/app/layouts/Main.zen +59 -0
  18. package/app/pages/about.zen +23 -0
  19. package/app/pages/blog/[id].zen +53 -0
  20. package/app/pages/blog/index.zen +32 -0
  21. package/app/pages/dynamic-dx.zen +712 -0
  22. package/app/pages/dynamic-primitives.zen +453 -0
  23. package/app/pages/index.zen +154 -0
  24. package/app/pages/navigation-demo.zen +229 -0
  25. package/app/pages/posts/[...slug].zen +61 -0
  26. package/app/pages/primitives-demo.zen +273 -0
  27. package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
  28. package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
  29. package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
  30. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
  31. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
  32. package/assets/logos/README.md +54 -0
  33. package/assets/logos/zen.icns +0 -0
  34. package/bun.lock +39 -0
  35. package/compiler/README.md +380 -0
  36. package/compiler/errors/compilerError.ts +24 -0
  37. package/compiler/finalize/finalizeOutput.ts +163 -0
  38. package/compiler/finalize/generateFinalBundle.ts +82 -0
  39. package/compiler/index.ts +44 -0
  40. package/compiler/ir/types.ts +83 -0
  41. package/compiler/legacy/binding.ts +254 -0
  42. package/compiler/legacy/bindings.ts +338 -0
  43. package/compiler/legacy/component-process.ts +1208 -0
  44. package/compiler/legacy/component.ts +301 -0
  45. package/compiler/legacy/event.ts +50 -0
  46. package/compiler/legacy/expression.ts +1149 -0
  47. package/compiler/legacy/mutation.ts +280 -0
  48. package/compiler/legacy/parse.ts +299 -0
  49. package/compiler/legacy/split.ts +608 -0
  50. package/compiler/legacy/types.ts +32 -0
  51. package/compiler/output/types.ts +34 -0
  52. package/compiler/parse/detectMapExpressions.ts +102 -0
  53. package/compiler/parse/parseScript.ts +22 -0
  54. package/compiler/parse/parseTemplate.ts +425 -0
  55. package/compiler/parse/parseZenFile.ts +66 -0
  56. package/compiler/parse/trackLoopContext.ts +82 -0
  57. package/compiler/runtime/dataExposure.ts +291 -0
  58. package/compiler/runtime/generateDOM.ts +144 -0
  59. package/compiler/runtime/generateHydrationBundle.ts +383 -0
  60. package/compiler/runtime/hydration.ts +309 -0
  61. package/compiler/runtime/navigation.ts +432 -0
  62. package/compiler/runtime/thinRuntime.ts +160 -0
  63. package/compiler/runtime/transformIR.ts +256 -0
  64. package/compiler/runtime/wrapExpression.ts +84 -0
  65. package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
  66. package/compiler/spa-build.ts +1000 -0
  67. package/compiler/test/validate-test.ts +104 -0
  68. package/compiler/transform/generateBindings.ts +47 -0
  69. package/compiler/transform/generateHTML.ts +28 -0
  70. package/compiler/transform/transformNode.ts +126 -0
  71. package/compiler/transform/transformTemplate.ts +38 -0
  72. package/compiler/validate/validateExpressions.ts +168 -0
  73. package/core/index.ts +135 -0
  74. package/core/lifecycle/index.ts +49 -0
  75. package/core/lifecycle/zen-mount.ts +182 -0
  76. package/core/lifecycle/zen-unmount.ts +88 -0
  77. package/core/reactivity/index.ts +54 -0
  78. package/core/reactivity/tracking.ts +167 -0
  79. package/core/reactivity/zen-batch.ts +57 -0
  80. package/core/reactivity/zen-effect.ts +139 -0
  81. package/core/reactivity/zen-memo.ts +146 -0
  82. package/core/reactivity/zen-ref.ts +52 -0
  83. package/core/reactivity/zen-signal.ts +121 -0
  84. package/core/reactivity/zen-state.ts +180 -0
  85. package/core/reactivity/zen-untrack.ts +44 -0
  86. package/docs/COMMENTS.md +111 -0
  87. package/docs/COMMITS.md +36 -0
  88. package/docs/CONTRIBUTING.md +116 -0
  89. package/docs/STYLEGUIDE.md +62 -0
  90. package/package.json +44 -0
  91. package/router/index.ts +76 -0
  92. package/router/manifest.ts +314 -0
  93. package/router/navigation/ZenLink.zen +231 -0
  94. package/router/navigation/index.ts +78 -0
  95. package/router/navigation/zen-link.ts +584 -0
  96. package/router/runtime.ts +458 -0
  97. package/router/types.ts +168 -0
  98. package/runtime/build.ts +17 -0
  99. package/runtime/serve.ts +93 -0
  100. package/scripts/webhook-proxy.ts +213 -0
  101. package/tsconfig.json +28 -0
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Generate Hydration Bundle
3
+ *
4
+ * Phase 5: Generates the complete runtime bundle including expressions and hydration code
5
+ */
6
+
7
+ import type { ExpressionIR } from '../ir/types'
8
+ import { wrapExpression } from './wrapExpression'
9
+
10
+ /**
11
+ * Generate the hydration runtime code as a string
12
+ * This is the browser-side runtime that hydrates DOM placeholders
13
+ */
14
+ export function generateHydrationRuntime(): string {
15
+ return `
16
+ // Zenith Runtime Hydration Layer (Phase 5)
17
+ (function() {
18
+ 'use strict';
19
+
20
+ // Expression registry - maps expression IDs to their evaluation functions
21
+ if (typeof window !== 'undefined' && !window.__ZENITH_EXPRESSIONS__) {
22
+ window.__ZENITH_EXPRESSIONS__ = new Map();
23
+ }
24
+
25
+ // Binding registry - tracks which DOM nodes are bound to which expressions
26
+ const __zen_bindings = [];
27
+
28
+ /**
29
+ * Update a text binding
30
+ * Phase 6: Accepts explicit data arguments
31
+ */
32
+ function updateTextBinding(node, expressionId, state, loaderData, props, stores) {
33
+ try {
34
+ const expression = window.__ZENITH_EXPRESSIONS__.get(expressionId);
35
+ if (!expression) {
36
+ console.warn('[Zenith] Expression ' + expressionId + ' not found in registry');
37
+ return;
38
+ }
39
+
40
+ // Call expression with appropriate arguments based on function length
41
+ const result = expression.length === 1
42
+ ? expression(state) // Legacy: state only
43
+ : expression(state, loaderData, props, stores); // Phase 6: explicit arguments
44
+
45
+ // Handle different result types
46
+ if (result === null || result === undefined || result === false) {
47
+ node.textContent = '';
48
+ } else if (typeof result === 'string' || typeof result === 'number') {
49
+ node.textContent = String(result);
50
+ } else if (result instanceof Node) {
51
+ // Replace node with result node
52
+ if (node.parentNode) {
53
+ node.parentNode.replaceChild(result, node);
54
+ }
55
+ } else if (Array.isArray(result)) {
56
+ // Handle array results (for map expressions)
57
+ if (node.parentNode) {
58
+ const fragment = document.createDocumentFragment();
59
+ for (let i = 0; i < result.length; i++) {
60
+ const item = result[i];
61
+ if (item instanceof Node) {
62
+ fragment.appendChild(item);
63
+ } else {
64
+ fragment.appendChild(document.createTextNode(String(item)));
65
+ }
66
+ }
67
+ node.parentNode.replaceChild(fragment, node);
68
+ }
69
+ } else {
70
+ node.textContent = String(result);
71
+ }
72
+ } catch (error) {
73
+ console.error('[Zenith] Error evaluating expression ' + expressionId + ':', error);
74
+ console.error('Expression ID:', expressionId, 'State:', state);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Update an attribute binding
80
+ * Phase 6: Accepts explicit data arguments
81
+ */
82
+ function updateAttributeBinding(element, attributeName, expressionId, state, loaderData, props, stores) {
83
+ try {
84
+ const expression = window.__ZENITH_EXPRESSIONS__.get(expressionId);
85
+ if (!expression) {
86
+ console.warn('[Zenith] Expression ' + expressionId + ' not found in registry');
87
+ return;
88
+ }
89
+
90
+ // Call expression with appropriate arguments based on function length
91
+ const result = expression.length === 1
92
+ ? expression(state) // Legacy: state only
93
+ : expression(state, loaderData, props, stores); // Phase 6: explicit arguments
94
+
95
+ // Handle different attribute types
96
+ if (attributeName === 'class' || attributeName === 'className') {
97
+ element.className = String(result != null ? result : '');
98
+ } else if (attributeName === 'style') {
99
+ if (typeof result === 'string') {
100
+ element.setAttribute('style', result);
101
+ } else if (result && typeof result === 'object') {
102
+ // Handle style object
103
+ const styleStr = Object.keys(result).map(function(key) {
104
+ return key + ': ' + result[key];
105
+ }).join('; ');
106
+ element.setAttribute('style', styleStr);
107
+ }
108
+ } else if (attributeName === 'disabled' || attributeName === 'checked' || attributeName === 'readonly') {
109
+ // Boolean attributes
110
+ if (result) {
111
+ element.setAttribute(attributeName, '');
112
+ } else {
113
+ element.removeAttribute(attributeName);
114
+ }
115
+ } else {
116
+ // Regular attributes
117
+ if (result === null || result === undefined || result === false) {
118
+ element.removeAttribute(attributeName);
119
+ } else {
120
+ element.setAttribute(attributeName, String(result));
121
+ }
122
+ }
123
+ } catch (error) {
124
+ console.error('[Zenith] Error updating attribute ' + attributeName + ' with expression ' + expressionId + ':', error);
125
+ console.error('Expression ID:', expressionId, 'State:', state);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Hydrate static HTML with dynamic expressions
131
+ * Phase 6: Accepts explicit loaderData, props, stores arguments
132
+ */
133
+ function hydrate(state, loaderData, props, stores, container) {
134
+ if (!state) {
135
+ console.warn('[Zenith] hydrate called without state object');
136
+ return;
137
+ }
138
+
139
+ // Handle optional arguments (backwards compatibility)
140
+ if (typeof container === 'undefined' && typeof stores === 'object' && stores && !stores.nodeType) {
141
+ // Called as hydrate(state, loaderData, props, stores, container)
142
+ container = document;
143
+ } else if (typeof props === 'object' && props && !props.nodeType && typeof stores === 'undefined') {
144
+ // Called as hydrate(state, loaderData, props) - container is props
145
+ container = props;
146
+ props = loaderData;
147
+ loaderData = undefined;
148
+ stores = undefined;
149
+ } else if (typeof loaderData === 'object' && loaderData && loaderData.nodeType) {
150
+ // Called as hydrate(state, container) - legacy signature
151
+ container = loaderData;
152
+ loaderData = undefined;
153
+ props = undefined;
154
+ stores = undefined;
155
+ } else {
156
+ container = container || document;
157
+ }
158
+
159
+ // Default empty objects for missing arguments
160
+ loaderData = loaderData || {};
161
+ props = props || {};
162
+ stores = stores || {};
163
+
164
+ // Store state and data globally for event handlers
165
+ if (typeof window !== 'undefined') {
166
+ window.__ZENITH_STATE__ = state;
167
+ window.__ZENITH_LOADER_DATA__ = loaderData;
168
+ window.__ZENITH_PROPS__ = props;
169
+ window.__ZENITH_STORES__ = stores;
170
+ }
171
+
172
+ // Clear existing bindings
173
+ __zen_bindings.length = 0;
174
+
175
+ // Find all text expression placeholders
176
+ const textPlaceholders = container.querySelectorAll('[data-zen-text]');
177
+ for (let i = 0; i < textPlaceholders.length; i++) {
178
+ const node = textPlaceholders[i];
179
+ const expressionId = node.getAttribute('data-zen-text');
180
+ if (!expressionId) continue;
181
+
182
+ __zen_bindings.push({
183
+ node: node,
184
+ type: 'text',
185
+ expressionId: expressionId
186
+ });
187
+
188
+ updateTextBinding(node, expressionId, state, loaderData, props, stores);
189
+ }
190
+
191
+ // Find all attribute expression placeholders
192
+ const attrSelectors = [
193
+ '[data-zen-attr-class]',
194
+ '[data-zen-attr-style]',
195
+ '[data-zen-attr-src]',
196
+ '[data-zen-attr-href]',
197
+ '[data-zen-attr-disabled]',
198
+ '[data-zen-attr-checked]'
199
+ ];
200
+
201
+ for (let s = 0; s < attrSelectors.length; s++) {
202
+ const attrPlaceholders = container.querySelectorAll(attrSelectors[s]);
203
+ for (let i = 0; i < attrPlaceholders.length; i++) {
204
+ const node = attrPlaceholders[i];
205
+ if (!(node instanceof Element)) continue;
206
+
207
+ // Extract attribute name from selector
208
+ const attrMatch = attrSelectors[s].match(/data-zen-attr-(\\w+)/);
209
+ if (!attrMatch) continue;
210
+ const attrName = attrMatch[1];
211
+
212
+ const expressionId = node.getAttribute('data-zen-attr-' + attrName);
213
+ if (!expressionId) continue;
214
+
215
+ __zen_bindings.push({
216
+ node: node,
217
+ type: 'attribute',
218
+ attributeName: attrName,
219
+ expressionId: expressionId
220
+ });
221
+
222
+ updateAttributeBinding(node, attrName, expressionId, state, loaderData, props, stores);
223
+ }
224
+ }
225
+
226
+ // Bind event handlers
227
+ bindEvents(container);
228
+ }
229
+
230
+ /**
231
+ * Bind event handlers to DOM elements
232
+ */
233
+ function bindEvents(container) {
234
+ container = container || document;
235
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
236
+
237
+ for (let e = 0; e < eventTypes.length; e++) {
238
+ const eventType = eventTypes[e];
239
+ const elements = container.querySelectorAll('[data-zen-' + eventType + ']');
240
+
241
+ for (let i = 0; i < elements.length; i++) {
242
+ const element = elements[i];
243
+ if (!(element instanceof Element)) continue;
244
+
245
+ const handlerName = element.getAttribute('data-zen-' + eventType);
246
+ if (!handlerName) continue;
247
+
248
+ // Remove existing listener if any (to avoid duplicates)
249
+ const handlerKey = '__zen_' + eventType + '_handler';
250
+ const existingHandler = element[handlerKey];
251
+ if (existingHandler) {
252
+ element.removeEventListener(eventType, existingHandler);
253
+ }
254
+
255
+ // Create new handler
256
+ const handler = function(event) {
257
+ try {
258
+ // Get handler function from window (functions are registered on window)
259
+ const handlerFunc = window[handlerName];
260
+ if (typeof handlerFunc === 'function') {
261
+ handlerFunc(event, element);
262
+ } else {
263
+ console.warn('[Zenith] Event handler "' + handlerName + '" not found for ' + eventType + ' event');
264
+ }
265
+ } catch (error) {
266
+ console.error('[Zenith] Error executing event handler "' + handlerName + '":', error);
267
+ }
268
+ };
269
+
270
+ // Store handler reference to allow cleanup
271
+ element[handlerKey] = handler;
272
+
273
+ element.addEventListener(eventType, handler);
274
+ }
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Update all bindings when state changes
280
+ * Phase 6: Accepts explicit data arguments
281
+ */
282
+ function update(state, loaderData, props, stores) {
283
+ if (!state) {
284
+ console.warn('[Zenith] update called without state object');
285
+ return;
286
+ }
287
+
288
+ // Handle optional arguments (backwards compatibility)
289
+ if (typeof loaderData === 'undefined') {
290
+ loaderData = window.__ZENITH_LOADER_DATA__ || {};
291
+ props = window.__ZENITH_PROPS__ || {};
292
+ stores = window.__ZENITH_STORES__ || {};
293
+ } else {
294
+ loaderData = loaderData || {};
295
+ props = props || {};
296
+ stores = stores || {};
297
+ }
298
+
299
+ // Update global state and data
300
+ if (typeof window !== 'undefined') {
301
+ window.__ZENITH_STATE__ = state;
302
+ window.__ZENITH_LOADER_DATA__ = loaderData;
303
+ window.__ZENITH_PROPS__ = props;
304
+ window.__ZENITH_STORES__ = stores;
305
+ }
306
+
307
+ // Update all tracked bindings
308
+ for (let i = 0; i < __zen_bindings.length; i++) {
309
+ const binding = __zen_bindings[i];
310
+ if (binding.type === 'text') {
311
+ updateTextBinding(binding.node, binding.expressionId, state, loaderData, props, stores);
312
+ } else if (binding.type === 'attribute' && binding.attributeName) {
313
+ if (binding.node instanceof Element) {
314
+ updateAttributeBinding(binding.node, binding.attributeName, binding.expressionId, state, loaderData, props, stores);
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Clear all bindings and event listeners
322
+ */
323
+ function cleanup(container) {
324
+ container = container || document;
325
+
326
+ // Remove event listeners
327
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
328
+ for (let e = 0; e < eventTypes.length; e++) {
329
+ const eventType = eventTypes[e];
330
+ const elements = container.querySelectorAll('[data-zen-' + eventType + ']');
331
+ for (let i = 0; i < elements.length; i++) {
332
+ const element = elements[i];
333
+ if (!(element instanceof Element)) continue;
334
+ const handlerKey = '__zen_' + eventType + '_handler';
335
+ const handler = element[handlerKey];
336
+ if (handler) {
337
+ element.removeEventListener(eventType, handler);
338
+ delete element[handlerKey];
339
+ }
340
+ }
341
+ }
342
+
343
+ // Clear bindings
344
+ __zen_bindings.length = 0;
345
+ }
346
+
347
+ // Export functions to window
348
+ if (typeof window !== 'undefined') {
349
+ window.__zenith_hydrate = hydrate;
350
+ window.__zenith_bindEvents = bindEvents;
351
+ window.__zenith_update = update;
352
+ window.__zenith_cleanup = cleanup;
353
+ }
354
+ })();
355
+ `
356
+ }
357
+
358
+ /**
359
+ * Generate expression registry initialization code
360
+ */
361
+ export function generateExpressionRegistry(expressions: ExpressionIR[]): string {
362
+ if (expressions.length === 0) {
363
+ return `
364
+ // No expressions to register
365
+ if (typeof window !== 'undefined' && window.__ZENITH_EXPRESSIONS__) {
366
+ // Registry already initialized
367
+ }`
368
+ }
369
+
370
+ const registryCode = expressions.map(expr => {
371
+ return ` window.__ZENITH_EXPRESSIONS__.set('${expr.id}', ${expr.id});`
372
+ }).join('\n')
373
+
374
+ return `
375
+ // Initialize expression registry
376
+ if (typeof window !== 'undefined') {
377
+ if (!window.__ZENITH_EXPRESSIONS__) {
378
+ window.__ZENITH_EXPRESSIONS__ = new Map();
379
+ }
380
+ ${registryCode}
381
+ }`
382
+ }
383
+
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Runtime Hydration Layer
3
+ *
4
+ * Phase 5: Browser-side runtime that hydrates static HTML with dynamic expressions
5
+ *
6
+ * This runtime:
7
+ * - Locates DOM placeholders (data-zen-text, data-zen-attr-*)
8
+ * - Evaluates precompiled expressions against state
9
+ * - Updates DOM textContent, attributes, and properties
10
+ * - Binds event handlers
11
+ * - Handles reactive state updates
12
+ */
13
+
14
+ /**
15
+ * Expression registry - maps expression IDs to their evaluation functions
16
+ * Populated at runtime with compiled expressions
17
+ */
18
+ declare global {
19
+ interface Window {
20
+ __ZENITH_EXPRESSIONS__?: Map<string, (state: any) => any>
21
+ __ZENITH_STATE__?: any
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Binding registry - tracks which DOM nodes are bound to which expressions
27
+ */
28
+ interface Binding {
29
+ node: Node
30
+ type: 'text' | 'attribute'
31
+ attributeName?: string
32
+ expressionId: string
33
+ }
34
+
35
+ const bindings: Binding[] = []
36
+
37
+ /**
38
+ * Hydrate static HTML with dynamic expressions
39
+ *
40
+ * @param state - The state object to evaluate expressions against
41
+ * @param container - The container element to hydrate (defaults to document)
42
+ */
43
+ export function hydrate(state: any, container: Document | Element = document): void {
44
+ if (!state) {
45
+ console.warn('[Zenith] hydrate called without state object')
46
+ return
47
+ }
48
+
49
+ // Store state globally for event handlers
50
+ if (typeof window !== 'undefined') {
51
+ window.__ZENITH_STATE__ = state
52
+ }
53
+
54
+ // Clear existing bindings
55
+ bindings.length = 0
56
+
57
+ // Find all text expression placeholders
58
+ const textPlaceholders = container.querySelectorAll('[data-zen-text]')
59
+ for (let i = 0; i < textPlaceholders.length; i++) {
60
+ const node = textPlaceholders[i]
61
+ if (!node) continue
62
+ const expressionId = node.getAttribute('data-zen-text')
63
+ if (!expressionId) continue
64
+
65
+ bindings.push({
66
+ node,
67
+ type: 'text',
68
+ expressionId
69
+ })
70
+
71
+ updateTextBinding(node, expressionId, state)
72
+ }
73
+
74
+ // Find all attribute expression placeholders
75
+ const attrPlaceholders = container.querySelectorAll('[data-zen-attr-class], [data-zen-attr-style], [data-zen-attr-src], [data-zen-attr-href], [data-zen-attr-disabled], [data-zen-attr-checked]')
76
+
77
+ for (let i = 0; i < attrPlaceholders.length; i++) {
78
+ const node = attrPlaceholders[i]
79
+ if (!(node instanceof Element)) continue
80
+
81
+ // Check each possible attribute
82
+ const attrNames = ['class', 'style', 'src', 'href', 'disabled', 'checked']
83
+ for (const attrName of attrNames) {
84
+ const expressionId = node.getAttribute(`data-zen-attr-${attrName}`)
85
+ if (!expressionId) continue
86
+
87
+ bindings.push({
88
+ node,
89
+ type: 'attribute',
90
+ attributeName: attrName,
91
+ expressionId
92
+ })
93
+
94
+ updateAttributeBinding(node, attrName, expressionId, state)
95
+ }
96
+ }
97
+
98
+ // Bind event handlers
99
+ bindEvents(container)
100
+ }
101
+
102
+ /**
103
+ * Update a text binding
104
+ */
105
+ function updateTextBinding(node: Node, expressionId: string, state: any): void {
106
+ try {
107
+ const expression = window.__ZENITH_EXPRESSIONS__?.get(expressionId)
108
+ if (!expression) {
109
+ console.warn(`[Zenith] Expression ${expressionId} not found in registry`)
110
+ return
111
+ }
112
+
113
+ const result = expression(state)
114
+
115
+ // Handle different result types
116
+ if (result === null || result === undefined || result === false) {
117
+ node.textContent = ''
118
+ } else if (typeof result === 'string' || typeof result === 'number') {
119
+ node.textContent = String(result)
120
+ } else if (result instanceof Node) {
121
+ // Replace node with result node
122
+ if (node.parentNode) {
123
+ node.parentNode.replaceChild(result, node)
124
+ }
125
+ } else if (Array.isArray(result)) {
126
+ // Handle array results (for map expressions)
127
+ if (node.parentNode) {
128
+ const fragment = document.createDocumentFragment()
129
+ for (const item of result) {
130
+ if (item instanceof Node) {
131
+ fragment.appendChild(item)
132
+ } else {
133
+ fragment.appendChild(document.createTextNode(String(item)))
134
+ }
135
+ }
136
+ node.parentNode.replaceChild(fragment, node)
137
+ }
138
+ } else {
139
+ node.textContent = String(result)
140
+ }
141
+ } catch (error: any) {
142
+ console.error(`[Zenith] Error evaluating expression ${expressionId}:`, error)
143
+ console.error('Expression ID:', expressionId, 'State:', state)
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Update an attribute binding
149
+ */
150
+ function updateAttributeBinding(
151
+ element: Element,
152
+ attributeName: string,
153
+ expressionId: string,
154
+ state: any
155
+ ): void {
156
+ try {
157
+ const expression = window.__ZENITH_EXPRESSIONS__?.get(expressionId)
158
+ if (!expression) {
159
+ console.warn(`[Zenith] Expression ${expressionId} not found in registry`)
160
+ return
161
+ }
162
+
163
+ const result = expression(state)
164
+
165
+ // Handle different attribute types
166
+ if (attributeName === 'class' || attributeName === 'className') {
167
+ element.className = String(result ?? '')
168
+ } else if (attributeName === 'style') {
169
+ if (typeof result === 'string') {
170
+ element.setAttribute('style', result)
171
+ } else if (result && typeof result === 'object') {
172
+ // Handle style object
173
+ const styleStr = Object.entries(result)
174
+ .map(([key, value]) => `${key}: ${value}`)
175
+ .join('; ')
176
+ element.setAttribute('style', styleStr)
177
+ }
178
+ } else if (attributeName === 'disabled' || attributeName === 'checked' || attributeName === 'readonly') {
179
+ // Boolean attributes
180
+ if (result) {
181
+ element.setAttribute(attributeName, '')
182
+ } else {
183
+ element.removeAttribute(attributeName)
184
+ }
185
+ } else {
186
+ // Regular attributes
187
+ if (result === null || result === undefined || result === false) {
188
+ element.removeAttribute(attributeName)
189
+ } else {
190
+ element.setAttribute(attributeName, String(result))
191
+ }
192
+ }
193
+ } catch (error: any) {
194
+ console.error(`[Zenith] Error updating attribute ${attributeName} with expression ${expressionId}:`, error)
195
+ console.error('Expression ID:', expressionId, 'State:', state)
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Bind event handlers to DOM elements
201
+ *
202
+ * @param container - The container element to bind events in (defaults to document)
203
+ */
204
+ export function bindEvents(container: Document | Element = document): void {
205
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown']
206
+
207
+ for (const eventType of eventTypes) {
208
+ const elements = container.querySelectorAll(`[data-zen-${eventType}]`)
209
+
210
+ for (let i = 0; i < elements.length; i++) {
211
+ const element = elements[i]
212
+ if (!(element instanceof Element)) continue
213
+
214
+ const handlerName = element.getAttribute(`data-zen-${eventType}`)
215
+ if (!handlerName) continue
216
+
217
+ // Remove existing listener if any (to avoid duplicates)
218
+ const existingHandler = (element as any)[`__zen_${eventType}_handler`]
219
+ if (existingHandler) {
220
+ element.removeEventListener(eventType, existingHandler)
221
+ }
222
+
223
+ // Create new handler
224
+ const handler = (event: Event) => {
225
+ try {
226
+ // Get handler function from window (functions are registered on window)
227
+ const handlerFunc = (window as any)[handlerName]
228
+ if (typeof handlerFunc === 'function') {
229
+ handlerFunc(event, element)
230
+ } else {
231
+ console.warn(`[Zenith] Event handler "${handlerName}" not found for ${eventType} event`)
232
+ }
233
+ } catch (error: any) {
234
+ console.error(`[Zenith] Error executing event handler "${handlerName}":`, error)
235
+ }
236
+ }
237
+
238
+ // Store handler reference to allow cleanup
239
+ ;(element as any)[`__zen_${eventType}_handler`] = handler
240
+
241
+ element.addEventListener(eventType, handler)
242
+ }
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Update all bindings when state changes
248
+ *
249
+ * @param state - The new state object
250
+ */
251
+ export function update(state: any): void {
252
+ if (!state) {
253
+ console.warn('[Zenith] update called without state object')
254
+ return
255
+ }
256
+
257
+ // Update global state
258
+ if (typeof window !== 'undefined') {
259
+ window.__ZENITH_STATE__ = state
260
+ }
261
+
262
+ // Update all tracked bindings
263
+ for (const binding of bindings) {
264
+ if (binding.type === 'text') {
265
+ updateTextBinding(binding.node, binding.expressionId, state)
266
+ } else if (binding.type === 'attribute' && binding.attributeName) {
267
+ if (binding.node instanceof Element) {
268
+ updateAttributeBinding(binding.node, binding.attributeName, binding.expressionId, state)
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Initialize the expression registry
276
+ * Called once when the runtime loads
277
+ *
278
+ * @param expressions - Map of expression IDs to evaluation functions
279
+ */
280
+ export function initExpressions(expressions: Map<string, (state: any) => any>): void {
281
+ if (typeof window !== 'undefined') {
282
+ window.__ZENITH_EXPRESSIONS__ = expressions
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Clear all bindings and event listeners
288
+ * Useful for cleanup when navigating away
289
+ */
290
+ export function cleanup(container: Document | Element = document): void {
291
+ // Remove event listeners
292
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown']
293
+ for (const eventType of eventTypes) {
294
+ const elements = container.querySelectorAll(`[data-zen-${eventType}]`)
295
+ for (let i = 0; i < elements.length; i++) {
296
+ const element = elements[i]
297
+ if (!(element instanceof Element)) continue
298
+ const handler = (element as any)[`__zen_${eventType}_handler`]
299
+ if (handler) {
300
+ element.removeEventListener(eventType, handler)
301
+ delete (element as any)[`__zen_${eventType}_handler`]
302
+ }
303
+ }
304
+ }
305
+
306
+ // Clear bindings
307
+ bindings.length = 0
308
+ }
309
+