@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,102 @@
1
+ /**
2
+ * Map Expression Detection
3
+ *
4
+ * Phase 7: Detects .map() expressions and extracts loop context information
5
+ *
6
+ * This module analyzes expression code to detect map expressions like:
7
+ * - todoItems.map(todo => ...)
8
+ * - notifications.map((n, index) => ...)
9
+ *
10
+ * It extracts:
11
+ * - The array source (todoItems, notifications)
12
+ * - Loop variable names (todo, n, index)
13
+ * - The map body/template
14
+ */
15
+
16
+ import type { ExpressionIR } from '../ir/types'
17
+
18
+ /**
19
+ * Detected map expression information
20
+ */
21
+ export interface MapExpressionInfo {
22
+ isMap: boolean
23
+ arraySource?: string // e.g., 'todoItems'
24
+ itemVariable?: string // e.g., 'todo'
25
+ indexVariable?: string // e.g., 'index'
26
+ mapBody?: string // The template/body inside the map
27
+ fullExpression?: string // The full expression code
28
+ }
29
+
30
+ /**
31
+ * Detect if an expression is a map expression and extract loop context
32
+ *
33
+ * Patterns detected:
34
+ * - arraySource.map(item => body)
35
+ * - arraySource.map((item, index) => body)
36
+ * - arraySource.map(item => (body))
37
+ */
38
+ export function detectMapExpression(expr: ExpressionIR): MapExpressionInfo {
39
+ const { code } = expr
40
+
41
+ // Pattern: arraySource.map(item => body)
42
+ // Pattern: arraySource.map((item, index) => body)
43
+ // Pattern: arraySource.map(item => (body))
44
+ const mapPattern = /^(.+?)\.\s*map\s*\(\s*\(?([^)=,\s]+)(?:\s*,\s*([^)=,\s]+))?\s*\)?\s*=>\s*(.+?)\)?$/s
45
+
46
+ const match = code.match(mapPattern)
47
+ if (!match) {
48
+ return { isMap: false }
49
+ }
50
+
51
+ const arraySource = match[1]?.trim()
52
+ const itemVariable = match[2]?.trim()
53
+ const indexVariable = match[3]?.trim()
54
+ const mapBody = match[4]?.trim()
55
+
56
+ if (!arraySource || !itemVariable || !mapBody) {
57
+ return { isMap: false }
58
+ }
59
+
60
+ return {
61
+ isMap: true,
62
+ arraySource,
63
+ itemVariable,
64
+ indexVariable,
65
+ mapBody,
66
+ fullExpression: code
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Extract loop variables from a map expression
72
+ * Returns array of variable names in order: [itemVariable, indexVariable?]
73
+ */
74
+ export function extractLoopVariables(mapInfo: MapExpressionInfo): string[] {
75
+ if (!mapInfo.isMap || !mapInfo.itemVariable) {
76
+ return []
77
+ }
78
+
79
+ const vars = [mapInfo.itemVariable]
80
+ if (mapInfo.indexVariable) {
81
+ vars.push(mapInfo.indexVariable)
82
+ }
83
+
84
+ return vars
85
+ }
86
+
87
+ /**
88
+ * Check if an expression references a loop variable
89
+ * Used to determine if an expression needs loop context
90
+ */
91
+ export function referencesLoopVariable(exprCode: string, loopVars: string[]): boolean {
92
+ for (const loopVar of loopVars) {
93
+ // Match variable references: loopVar.property, loopVar, etc.
94
+ // Use word boundaries to avoid partial matches
95
+ const pattern = new RegExp(`\\b${loopVar}\\b`)
96
+ if (pattern.test(exprCode)) {
97
+ return true
98
+ }
99
+ }
100
+ return false
101
+ }
102
+
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Script Parser
3
+ *
4
+ * Extracts <script> blocks from .zen files
5
+ * Phase 1: Only extracts raw content, no evaluation
6
+ */
7
+
8
+ import type { ScriptIR } from '../ir/types'
9
+
10
+ export function parseScript(html: string): ScriptIR | null {
11
+ // Extract script content using regex (simple extraction for Phase 1)
12
+ const scriptMatch = html.match(/<script[^>]*>([\s\S]*?)<\/script>/i)
13
+
14
+ if (!scriptMatch || !scriptMatch[1]) {
15
+ return null
16
+ }
17
+
18
+ return {
19
+ raw: scriptMatch[1].trim()
20
+ }
21
+ }
22
+
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Template Parser
3
+ *
4
+ * Parses HTML template and extracts expressions
5
+ * Phase 1: Only extracts, does not execute
6
+ */
7
+
8
+ import { parse, parseFragment } from 'parse5'
9
+ import type { TemplateIR, TemplateNode, ElementNode, TextNode, ExpressionNode, AttributeIR, ExpressionIR, SourceLocation, LoopContext } from '../ir/types'
10
+ import { CompilerError } from '../errors/compilerError'
11
+ import { parseScript } from './parseScript'
12
+ import { detectMapExpression, extractLoopVariables, referencesLoopVariable } from './detectMapExpressions'
13
+ import { shouldAttachLoopContext, mergeLoopContext, extractLoopContextFromExpression } from './trackLoopContext'
14
+
15
+ // Generate stable IDs for expressions
16
+ let expressionIdCounter = 0
17
+ function generateExpressionId(): string {
18
+ return `expr_${expressionIdCounter++}`
19
+ }
20
+
21
+ /**
22
+ * Strip script and style blocks from HTML before parsing
23
+ */
24
+ function stripBlocks(html: string): string {
25
+ // Remove script blocks
26
+ let stripped = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
27
+ // Remove style blocks
28
+ stripped = stripped.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
29
+ return stripped
30
+ }
31
+
32
+ /**
33
+ * Normalize attribute expressions before parsing
34
+ * Replaces attr={expr} with attr="__ZEN_EXPR_base64" so parse5 can parse it
35
+ */
36
+ function normalizeAttributeExpressions(html: string): { normalized: string; expressions: Map<string, string> } {
37
+ const exprMap = new Map<string, string>()
38
+ let exprCounter = 0
39
+
40
+ // Match attributes with expression values: attr={...}
41
+ // Use a more sophisticated regex to handle nested braces and quotes
42
+ const normalized = html.replace(/(\w+)=\{([^}]+)\}/g, (match, attrName, expr) => {
43
+ const placeholder = `__ZEN_EXPR_${exprCounter++}`
44
+ exprMap.set(placeholder, expr.trim())
45
+ return `${attrName}="${placeholder}"`
46
+ })
47
+
48
+ return { normalized, expressions: exprMap }
49
+ }
50
+
51
+ /**
52
+ * Calculate source location from parse5 node
53
+ */
54
+ function getLocation(node: any, originalHtml: string): SourceLocation {
55
+ // parse5 provides sourceCodeLocation if available
56
+ if (node.sourceCodeLocation) {
57
+ return {
58
+ line: node.sourceCodeLocation.startLine || 1,
59
+ column: node.sourceCodeLocation.startCol || 1
60
+ }
61
+ }
62
+ // Fallback if location info not available
63
+ return { line: 1, column: 1 }
64
+ }
65
+
66
+ /**
67
+ * Extract expressions from text content
68
+ * Returns array of { expression, location } and the text with expressions replaced
69
+ * Phase 7: Supports loop context for expressions inside map iterations
70
+ */
71
+ function extractExpressionsFromText(
72
+ text: string,
73
+ baseLocation: SourceLocation,
74
+ expressions: ExpressionIR[],
75
+ loopContext?: LoopContext // Phase 7: Loop context from parent map expressions
76
+ ): { processedText: string; nodes: (TextNode | ExpressionNode)[] } {
77
+ const nodes: (TextNode | ExpressionNode)[] = []
78
+ let processedText = ''
79
+ let currentIndex = 0
80
+
81
+ // Match { ... } expressions (non-greedy)
82
+ const expressionRegex = /\{([^}]+)\}/g
83
+ let match
84
+
85
+ while ((match = expressionRegex.exec(text)) !== null) {
86
+ const beforeExpr = text.substring(currentIndex, match.index)
87
+ if (beforeExpr) {
88
+ nodes.push({
89
+ type: 'text',
90
+ value: beforeExpr,
91
+ location: {
92
+ line: baseLocation.line,
93
+ column: baseLocation.column + currentIndex
94
+ }
95
+ })
96
+ processedText += beforeExpr
97
+ }
98
+
99
+ // Extract expression
100
+ const exprCode = (match[1] || '').trim()
101
+ const exprId = generateExpressionId()
102
+ const exprLocation: SourceLocation = {
103
+ line: baseLocation.line,
104
+ column: baseLocation.column + match.index + 1 // +1 for opening brace
105
+ }
106
+
107
+ const exprIR: ExpressionIR = {
108
+ id: exprId,
109
+ code: exprCode,
110
+ location: exprLocation
111
+ }
112
+ expressions.push(exprIR)
113
+
114
+ // Phase 7: Detect if this is a map expression and extract loop context
115
+ const mapLoopContext = extractLoopContextFromExpression(exprIR)
116
+ const activeLoopContext = mergeLoopContext(loopContext, mapLoopContext)
117
+
118
+ // Phase 7: Attach loop context if expression references loop variables
119
+ const attachedLoopContext = shouldAttachLoopContext(exprIR, activeLoopContext)
120
+
121
+ nodes.push({
122
+ type: 'expression',
123
+ expression: exprId,
124
+ location: exprLocation,
125
+ loopContext: attachedLoopContext
126
+ })
127
+
128
+ processedText += `{${exprCode}}` // Keep in processed text for now
129
+ currentIndex = match.index + match[0].length
130
+ }
131
+
132
+ // Add remaining text
133
+ const remaining = text.substring(currentIndex)
134
+ if (remaining) {
135
+ nodes.push({
136
+ type: 'text',
137
+ value: remaining,
138
+ location: {
139
+ line: baseLocation.line,
140
+ column: baseLocation.column + currentIndex
141
+ }
142
+ })
143
+ processedText += remaining
144
+ }
145
+
146
+ // If no expressions found, return single text node
147
+ if (nodes.length === 0) {
148
+ nodes.push({
149
+ type: 'text',
150
+ value: text,
151
+ location: baseLocation
152
+ })
153
+ processedText = text
154
+ }
155
+
156
+ return { processedText, nodes }
157
+ }
158
+
159
+ /**
160
+ * Parse attribute value - may contain expressions
161
+ * Phase 7: Supports loop context for expressions inside map iterations
162
+ */
163
+ function parseAttributeValue(
164
+ value: string,
165
+ baseLocation: SourceLocation,
166
+ expressions: ExpressionIR[],
167
+ normalizedExprs: Map<string, string>,
168
+ loopContext?: LoopContext // Phase 7: Loop context from parent map expressions
169
+ ): string | ExpressionIR {
170
+ // Check if this is a normalized expression placeholder
171
+ if (value.startsWith('__ZEN_EXPR_')) {
172
+ const exprCode = normalizedExprs.get(value)
173
+ if (!exprCode) {
174
+ throw new Error(`Normalized expression placeholder not found: ${value}`)
175
+ }
176
+
177
+ const exprId = generateExpressionId()
178
+
179
+ expressions.push({
180
+ id: exprId,
181
+ code: exprCode,
182
+ location: baseLocation
183
+ })
184
+
185
+ return {
186
+ id: exprId,
187
+ code: exprCode,
188
+ location: baseLocation
189
+ }
190
+ }
191
+
192
+ // Check if attribute value is an expression { ... } (shouldn't happen after normalization)
193
+ const exprMatch = value.match(/^\{([^}]+)\}$/)
194
+ if (exprMatch && exprMatch[1]) {
195
+ const exprCode = exprMatch[1].trim()
196
+ const exprId = generateExpressionId()
197
+
198
+ expressions.push({
199
+ id: exprId,
200
+ code: exprCode,
201
+ location: baseLocation
202
+ })
203
+
204
+ return {
205
+ id: exprId,
206
+ code: exprCode,
207
+ location: baseLocation
208
+ }
209
+ }
210
+
211
+ // Regular string value
212
+ return value
213
+ }
214
+
215
+ /**
216
+ * Convert parse5 node to TemplateNode
217
+ * Phase 7: Supports loop context propagation for map expressions
218
+ */
219
+ function parseNode(
220
+ node: any,
221
+ originalHtml: string,
222
+ expressions: ExpressionIR[],
223
+ normalizedExprs: Map<string, string>,
224
+ parentLoopContext?: LoopContext // Phase 7: Loop context from parent map expressions
225
+ ): TemplateNode | null {
226
+ if (node.nodeName === '#text') {
227
+ const text = node.value || ''
228
+ const location = getLocation(node, originalHtml)
229
+
230
+ // Extract expressions from text
231
+ // Phase 7: Pass loop context to detect map expressions and attach context
232
+ const { nodes } = extractExpressionsFromText(text, location, expressions, parentLoopContext)
233
+
234
+ // If single text node with no expressions, return it
235
+ if (nodes.length === 1 && nodes[0] && nodes[0].type === 'text') {
236
+ return nodes[0]
237
+ }
238
+
239
+ // Otherwise, we need to handle multiple nodes
240
+ // For Phase 1, we'll flatten to text for now (will be handled in future phases)
241
+ // This is a limitation we accept for Phase 1
242
+ const firstNode = nodes[0]
243
+ if (firstNode) {
244
+ return firstNode
245
+ }
246
+ return {
247
+ type: 'text',
248
+ value: text,
249
+ location
250
+ }
251
+ }
252
+
253
+ if (node.nodeName === '#comment') {
254
+ // Skip comments for Phase 1
255
+ return null
256
+ }
257
+
258
+ if (node.nodeName && node.nodeName !== '#text' && node.nodeName !== '#comment') {
259
+ const location = getLocation(node, originalHtml)
260
+ const tag = node.tagName?.toLowerCase() || node.nodeName
261
+
262
+ // Parse attributes
263
+ const attributes: AttributeIR[] = []
264
+ if (node.attrs) {
265
+ for (const attr of node.attrs) {
266
+ const attrLocation = node.sourceCodeLocation?.attrs?.[attr.name]
267
+ ? {
268
+ line: node.sourceCodeLocation.attrs[attr.name].startLine || location.line,
269
+ column: node.sourceCodeLocation.attrs[attr.name].startCol || location.column
270
+ }
271
+ : location
272
+
273
+ // Handle :attr="expr" syntax (colon-prefixed reactive attributes)
274
+ let attrName = attr.name
275
+ let attrValue = attr.value || ''
276
+ let isReactive = false
277
+
278
+ if (attrName.startsWith(':')) {
279
+ // This is a reactive attribute like :class="expr"
280
+ attrName = attrName.slice(1) // Remove the colon
281
+ isReactive = true
282
+ // The value is already a string expression (not in braces)
283
+ // Treat it as an expression
284
+ const exprId = generateExpressionId()
285
+ const exprCode = attrValue.trim()
286
+
287
+ const exprIR: ExpressionIR = {
288
+ id: exprId,
289
+ code: exprCode,
290
+ location: attrLocation
291
+ }
292
+ expressions.push(exprIR)
293
+
294
+ // Phase 7: Attach loop context if expression references loop variables
295
+ const attachedLoopContext = shouldAttachLoopContext(exprIR, parentLoopContext)
296
+
297
+ attributes.push({
298
+ name: attrName, // Store without colon (e.g., "class" not ":class")
299
+ value: exprIR,
300
+ location: attrLocation,
301
+ loopContext: attachedLoopContext
302
+ })
303
+ } else {
304
+ // Regular attribute or attr={expr} syntax
305
+ const attrValueResult = parseAttributeValue(attrValue, attrLocation, expressions, normalizedExprs, parentLoopContext)
306
+
307
+ if (typeof attrValueResult === 'string') {
308
+ // Static attribute value
309
+ attributes.push({
310
+ name: attrName,
311
+ value: attrValueResult,
312
+ location: attrLocation
313
+ })
314
+ } else {
315
+ // Expression attribute value
316
+ const exprIR = attrValueResult
317
+
318
+ // Phase 7: Attach loop context if expression references loop variables
319
+ const attachedLoopContext = shouldAttachLoopContext(exprIR, parentLoopContext)
320
+
321
+ attributes.push({
322
+ name: attrName,
323
+ value: exprIR,
324
+ location: attrLocation,
325
+ loopContext: attachedLoopContext
326
+ })
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ // Parse children
333
+ const children: TemplateNode[] = []
334
+ if (node.childNodes) {
335
+ for (const child of node.childNodes) {
336
+ if (child.nodeName === '#text') {
337
+ // Handle text nodes that may contain expressions
338
+ const text = child.value || ''
339
+ const location = getLocation(child, originalHtml)
340
+ const { nodes: textNodes } = extractExpressionsFromText(text, location, expressions, parentLoopContext)
341
+
342
+ // Add all nodes from text (can be multiple: text + expression + text)
343
+ for (const textNode of textNodes) {
344
+ children.push(textNode)
345
+ }
346
+ } else {
347
+ const childNode = parseNode(child, originalHtml, expressions, normalizedExprs, parentLoopContext)
348
+ if (childNode) {
349
+ children.push(childNode)
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ // Phase 7: Check if any child expression is a map expression and extract its loop context
356
+ // This allows nested loops to work correctly
357
+ let elementLoopContext = parentLoopContext
358
+
359
+ // Check children for map expressions (they create new loop contexts)
360
+ for (const child of children) {
361
+ if (child.type === 'expression' && child.loopContext) {
362
+ // If we find a map expression child, merge its context
363
+ elementLoopContext = mergeLoopContext(elementLoopContext, child.loopContext)
364
+ }
365
+ }
366
+
367
+ return {
368
+ type: 'element',
369
+ tag,
370
+ attributes,
371
+ children,
372
+ location,
373
+ loopContext: elementLoopContext // Phase 7: Inherited loop context for child processing
374
+ }
375
+ }
376
+
377
+ return null
378
+ }
379
+
380
+ /**
381
+ * Parse template from HTML string
382
+ */
383
+ export function parseTemplate(html: string, filePath: string): TemplateIR {
384
+ // Strip script and style blocks
385
+ let templateHtml = stripBlocks(html)
386
+
387
+ // Normalize attribute expressions so parse5 can parse them
388
+ const { normalized, expressions: normalizedExprs } = normalizeAttributeExpressions(templateHtml)
389
+ templateHtml = normalized
390
+
391
+ try {
392
+ // Parse HTML using parseFragment (handles fragments without html/body wrapper)
393
+ const fragment = parseFragment(templateHtml, {
394
+ sourceCodeLocationInfo: true
395
+ })
396
+
397
+ const expressions: ExpressionIR[] = []
398
+ const nodes: TemplateNode[] = []
399
+
400
+ // Parse fragment children
401
+ // Phase 7: Start with no loop context (top-level expressions)
402
+ if (fragment.childNodes) {
403
+ for (const node of fragment.childNodes) {
404
+ const parsed = parseNode(node, templateHtml, expressions, normalizedExprs, undefined)
405
+ if (parsed) {
406
+ nodes.push(parsed)
407
+ }
408
+ }
409
+ }
410
+
411
+ return {
412
+ raw: templateHtml,
413
+ nodes,
414
+ expressions
415
+ }
416
+ } catch (error: any) {
417
+ throw new CompilerError(
418
+ `Template parsing failed: ${error.message}`,
419
+ filePath,
420
+ 1,
421
+ 1
422
+ )
423
+ }
424
+ }
425
+
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Zenith File Parser
3
+ *
4
+ * Main entry point for parsing .zen files
5
+ * Phase 1: Parse & Extract only
6
+ */
7
+
8
+ import { readFileSync } from 'fs'
9
+ import type { ZenIR, StyleIR } from '../ir/types'
10
+ import { parseTemplate } from './parseTemplate'
11
+ import { parseScript } from './parseScript'
12
+ import { CompilerError } from '../errors/compilerError'
13
+
14
+ /**
15
+ * Extract style blocks from HTML
16
+ */
17
+ function parseStyles(html: string): StyleIR[] {
18
+ const styles: StyleIR[] = []
19
+ const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi
20
+ let match
21
+
22
+ while ((match = styleRegex.exec(html)) !== null) {
23
+ if (match[1]) {
24
+ styles.push({
25
+ raw: match[1].trim()
26
+ })
27
+ }
28
+ }
29
+
30
+ return styles
31
+ }
32
+
33
+ /**
34
+ * Parse a .zen file into IR
35
+ */
36
+ export function parseZenFile(filePath: string): ZenIR {
37
+ let source: string
38
+
39
+ try {
40
+ source = readFileSync(filePath, 'utf-8')
41
+ } catch (error: any) {
42
+ throw new CompilerError(
43
+ `Failed to read file: ${error.message}`,
44
+ filePath,
45
+ 1,
46
+ 1
47
+ )
48
+ }
49
+
50
+ // Parse template
51
+ const template = parseTemplate(source, filePath)
52
+
53
+ // Parse script
54
+ const script = parseScript(source)
55
+
56
+ // Parse styles
57
+ const styles = parseStyles(source)
58
+
59
+ return {
60
+ filePath,
61
+ template,
62
+ script,
63
+ styles
64
+ }
65
+ }
66
+
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Loop Context Tracking
3
+ *
4
+ * Phase 7: Utilities for tracking and propagating loop context through the parse tree
5
+ */
6
+
7
+ import type { LoopContext, ExpressionIR } from '../ir/types'
8
+ import { detectMapExpression, referencesLoopVariable } from './detectMapExpressions'
9
+
10
+ /**
11
+ * Check if an expression should have loop context attached
12
+ * Returns the loop context if the expression references loop variables
13
+ */
14
+ export function shouldAttachLoopContext(
15
+ expr: ExpressionIR,
16
+ parentLoopContext?: LoopContext
17
+ ): LoopContext | undefined {
18
+ if (!parentLoopContext) {
19
+ return undefined
20
+ }
21
+
22
+ // Check if this expression references any loop variables
23
+ if (referencesLoopVariable(expr.code, parentLoopContext.variables)) {
24
+ return parentLoopContext
25
+ }
26
+
27
+ return undefined
28
+ }
29
+
30
+ /**
31
+ * Merge loop contexts for nested loops
32
+ * Inner loops inherit outer loop variables
33
+ */
34
+ export function mergeLoopContext(
35
+ outer?: LoopContext,
36
+ inner?: LoopContext
37
+ ): LoopContext | undefined {
38
+ if (!inner) {
39
+ return outer
40
+ }
41
+
42
+ if (!outer) {
43
+ return inner
44
+ }
45
+
46
+ // Merge variables: outer variables come first, then inner
47
+ // This allows expressions to reference both outer and inner loop variables
48
+ return {
49
+ variables: [...outer.variables, ...inner.variables],
50
+ mapSource: inner.mapSource || outer.mapSource
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Detect if an expression is a map expression and extract its loop context
56
+ */
57
+ export function extractLoopContextFromExpression(expr: ExpressionIR): LoopContext | undefined {
58
+ const mapInfo = detectMapExpression(expr)
59
+
60
+ if (!mapInfo.isMap) {
61
+ return undefined
62
+ }
63
+
64
+ // extractLoopVariables expects a MapExpressionInfo, not a string
65
+ const variables: string[] = []
66
+ if (mapInfo.itemVariable) {
67
+ variables.push(mapInfo.itemVariable)
68
+ }
69
+ if (mapInfo.indexVariable) {
70
+ variables.push(mapInfo.indexVariable)
71
+ }
72
+
73
+ if (variables.length === 0) {
74
+ return undefined
75
+ }
76
+
77
+ return {
78
+ variables,
79
+ mapSource: mapInfo.arraySource
80
+ }
81
+ }
82
+