@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,1149 @@
1
+ // compiler/expression.ts
2
+ // Expression parser and analyzer for dynamic HTML expressions in Zenith
3
+ // Handles: conditionals (&&, ||), ternaries (?:), map iterations, and inline expressions
4
+
5
+ /**
6
+ * Expression types that can appear in { } blocks in HTML
7
+ */
8
+ export type ExpressionType =
9
+ | 'static' // Simple state reference: { count }
10
+ | 'conditional' // Boolean conditional: { isLoggedIn && <span>...</span> }
11
+ | 'ternary' // Ternary: { isLoading ? "Loading" : "Submit" }
12
+ | 'map' // Array map: { items.map(item => <li>{item}</li>) }
13
+ | 'complex' // Complex expression requiring runtime evaluation
14
+
15
+ /**
16
+ * Parsed expression information
17
+ */
18
+ export interface ParsedExpression {
19
+ type: ExpressionType
20
+ raw: string // Original expression text
21
+ condition?: string // For conditionals/ternaries: the condition part
22
+ trueBranch?: string // For conditionals/ternaries: the true branch
23
+ falseBranch?: string // For ternaries: the false branch
24
+ arraySource?: string // For map: the array being mapped
25
+ itemName?: string // For map: the item variable name
26
+ indexName?: string // For map: the index variable name (optional)
27
+ mapBody?: string // For map: the body/template of the map
28
+ keyExpression?: string // For map: the key attribute expression
29
+ dependencies: string[] // State variables this expression depends on
30
+ isStatic: boolean // Can be evaluated at build time
31
+ hasJSX: boolean // Contains JSX/HTML elements
32
+ }
33
+
34
+ /**
35
+ * Result of analyzing an expression block in HTML
36
+ */
37
+ export interface ExpressionBlock {
38
+ startIndex: number // Position in source HTML
39
+ endIndex: number // End position in source HTML
40
+ expression: ParsedExpression
41
+ placeholderId: string // Unique ID for DOM placeholder
42
+ }
43
+
44
+ /**
45
+ * Decode HTML entities in a string
46
+ */
47
+ function decodeHtmlEntities(str: string): string {
48
+ return str
49
+ .replace(/&amp;/g, '&')
50
+ .replace(/&lt;/g, '<')
51
+ .replace(/&gt;/g, '>')
52
+ .replace(/&quot;/g, '"')
53
+ .replace(/&#39;/g, "'")
54
+ .replace(/&#x27;/g, "'")
55
+ .replace(/&nbsp;/g, ' ')
56
+ }
57
+
58
+ /**
59
+ * Parse a single expression string (content inside { })
60
+ */
61
+ export function parseExpression(raw: string, declaredStates: Set<string>): ParsedExpression {
62
+ // Decode HTML entities first (parse5 encodes them during serialization)
63
+ const decoded = decodeHtmlEntities(raw)
64
+ const trimmed = decoded.trim()
65
+
66
+ // Track dependencies
67
+ const dependencies: string[] = []
68
+
69
+ // Check if it references declared states
70
+ for (const state of declaredStates) {
71
+ const stateRegex = new RegExp(`\\b${state}\\b`, 'g')
72
+ if (stateRegex.test(trimmed)) {
73
+ dependencies.push(state)
74
+ }
75
+ }
76
+
77
+ // Check for JSX/HTML in the expression
78
+ const hasJSX = /<[a-zA-Z][^>]*>/.test(trimmed) || /\/>/.test(trimmed)
79
+
80
+ // Determine expression type
81
+ const type = detectExpressionType(trimmed)
82
+
83
+ const result: ParsedExpression = {
84
+ type,
85
+ raw: trimmed,
86
+ dependencies,
87
+ isStatic: dependencies.length === 0 && !hasJSX,
88
+ hasJSX
89
+ }
90
+
91
+ // Parse based on type
92
+ switch (type) {
93
+ case 'conditional':
94
+ parseConditional(trimmed, result)
95
+ break
96
+ case 'ternary':
97
+ parseTernary(trimmed, result)
98
+ break
99
+ case 'map':
100
+ parseMap(trimmed, result)
101
+ break
102
+ case 'static':
103
+ // Simple identifier, no additional parsing needed
104
+ break
105
+ case 'complex':
106
+ // Complex expression, evaluate at runtime
107
+ break
108
+ }
109
+
110
+ return result
111
+ }
112
+
113
+ /**
114
+ * Detect the type of expression
115
+ */
116
+ function detectExpressionType(expr: string): ExpressionType {
117
+ const trimmed = expr.trim()
118
+
119
+ // Check for .map() - most specific first
120
+ if (/\.\s*map\s*\(/.test(trimmed)) {
121
+ return 'map'
122
+ }
123
+
124
+ // Check for ternary operator (must handle nested parens/brackets)
125
+ if (hasTernary(trimmed)) {
126
+ return 'ternary'
127
+ }
128
+
129
+ // Check for && or || conditionals (with JSX)
130
+ if (/\s*&&\s*/.test(trimmed) || /\s*\|\|\s*/.test(trimmed)) {
131
+ return 'conditional'
132
+ }
133
+
134
+ // Simple identifier - static reference
135
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmed)) {
136
+ return 'static'
137
+ }
138
+
139
+ // Complex expression
140
+ return 'complex'
141
+ }
142
+
143
+ /**
144
+ * Check if expression contains a ternary at the top level (not nested)
145
+ */
146
+ function hasTernary(expr: string): boolean {
147
+ let depth = 0
148
+ let inString = false
149
+ let stringChar = ''
150
+ let inTemplate = false
151
+
152
+ for (let i = 0; i < expr.length; i++) {
153
+ const char = expr[i]
154
+ const prevChar = i > 0 ? expr[i - 1] : ''
155
+
156
+ // Handle string literals
157
+ if (!inString && (char === '"' || char === "'" || char === '`')) {
158
+ inString = true
159
+ stringChar = char
160
+ if (char === '`') inTemplate = true
161
+ continue
162
+ }
163
+
164
+ if (inString && char === stringChar && prevChar !== '\\') {
165
+ inString = false
166
+ inTemplate = false
167
+ continue
168
+ }
169
+
170
+ if (inString) continue
171
+
172
+ // Track depth for parens, brackets, braces
173
+ if (char === '(' || char === '[' || char === '{') {
174
+ depth++
175
+ continue
176
+ }
177
+ if (char === ')' || char === ']' || char === '}') {
178
+ depth--
179
+ continue
180
+ }
181
+
182
+ // Check for ? at depth 0 (top level)
183
+ if (char === '?' && depth === 0) {
184
+ // Make sure it's not ?. or ??
185
+ const nextChar = i < expr.length - 1 ? expr[i + 1] : ''
186
+ if (nextChar !== '.' && nextChar !== '?') {
187
+ return true
188
+ }
189
+ }
190
+ }
191
+
192
+ return false
193
+ }
194
+
195
+ /**
196
+ * Parse a conditional expression (&&, ||)
197
+ */
198
+ function parseConditional(expr: string, result: ParsedExpression): void {
199
+ // Handle both && and ||
200
+ // Pattern: condition && <element>
201
+ // Pattern: !condition && <element>
202
+
203
+ // Find the && or || operator at the top level
204
+ let depth = 0
205
+ let inString = false
206
+ let stringChar = ''
207
+ let operatorIndex = -1
208
+ let operatorType = ''
209
+
210
+ for (let i = 0; i < expr.length - 1; i++) {
211
+ const char = expr[i]
212
+ const nextChar = expr[i + 1]
213
+
214
+ // Handle string literals
215
+ if (!inString && (char === '"' || char === "'" || char === '`')) {
216
+ inString = true
217
+ stringChar = char
218
+ continue
219
+ }
220
+
221
+ if (inString && char === stringChar && (i === 0 || expr[i - 1] !== '\\')) {
222
+ inString = false
223
+ continue
224
+ }
225
+
226
+ if (inString) continue
227
+
228
+ // Track depth
229
+ if (char === '(' || char === '[' || char === '{' || char === '<') {
230
+ depth++
231
+ continue
232
+ }
233
+ if (char === ')' || char === ']' || char === '}' || char === '>') {
234
+ depth--
235
+ continue
236
+ }
237
+
238
+ // Look for && or || at depth 0
239
+ if (depth === 0) {
240
+ if (char === '&' && nextChar === '&') {
241
+ operatorIndex = i
242
+ operatorType = '&&'
243
+ break
244
+ }
245
+ if (char === '|' && nextChar === '|') {
246
+ operatorIndex = i
247
+ operatorType = '||'
248
+ break
249
+ }
250
+ }
251
+ }
252
+
253
+ if (operatorIndex > -1) {
254
+ result.condition = expr.substring(0, operatorIndex).trim()
255
+ result.trueBranch = expr.substring(operatorIndex + 2).trim()
256
+
257
+ // For || operator, the semantics are reversed
258
+ if (operatorType === '||') {
259
+ // a || b means: if !a then b
260
+ // So we invert: condition becomes !condition, trueBranch stays
261
+ result.condition = `!(${result.condition})`
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Parse a ternary expression (condition ? true : false)
268
+ */
269
+ function parseTernary(expr: string, result: ParsedExpression): void {
270
+ let depth = 0
271
+ let inString = false
272
+ let stringChar = ''
273
+ let questionIndex = -1
274
+ let colonIndex = -1
275
+
276
+ for (let i = 0; i < expr.length; i++) {
277
+ const char = expr[i]
278
+ const prevChar = i > 0 ? expr[i - 1] : ''
279
+ const nextChar = i < expr.length - 1 ? expr[i + 1] : ''
280
+
281
+ // Handle string literals
282
+ if (!inString && (char === '"' || char === "'" || char === '`')) {
283
+ inString = true
284
+ stringChar = char
285
+ continue
286
+ }
287
+
288
+ if (inString && char === stringChar && prevChar !== '\\') {
289
+ inString = false
290
+ continue
291
+ }
292
+
293
+ if (inString) continue
294
+
295
+ // Track depth
296
+ if (char === '(' || char === '[' || char === '{') {
297
+ depth++
298
+ continue
299
+ }
300
+ if (char === ')' || char === ']' || char === '}') {
301
+ depth--
302
+ continue
303
+ }
304
+
305
+ // Find ? at depth 0
306
+ if (char === '?' && depth === 0 && questionIndex === -1) {
307
+ // Make sure it's not ?. or ??
308
+ if (nextChar !== '.' && nextChar !== '?') {
309
+ questionIndex = i
310
+ }
311
+ }
312
+
313
+ // Find : at depth 0 (after ?)
314
+ if (char === ':' && depth === 0 && questionIndex > -1 && colonIndex === -1) {
315
+ colonIndex = i
316
+ }
317
+ }
318
+
319
+ if (questionIndex > -1 && colonIndex > -1) {
320
+ result.condition = expr.substring(0, questionIndex).trim()
321
+ result.trueBranch = expr.substring(questionIndex + 1, colonIndex).trim()
322
+ result.falseBranch = expr.substring(colonIndex + 1).trim()
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Parse a map expression (array.map(item => <element>))
328
+ */
329
+ function parseMap(expr: string, result: ParsedExpression): void {
330
+ // Pattern: arraySource.map((item, index?) => body)
331
+ // Pattern: arraySource.map(item => body)
332
+
333
+ const mapMatch = expr.match(/^(.+?)\s*\.\s*map\s*\(\s*\(?([^)=,]+)(?:\s*,\s*([^)=]+))?\)?\s*=>\s*(.+)\)$/s)
334
+
335
+ if (mapMatch) {
336
+ result.arraySource = mapMatch[1]?.trim()
337
+ result.itemName = mapMatch[2]?.trim()
338
+ result.indexName = mapMatch[3]?.trim()
339
+ result.mapBody = mapMatch[4]?.trim()
340
+
341
+ // Extract key from map body if present
342
+ const keyMatch = result.mapBody?.match(/key\s*=\s*\{([^}]+)\}|key\s*=\s*"([^"]+)"|key\s*=\s*'([^']+)'/)
343
+ if (keyMatch) {
344
+ result.keyExpression = (keyMatch[1] || keyMatch[2] || keyMatch[3])?.trim()
345
+ }
346
+ } else {
347
+ // Try alternate pattern without parens around params
348
+ const altMatch = expr.match(/^(.+?)\s*\.\s*map\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>\s*(.+)\)$/s)
349
+ if (altMatch) {
350
+ result.arraySource = altMatch[1]?.trim()
351
+ result.itemName = altMatch[2]?.trim()
352
+ result.mapBody = altMatch[3]?.trim()
353
+
354
+ // Extract key
355
+ const keyMatch = result.mapBody?.match(/key\s*=\s*\{([^}]+)\}|key\s*=\s*"([^"]+)"|key\s*=\s*'([^']+)'/)
356
+ if (keyMatch) {
357
+ result.keyExpression = (keyMatch[1] || keyMatch[2] || keyMatch[3])?.trim()
358
+ }
359
+ }
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Extract a balanced expression starting at position i in html
365
+ * Returns the content inside { } and the end position
366
+ */
367
+ function extractBalancedExpression(html: string, startIndex: number): { content: string; endIndex: number } | null {
368
+ if (html[startIndex] !== '{') return null
369
+
370
+ let depth = 1
371
+ let i = startIndex + 1
372
+ let inString = false
373
+ let stringChar = ''
374
+
375
+ while (i < html.length && depth > 0) {
376
+ const char = html[i]
377
+ const prevChar = i > 0 ? html[i - 1] : ''
378
+
379
+ // Handle string literals
380
+ if (!inString && (char === '"' || char === "'" || char === '`')) {
381
+ inString = true
382
+ stringChar = char
383
+ i++
384
+ continue
385
+ }
386
+
387
+ if (inString) {
388
+ if (char === stringChar && prevChar !== '\\') {
389
+ inString = false
390
+ }
391
+ i++
392
+ continue
393
+ }
394
+
395
+ // Track brace depth
396
+ if (char === '{') {
397
+ depth++
398
+ } else if (char === '}') {
399
+ depth--
400
+ }
401
+
402
+ i++
403
+ }
404
+
405
+ if (depth !== 0) return null
406
+
407
+ return {
408
+ content: html.substring(startIndex + 1, i - 1),
409
+ endIndex: i
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Extract all expression blocks from HTML content
415
+ */
416
+ export function extractExpressionBlocks(
417
+ html: string,
418
+ declaredStates: Set<string>
419
+ ): ExpressionBlock[] {
420
+ const blocks: ExpressionBlock[] = []
421
+ let placeholderCounter = 0
422
+
423
+ // First, mark regions to skip (script, style)
424
+ const skipRegions: Array<{ start: number; end: number }> = []
425
+
426
+ let match
427
+ const scriptRegex = /<script[^>]*>[\s\S]*?<\/script>/gi
428
+ while ((match = scriptRegex.exec(html)) !== null) {
429
+ skipRegions.push({ start: match.index, end: match.index + match[0].length })
430
+ }
431
+
432
+ const styleRegex = /<style[^>]*>[\s\S]*?<\/style>/gi
433
+ while ((match = styleRegex.exec(html)) !== null) {
434
+ skipRegions.push({ start: match.index, end: match.index + match[0].length })
435
+ }
436
+
437
+ // Find all opening braces and extract balanced expressions
438
+ for (let i = 0; i < html.length; i++) {
439
+ if (html[i] !== '{') continue
440
+
441
+ // Check if this is in a skip region
442
+ const inSkipRegion = skipRegions.some(region =>
443
+ i >= region.start && i < region.end
444
+ )
445
+ if (inSkipRegion) continue
446
+
447
+ // Check if this is inside an attribute value
448
+ // Look backwards for attribute pattern: attrName="... or attrName='...
449
+ let j = i - 1
450
+ let inAttrValue = false
451
+ while (j >= 0 && html[j] !== '<' && html[j] !== '>') {
452
+ if (html[j] === '"' || html[j] === "'") {
453
+ // Found a quote, check if there's an = before it
454
+ let k = j - 1
455
+ while (k >= 0 && (html[k] === ' ' || html[k] === '\t' || html[k] === '\n')) k--
456
+ if (k >= 0 && html[k] === '=') {
457
+ inAttrValue = true
458
+ break
459
+ }
460
+ }
461
+ j--
462
+ }
463
+ if (inAttrValue) continue
464
+
465
+ // Extract balanced expression
466
+ const extracted = extractBalancedExpression(html, i)
467
+ if (!extracted) continue
468
+
469
+ const exprContent = extracted.content
470
+ const trimmed = exprContent.trim()
471
+
472
+ // Skip empty expressions
473
+ if (!trimmed) continue
474
+
475
+ // Skip simple state references - they're handled by the existing binding system
476
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmed) && declaredStates.has(trimmed)) {
477
+ continue
478
+ }
479
+
480
+ const expression = parseExpression(exprContent, declaredStates)
481
+
482
+ // Only process non-static expressions (static ones are simple bindings)
483
+ if (expression.type !== 'static' || expression.hasJSX) {
484
+ blocks.push({
485
+ startIndex: i,
486
+ endIndex: extracted.endIndex,
487
+ expression,
488
+ placeholderId: `zen-expr-${placeholderCounter++}`
489
+ })
490
+ }
491
+
492
+ // Skip to end of this expression
493
+ i = extracted.endIndex - 1
494
+ }
495
+
496
+ return blocks
497
+ }
498
+
499
+ /**
500
+ * Convert JSX-like syntax to DOM creation code
501
+ */
502
+ export function jsxToCreateElement(jsx: string): string {
503
+ // Simple JSX to createElement conversion
504
+ // <div className="test">content</div> -> createElement('div', { className: 'test' }, 'content')
505
+
506
+ // Handle self-closing tags
507
+ jsx = jsx.replace(/<([a-zA-Z][a-zA-Z0-9]*)\s*([^>]*?)\/>/g, (_, tag, attrs) => {
508
+ return `<${tag} ${attrs}></${tag}>`
509
+ })
510
+
511
+ // Parse and convert
512
+ return `__zen_jsx(${JSON.stringify(jsx)})`
513
+ }
514
+
515
+ /**
516
+ * Generate runtime code for an expression
517
+ */
518
+ export function generateExpressionRuntime(expr: ParsedExpression, placeholderId: string): string {
519
+ switch (expr.type) {
520
+ case 'conditional':
521
+ return generateConditionalRuntime(expr, placeholderId)
522
+ case 'ternary':
523
+ return generateTernaryRuntime(expr, placeholderId)
524
+ case 'map':
525
+ return generateMapRuntime(expr, placeholderId)
526
+ case 'complex':
527
+ return generateComplexRuntime(expr, placeholderId)
528
+ default:
529
+ return ''
530
+ }
531
+ }
532
+
533
+ function generateConditionalRuntime(expr: ParsedExpression, placeholderId: string): string {
534
+ const condition = expr.condition || 'false'
535
+ const trueBranch = expr.trueBranch || ''
536
+
537
+ // Escape the trueBranch for JSON
538
+ const escapedBranch = JSON.stringify(trueBranch)
539
+
540
+ // Use window.__zen_eval_expr to evaluate condition in global scope
541
+ const conditionCode = `window.__zen_eval_expr(${JSON.stringify(condition)})`
542
+
543
+ return `
544
+ // Conditional expression: ${condition.replace(/\n/g, ' ')} && ...
545
+ (function() {
546
+ const placeholder = document.querySelector('[data-zen-expr="${placeholderId}"]');
547
+ if (!placeholder) return;
548
+
549
+ let currentElement = null;
550
+
551
+ function updateConditional() {
552
+ try {
553
+ const show = Boolean(${conditionCode});
554
+
555
+ if (show && !currentElement) {
556
+ // Create and insert the element
557
+ currentElement = window.__zen_parse_jsx(${escapedBranch});
558
+ if (currentElement && placeholder.parentNode) {
559
+ placeholder.parentNode.insertBefore(currentElement, placeholder.nextSibling);
560
+ }
561
+ } else if (!show && currentElement) {
562
+ // Remove the element
563
+ if (currentElement.parentNode) {
564
+ currentElement.parentNode.removeChild(currentElement);
565
+ }
566
+ currentElement = null;
567
+ }
568
+ } catch (e) {
569
+ console.warn('[Zenith] Conditional evaluation error:', e);
570
+ }
571
+ }
572
+
573
+ // Initial render after DOM is ready
574
+ if (document.readyState === 'loading') {
575
+ document.addEventListener('DOMContentLoaded', updateConditional);
576
+ } else {
577
+ setTimeout(updateConditional, 0);
578
+ }
579
+
580
+ // Register for state updates
581
+ window.__zen_register_expression_update('${placeholderId}', updateConditional, ${JSON.stringify(expr.dependencies)});
582
+ })();
583
+ `
584
+ }
585
+
586
+ function generateTernaryRuntime(expr: ParsedExpression, placeholderId: string): string {
587
+ const condition = expr.condition || 'false'
588
+ let trueBranch = expr.trueBranch || '""'
589
+ let falseBranch = expr.falseBranch || '""'
590
+
591
+ // Check if branches are string literals or expressions
592
+ const trueIsString = /^["']/.test(trueBranch.trim())
593
+ const falseIsString = /^["']/.test(falseBranch.trim())
594
+
595
+ // If branches contain JSX, wrap them for parsing
596
+ const trueHasJSX = /<[a-zA-Z]/.test(trueBranch)
597
+ const falseHasJSX = /<[a-zA-Z]/.test(falseBranch)
598
+
599
+ // Use window.__zen_eval_expr to evaluate expressions in global scope
600
+ const conditionCode = `window.__zen_eval_expr(${JSON.stringify(condition)})`
601
+ const trueBranchCode = trueHasJSX
602
+ ? `window.__zen_parse_jsx(${JSON.stringify(trueBranch)})`
603
+ : trueIsString
604
+ ? trueBranch
605
+ : `window.__zen_eval_expr(${JSON.stringify(trueBranch)})`
606
+ const falseBranchCode = falseHasJSX
607
+ ? `window.__zen_parse_jsx(${JSON.stringify(falseBranch)})`
608
+ : falseIsString
609
+ ? falseBranch
610
+ : `window.__zen_eval_expr(${JSON.stringify(falseBranch)})`
611
+
612
+ return `
613
+ // Ternary expression: ${condition.replace(/\n/g, ' ')} ? ... : ...
614
+ (function() {
615
+ const placeholder = document.querySelector('[data-zen-expr="${placeholderId}"]');
616
+ if (!placeholder) return;
617
+
618
+ function updateTernary() {
619
+ try {
620
+ const conditionResult = Boolean(${conditionCode});
621
+ let result = conditionResult ? ${trueBranchCode} : ${falseBranchCode};
622
+
623
+ // Clear placeholder content
624
+ placeholder.innerHTML = '';
625
+
626
+ if (result === null || result === undefined || result === false) {
627
+ // Empty
628
+ } else if (typeof result === 'string' || typeof result === 'number') {
629
+ placeholder.textContent = String(result);
630
+ } else if (result instanceof Node) {
631
+ placeholder.appendChild(result);
632
+ } else if (result && typeof result === 'object') {
633
+ // Try to render as JSX
634
+ const element = window.__zen_parse_jsx(String(result));
635
+ if (element) placeholder.appendChild(element);
636
+ }
637
+ } catch (e) {
638
+ console.warn('[Zenith] Ternary evaluation error:', e);
639
+ }
640
+ }
641
+
642
+ // Initial render after DOM is ready
643
+ if (document.readyState === 'loading') {
644
+ document.addEventListener('DOMContentLoaded', updateTernary);
645
+ } else {
646
+ setTimeout(updateTernary, 0);
647
+ }
648
+
649
+ // Register for state updates
650
+ window.__zen_register_expression_update('${placeholderId}', updateTernary, ${JSON.stringify(expr.dependencies)});
651
+ })();
652
+ `
653
+ }
654
+
655
+ function generateMapRuntime(expr: ParsedExpression, placeholderId: string): string {
656
+ const arraySource = expr.arraySource || '[]'
657
+ const itemName = expr.itemName || 'item'
658
+ const indexName = expr.indexName || 'index'
659
+ const mapBody = expr.mapBody || '""'
660
+ const keyExpr = expr.keyExpression || indexName
661
+
662
+ // Process the map body to replace item/index references
663
+ const escapedMapBody = JSON.stringify(mapBody)
664
+
665
+ return `
666
+ // Map expression: ${arraySource}.map(${itemName} => ...)
667
+ (function() {
668
+ const placeholder = document.querySelector('[data-zen-expr="${placeholderId}"]');
669
+ if (!placeholder) return;
670
+
671
+ const itemCache = new Map(); // key -> element
672
+ const mapBodyTemplate = ${escapedMapBody};
673
+
674
+ function updateMap() {
675
+ try {
676
+ const array = window.__zen_eval_expr(${JSON.stringify(arraySource)});
677
+ if (!Array.isArray(array)) {
678
+ console.warn('[Zenith] Map source is not an array:', array);
679
+ return;
680
+ }
681
+
682
+ const newKeys = new Set();
683
+ const parent = placeholder.parentNode;
684
+ if (!parent) return;
685
+
686
+ // Create fragment for new elements
687
+ const elementsInOrder = [];
688
+
689
+ array.forEach(function(${itemName}, ${indexName}) {
690
+ const key = String(${keyExpr.replace(/`/g, '\\`').replace(/\${/g, '\\${')});
691
+ newKeys.add(key);
692
+
693
+ // Create context for template processing
694
+ const context = {};
695
+ context['${itemName}'] = ${itemName};
696
+ context['${indexName}'] = ${indexName};
697
+
698
+ let element = itemCache.get(key);
699
+ if (!element) {
700
+ // Create new element by processing template with context
701
+ element = window.__zen_parse_jsx(mapBodyTemplate, context);
702
+ if (element && element.setAttribute) {
703
+ element.setAttribute('data-zen-key', key);
704
+ element.setAttribute('data-zen-map', '${placeholderId}');
705
+ }
706
+ itemCache.set(key, element);
707
+ }
708
+
709
+ if (element) {
710
+ elementsInOrder.push(element);
711
+ }
712
+ });
713
+
714
+ // Remove old items no longer in array
715
+ for (const [key, element] of itemCache.entries()) {
716
+ if (!newKeys.has(key)) {
717
+ if (element && element.parentNode) {
718
+ element.parentNode.removeChild(element);
719
+ }
720
+ itemCache.delete(key);
721
+ }
722
+ }
723
+
724
+ // Remove all existing mapped elements for this expression
725
+ const existingMapped = parent.querySelectorAll('[data-zen-map="${placeholderId}"]');
726
+ existingMapped.forEach(function(el) {
727
+ if (el.parentNode) el.parentNode.removeChild(el);
728
+ });
729
+
730
+ // Insert elements in order after placeholder
731
+ let insertPoint = placeholder;
732
+ for (const el of elementsInOrder) {
733
+ if (el && insertPoint.parentNode) {
734
+ insertPoint.parentNode.insertBefore(el, insertPoint.nextSibling);
735
+ insertPoint = el;
736
+ }
737
+ }
738
+ } catch (e) {
739
+ console.warn('[Zenith] Map evaluation error:', e);
740
+ }
741
+ }
742
+
743
+ // Initial render after DOM is ready
744
+ if (document.readyState === 'loading') {
745
+ document.addEventListener('DOMContentLoaded', updateMap);
746
+ } else {
747
+ updateMap();
748
+ }
749
+
750
+ // Register for state updates
751
+ window.__zen_register_expression_update('${placeholderId}', updateMap, ${JSON.stringify(expr.dependencies)});
752
+ })();
753
+ `
754
+ }
755
+
756
+ function generateComplexRuntime(expr: ParsedExpression, placeholderId: string): string {
757
+ const hasJSX = /<[a-zA-Z]/.test(expr.raw)
758
+
759
+ // For complex expressions, use window.__zen_eval_expr to evaluate in global scope
760
+ // or parse JSX if it contains HTML
761
+ const evalCode = hasJSX
762
+ ? `window.__zen_parse_jsx(${JSON.stringify(expr.raw)})`
763
+ : `window.__zen_eval_expr(${JSON.stringify(expr.raw)})`
764
+
765
+ return `
766
+ // Complex expression: ${expr.raw.replace(/\n/g, ' ').substring(0, 50)}...
767
+ (function() {
768
+ const placeholder = document.querySelector('[data-zen-expr="${placeholderId}"]');
769
+ if (!placeholder) return;
770
+
771
+ function updateExpression() {
772
+ try {
773
+ const result = ${evalCode};
774
+
775
+ // Clear current content
776
+ placeholder.innerHTML = '';
777
+
778
+ if (result === null || result === undefined || result === false) {
779
+ // Empty - leave placeholder empty
780
+ } else if (typeof result === 'string' || typeof result === 'number') {
781
+ placeholder.textContent = String(result);
782
+ } else if (result instanceof Node) {
783
+ placeholder.appendChild(result);
784
+ } else if (Array.isArray(result)) {
785
+ // Handle array of elements
786
+ result.forEach(function(item) {
787
+ if (item instanceof Node) {
788
+ placeholder.appendChild(item);
789
+ } else if (typeof item === 'string' || typeof item === 'number') {
790
+ placeholder.appendChild(document.createTextNode(String(item)));
791
+ }
792
+ });
793
+ } else if (result && typeof result === 'object') {
794
+ // Try to render as JSX string
795
+ const element = window.__zen_parse_jsx(String(result));
796
+ if (element) placeholder.appendChild(element);
797
+ }
798
+ } catch (e) {
799
+ console.warn('[Zenith] Expression evaluation error:', e);
800
+ }
801
+ }
802
+
803
+ // Initial render after DOM is ready
804
+ if (document.readyState === 'loading') {
805
+ document.addEventListener('DOMContentLoaded', updateExpression);
806
+ } else {
807
+ setTimeout(updateExpression, 0);
808
+ }
809
+
810
+ // Register for state updates
811
+ window.__zen_register_expression_update('${placeholderId}', updateExpression, ${JSON.stringify(expr.dependencies)});
812
+ })();
813
+ `
814
+ }
815
+
816
+ /**
817
+ * Transform HTML to replace expression blocks with placeholders
818
+ */
819
+ export function transformExpressionBlocks(
820
+ html: string,
821
+ blocks: ExpressionBlock[]
822
+ ): string {
823
+ if (blocks.length === 0) return html
824
+
825
+ // Sort blocks by start index in reverse order (to avoid index shifting)
826
+ const sortedBlocks = [...blocks].sort((a, b) => b.startIndex - a.startIndex)
827
+
828
+ let result = html
829
+ for (const block of sortedBlocks) {
830
+ const placeholder = `<span data-zen-expr="${block.placeholderId}" style="display:contents;"></span>`
831
+ result = result.substring(0, block.startIndex) + placeholder + result.substring(block.endIndex)
832
+ }
833
+
834
+ return result
835
+ }
836
+
837
+ /**
838
+ * Attribute expression binding
839
+ */
840
+ export interface AttributeExpressionBinding {
841
+ elementSelector: string // Selector to find the element
842
+ attributeName: string // Name of the attribute (class, src, href, etc.)
843
+ expression: string // The expression to evaluate
844
+ dependencies: string[] // State variables this expression depends on
845
+ bindingId: string // Unique binding ID
846
+ }
847
+
848
+ /**
849
+ * Extract attribute expressions from HTML
850
+ * Handles: attr={expression}, className={expr ? "a" : "b"}, src={user.avatarUrl}
851
+ */
852
+ export function extractAttributeExpressions(
853
+ html: string,
854
+ declaredStates: Set<string>
855
+ ): { transformedHtml: string; bindings: AttributeExpressionBinding[] } {
856
+ const bindings: AttributeExpressionBinding[] = []
857
+ let bindingCounter = 0
858
+
859
+ // Skip script and style content
860
+ const skipRegions: Array<{ start: number; end: number }> = []
861
+
862
+ let match
863
+ const scriptRegex = /<script[^>]*>[\s\S]*?<\/script>/gi
864
+ while ((match = scriptRegex.exec(html)) !== null) {
865
+ skipRegions.push({ start: match.index, end: match.index + match[0].length })
866
+ }
867
+
868
+ const styleRegex = /<style[^>]*>[\s\S]*?<\/style>/gi
869
+ while ((match = styleRegex.exec(html)) !== null) {
870
+ skipRegions.push({ start: match.index, end: match.index + match[0].length })
871
+ }
872
+
873
+ // Match attributes with expression values: attr={...}
874
+ // This includes: class={}, className={}, src={}, href={}, disabled={}, etc.
875
+ const attrExprRegex = /(\s+)([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*\{([^}]+)\}/g
876
+
877
+ let transformedHtml = html
878
+ const replacements: Array<{ start: number; end: number; replacement: string; binding: AttributeExpressionBinding }> = []
879
+
880
+ while ((match = attrExprRegex.exec(html)) !== null) {
881
+ const start = match.index
882
+ const end = start + match[0].length
883
+
884
+ // Check if in skip region
885
+ const shouldSkip = skipRegions.some(region =>
886
+ start >= region.start && start < region.end
887
+ )
888
+ if (shouldSkip) continue
889
+
890
+ const whitespace = match[1] || ' '
891
+ const attrName = match[2]
892
+ const expression = match[3]?.trim()
893
+
894
+ if (!attrName || !expression) continue
895
+
896
+ // Skip :class and :value (handled by existing binding system)
897
+ if (attrName === ':class' || attrName === ':value') continue
898
+
899
+ // Skip style attribute with object syntax for now
900
+ if (attrName === 'style' && expression.startsWith('{')) continue
901
+
902
+ // Find dependencies in the expression
903
+ const dependencies: string[] = []
904
+ for (const state of declaredStates) {
905
+ const stateRegex = new RegExp(`\\b${state}\\b`, 'g')
906
+ if (stateRegex.test(expression)) {
907
+ dependencies.push(state)
908
+ }
909
+ }
910
+
911
+ // Only create binding if there are dependencies (dynamic expression)
912
+ if (dependencies.length > 0) {
913
+ const bindingId = `zen-attr-${bindingCounter++}`
914
+
915
+ // Map className to class
916
+ const normalizedAttrName = attrName === 'className' ? 'class' : attrName
917
+
918
+ const binding: AttributeExpressionBinding = {
919
+ elementSelector: `[data-zen-attr-bind="${bindingId}"]`,
920
+ attributeName: normalizedAttrName,
921
+ expression,
922
+ dependencies,
923
+ bindingId
924
+ }
925
+
926
+ bindings.push(binding)
927
+
928
+ // Replace with static attribute and add binding marker
929
+ // For initial value, we'll evaluate it at runtime
930
+ const replacement = `${whitespace}data-zen-attr-bind="${bindingId}" data-zen-attr-name="${normalizedAttrName}" data-zen-attr-expr="${escapeAttrValue(expression)}"`
931
+
932
+ replacements.push({ start, end, replacement, binding })
933
+ } else {
934
+ // Static expression - just evaluate and inline
935
+ // For now, leave as-is and let the browser handle it
936
+ }
937
+ }
938
+
939
+ // Apply replacements in reverse order to preserve indices
940
+ replacements.sort((a, b) => b.start - a.start)
941
+ for (const r of replacements) {
942
+ transformedHtml = transformedHtml.substring(0, r.start) + r.replacement + transformedHtml.substring(r.end)
943
+ }
944
+
945
+ return { transformedHtml, bindings }
946
+ }
947
+
948
+ /**
949
+ * Escape attribute value for HTML
950
+ */
951
+ function escapeAttrValue(value: string): string {
952
+ return value
953
+ .replace(/&/g, '&amp;')
954
+ .replace(/"/g, '&quot;')
955
+ .replace(/'/g, '&#39;')
956
+ .replace(/</g, '&lt;')
957
+ .replace(/>/g, '&gt;')
958
+ }
959
+
960
+ /**
961
+ * Generate runtime code for attribute expression bindings
962
+ */
963
+ export function generateAttributeExpressionRuntime(bindings: AttributeExpressionBinding[]): string {
964
+ if (bindings.length === 0) return ''
965
+
966
+ const bindingCodes = bindings.map(binding => {
967
+ const isBoolean = ['disabled', 'checked', 'readonly', 'required', 'hidden'].includes(binding.attributeName)
968
+ const isClass = binding.attributeName === 'class'
969
+
970
+ return `
971
+ // Attribute binding: ${binding.attributeName}={${binding.expression.substring(0, 30)}...}
972
+ (function() {
973
+ const el = document.querySelector('[data-zen-attr-bind="${binding.bindingId}"]');
974
+ if (!el) return;
975
+
976
+ function updateAttribute() {
977
+ try {
978
+ const value = window.__zen_eval_expr(${JSON.stringify(binding.expression)});
979
+ ${isBoolean ? `
980
+ // Boolean attribute
981
+ if (value) {
982
+ el.setAttribute('${binding.attributeName}', '');
983
+ } else {
984
+ el.removeAttribute('${binding.attributeName}');
985
+ }` : isClass ? `
986
+ // Class attribute - handle string, object, or array
987
+ if (typeof value === 'string') {
988
+ el.className = value;
989
+ } else if (Array.isArray(value)) {
990
+ el.className = value.filter(Boolean).join(' ');
991
+ } else if (value && typeof value === 'object') {
992
+ el.className = Object.entries(value)
993
+ .filter(([_, v]) => v)
994
+ .map(([k]) => k)
995
+ .join(' ');
996
+ } else if (value === null || value === undefined || value === false) {
997
+ el.className = '';
998
+ } else {
999
+ el.className = String(value);
1000
+ }` : `
1001
+ // Regular attribute
1002
+ if (value === null || value === undefined || value === false) {
1003
+ el.removeAttribute('${binding.attributeName}');
1004
+ } else {
1005
+ el.setAttribute('${binding.attributeName}', String(value));
1006
+ }`}
1007
+ } catch (e) {
1008
+ console.warn('[Zenith] Attribute expression error:', e);
1009
+ }
1010
+ }
1011
+
1012
+ // Initial update
1013
+ if (document.readyState === 'loading') {
1014
+ document.addEventListener('DOMContentLoaded', updateAttribute);
1015
+ } else {
1016
+ updateAttribute();
1017
+ }
1018
+
1019
+ // Register for state updates
1020
+ window.__zen_register_expression_update('${binding.bindingId}', updateAttribute, ${JSON.stringify(binding.dependencies)});
1021
+ })();`
1022
+ })
1023
+
1024
+ return `
1025
+ // Attribute Expression Bindings
1026
+ ${bindingCodes.join('\n')}
1027
+ `
1028
+ }
1029
+
1030
+ /**
1031
+ * Generate the expression runtime helper code
1032
+ */
1033
+ export function generateExpressionRuntimeHelpers(): string {
1034
+ return `
1035
+ // Zenith Dynamic Expression Runtime
1036
+ (function() {
1037
+ // Expression update registry
1038
+ const expressionUpdaters = new Map(); // placeholderId -> { update: fn, dependencies: string[] }
1039
+
1040
+ // Register an expression updater
1041
+ window.__zen_register_expression_update = function(placeholderId, updateFn, dependencies) {
1042
+ expressionUpdaters.set(placeholderId, { update: updateFn, dependencies });
1043
+ };
1044
+
1045
+ // Trigger updates for expressions that depend on a state
1046
+ window.__zen_trigger_expression_updates = function(stateName) {
1047
+ for (const [id, info] of expressionUpdaters.entries()) {
1048
+ // Check if this expression depends on the changed state
1049
+ const deps = info.dependencies;
1050
+ // Also check for partial matches (instance-scoped state)
1051
+ const shouldUpdate = deps.some(dep =>
1052
+ dep === stateName ||
1053
+ stateName.includes(dep) ||
1054
+ dep.includes(stateName)
1055
+ );
1056
+ if (shouldUpdate) {
1057
+ try {
1058
+ info.update();
1059
+ } catch (e) {
1060
+ console.warn('[Zenith] Expression update error:', e);
1061
+ }
1062
+ }
1063
+ }
1064
+ };
1065
+
1066
+ // Evaluate expression in global scope (accesses window properties)
1067
+ window.__zen_eval_expr = function(expr) {
1068
+ try {
1069
+ // Use Function constructor with 'with(window)' to access window properties
1070
+ // This allows expressions like 'users.length' to access window.users
1071
+ return (new Function('with(window) { return (' + expr + '); }'))();
1072
+ } catch (e) {
1073
+ console.warn('[Zenith] Expression evaluation error:', expr, e);
1074
+ return undefined;
1075
+ }
1076
+ };
1077
+
1078
+ // Parse JSX/HTML string into DOM element(s)
1079
+ window.__zen_parse_jsx = function(jsx, context) {
1080
+ if (jsx === null || jsx === undefined || jsx === false) return null;
1081
+ if (typeof jsx === 'number') jsx = String(jsx);
1082
+ if (typeof jsx !== 'string') return null;
1083
+
1084
+ // Apply context (for map iterations)
1085
+ let processed = jsx;
1086
+ if (context) {
1087
+ for (const [key, value] of Object.entries(context)) {
1088
+ // Replace {key} with value in text content
1089
+ const textRegex = new RegExp('\\\\{\\\\s*' + key + '\\\\s*\\\\}', 'g');
1090
+ const safeValue = value === null || value === undefined ? '' : String(value);
1091
+ processed = processed.replace(textRegex, safeValue);
1092
+
1093
+ // Replace key references in attribute expressions
1094
+ const attrRegex = new RegExp('\\\\{' + key + '\\\\}', 'g');
1095
+ processed = processed.replace(attrRegex, safeValue);
1096
+ }
1097
+ }
1098
+
1099
+ // Parse HTML string
1100
+ const template = document.createElement('template');
1101
+ template.innerHTML = processed.trim();
1102
+
1103
+ // Return single element or document fragment for multiple
1104
+ if (template.content.childNodes.length === 1) {
1105
+ return template.content.firstChild;
1106
+ }
1107
+ return template.content;
1108
+ };
1109
+
1110
+ // Create element from tag, attributes, and children
1111
+ window.__zen_create_element = function(tag, attrs, ...children) {
1112
+ const el = document.createElement(tag);
1113
+
1114
+ // Set attributes
1115
+ if (attrs) {
1116
+ for (const [key, value] of Object.entries(attrs)) {
1117
+ if (key === 'className') {
1118
+ el.className = value;
1119
+ } else if (key === 'style' && typeof value === 'object') {
1120
+ Object.assign(el.style, value);
1121
+ } else if (key.startsWith('on') && typeof value === 'function') {
1122
+ el.addEventListener(key.slice(2).toLowerCase(), value);
1123
+ } else if (value !== null && value !== undefined && value !== false) {
1124
+ el.setAttribute(key, String(value));
1125
+ }
1126
+ }
1127
+ }
1128
+
1129
+ // Append children
1130
+ for (const child of children.flat()) {
1131
+ if (child === null || child === undefined || child === false) continue;
1132
+ if (typeof child === 'string' || typeof child === 'number') {
1133
+ el.appendChild(document.createTextNode(String(child)));
1134
+ } else if (child instanceof Node) {
1135
+ el.appendChild(child);
1136
+ }
1137
+ }
1138
+
1139
+ return el;
1140
+ };
1141
+
1142
+ // Track state property access
1143
+ window.__zen_track_state = function(stateName) {
1144
+ // Used for dependency tracking
1145
+ };
1146
+ })();
1147
+ `
1148
+ }
1149
+