@zenithbuild/core 1.2.2 → 1.2.4

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 (83) hide show
  1. package/README.md +20 -19
  2. package/cli/commands/add.ts +2 -2
  3. package/cli/commands/build.ts +2 -3
  4. package/cli/commands/dev.ts +94 -74
  5. package/cli/commands/index.ts +1 -1
  6. package/cli/commands/preview.ts +1 -1
  7. package/cli/commands/remove.ts +2 -2
  8. package/cli/index.ts +1 -1
  9. package/cli/main.ts +1 -1
  10. package/cli/utils/logger.ts +1 -1
  11. package/cli/utils/plugin-manager.ts +1 -1
  12. package/cli/utils/project.ts +4 -4
  13. package/core/components/ErrorPage.zen +218 -0
  14. package/core/components/index.ts +15 -0
  15. package/core/config.ts +1 -0
  16. package/core/index.ts +29 -0
  17. package/dist/compiler-native-frej59m4.node +0 -0
  18. package/dist/core/compiler-native-frej59m4.node +0 -0
  19. package/dist/core/index.js +6293 -0
  20. package/dist/runtime/lifecycle/index.js +1 -0
  21. package/dist/runtime/reactivity/index.js +1 -0
  22. package/dist/zen-build.js +1 -20118
  23. package/dist/zen-dev.js +1 -20118
  24. package/dist/zen-preview.js +1 -20118
  25. package/dist/zenith.js +1 -20118
  26. package/package.json +11 -20
  27. package/compiler/README.md +0 -380
  28. package/compiler/build-analyzer.ts +0 -122
  29. package/compiler/css/index.ts +0 -317
  30. package/compiler/discovery/componentDiscovery.ts +0 -242
  31. package/compiler/discovery/layouts.ts +0 -70
  32. package/compiler/errors/compilerError.ts +0 -56
  33. package/compiler/finalize/finalizeOutput.ts +0 -192
  34. package/compiler/finalize/generateFinalBundle.ts +0 -82
  35. package/compiler/index.ts +0 -83
  36. package/compiler/ir/types.ts +0 -174
  37. package/compiler/output/types.ts +0 -48
  38. package/compiler/parse/detectMapExpressions.ts +0 -102
  39. package/compiler/parse/importTypes.ts +0 -78
  40. package/compiler/parse/parseImports.ts +0 -309
  41. package/compiler/parse/parseScript.ts +0 -46
  42. package/compiler/parse/parseTemplate.ts +0 -628
  43. package/compiler/parse/parseZenFile.ts +0 -66
  44. package/compiler/parse/scriptAnalysis.ts +0 -91
  45. package/compiler/parse/trackLoopContext.ts +0 -82
  46. package/compiler/runtime/dataExposure.ts +0 -332
  47. package/compiler/runtime/generateDOM.ts +0 -255
  48. package/compiler/runtime/generateHydrationBundle.ts +0 -407
  49. package/compiler/runtime/hydration.ts +0 -309
  50. package/compiler/runtime/navigation.ts +0 -432
  51. package/compiler/runtime/thinRuntime.ts +0 -160
  52. package/compiler/runtime/transformIR.ts +0 -406
  53. package/compiler/runtime/wrapExpression.ts +0 -114
  54. package/compiler/runtime/wrapExpressionWithLoop.ts +0 -97
  55. package/compiler/spa-build.ts +0 -917
  56. package/compiler/ssg-build.ts +0 -486
  57. package/compiler/test/component-stacking.test.ts +0 -365
  58. package/compiler/test/map-lowering.test.ts +0 -130
  59. package/compiler/test/validate-test.ts +0 -104
  60. package/compiler/transform/classifyExpression.ts +0 -444
  61. package/compiler/transform/componentResolver.ts +0 -350
  62. package/compiler/transform/componentScriptTransformer.ts +0 -303
  63. package/compiler/transform/expressionTransformer.ts +0 -385
  64. package/compiler/transform/fragmentLowering.ts +0 -819
  65. package/compiler/transform/generateBindings.ts +0 -68
  66. package/compiler/transform/generateHTML.ts +0 -28
  67. package/compiler/transform/layoutProcessor.ts +0 -132
  68. package/compiler/transform/slotResolver.ts +0 -292
  69. package/compiler/transform/transformNode.ts +0 -314
  70. package/compiler/transform/transformTemplate.ts +0 -38
  71. package/compiler/validate/invariants.ts +0 -292
  72. package/compiler/validate/validateExpressions.ts +0 -168
  73. package/core/config/index.ts +0 -18
  74. package/core/config/loader.ts +0 -69
  75. package/core/config/types.ts +0 -119
  76. package/core/plugins/bridge.ts +0 -193
  77. package/core/plugins/index.ts +0 -7
  78. package/core/plugins/registry.ts +0 -126
  79. package/dist/cli.js +0 -11675
  80. package/runtime/build.ts +0 -17
  81. package/runtime/bundle-generator.ts +0 -1266
  82. package/runtime/client-runtime.ts +0 -891
  83. package/runtime/serve.ts +0 -93
@@ -1,628 +0,0 @@
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, InvariantError } from '../errors/compilerError'
11
- import { parseScript } from './parseScript'
12
- import { detectMapExpression, extractLoopVariables, referencesLoopVariable } from './detectMapExpressions'
13
- import { shouldAttachLoopContext, mergeLoopContext, extractLoopContextFromExpression } from './trackLoopContext'
14
- import { INVARIANT } from '../validate/invariants'
15
- import { lowerFragments } from '../transform/fragmentLowering'
16
-
17
- // Generate stable IDs for expressions
18
- let expressionIdCounter = 0
19
- function generateExpressionId(): string {
20
- return `expr_${expressionIdCounter++}`
21
- }
22
-
23
- /**
24
- * Strip script and style blocks from HTML before parsing
25
- * Preserves external script tags (<script src="...">) but removes inline scripts
26
- */
27
- function stripBlocks(html: string): string {
28
- // Remove only inline script blocks (those WITHOUT src attribute), preserve external scripts
29
- let stripped = html.replace(/<script([^>]*)>([\s\S]*?)<\/script>/gi, (match, attrs, content) => {
30
- // Keep script tags with src attribute (external scripts)
31
- if (attrs.includes('src=')) {
32
- return match;
33
- }
34
- // Remove inline scripts (those without src)
35
- return '';
36
- })
37
- // Remove style blocks
38
- stripped = stripped.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
39
- return stripped
40
- }
41
-
42
- /**
43
- * Find the end of a balanced brace expression, handling strings and template literals
44
- * Returns the index after the closing brace, or -1 if unbalanced
45
- */
46
- function findBalancedBraceEnd(html: string, startIndex: number): number {
47
- let braceCount = 1
48
- let i = startIndex + 1
49
- let inString = false
50
- let stringChar = ''
51
- let inTemplate = false
52
-
53
- while (i < html.length && braceCount > 0) {
54
- const char = html[i]
55
- const prevChar = i > 0 ? html[i - 1] : ''
56
-
57
- // Handle escape sequences
58
- if (prevChar === '\\') {
59
- i++
60
- continue
61
- }
62
-
63
- // Handle string literals (not inside template)
64
- if (!inString && !inTemplate && (char === '"' || char === "'")) {
65
- inString = true
66
- stringChar = char
67
- i++
68
- continue
69
- }
70
-
71
- if (inString && char === stringChar) {
72
- inString = false
73
- stringChar = ''
74
- i++
75
- continue
76
- }
77
-
78
- // Handle template literals
79
- if (!inString && !inTemplate && char === '`') {
80
- inTemplate = true
81
- i++
82
- continue
83
- }
84
-
85
- if (inTemplate && char === '`') {
86
- inTemplate = false
87
- i++
88
- continue
89
- }
90
-
91
- // Handle ${} inside template literals - need to track nested braces
92
- if (inTemplate && char === '$' && html[i + 1] === '{') {
93
- // Skip the ${ and count as opening brace
94
- i += 2
95
- let templateBraceCount = 1
96
- while (i < html.length && templateBraceCount > 0) {
97
- if (html[i] === '{') templateBraceCount++
98
- else if (html[i] === '}') templateBraceCount--
99
- i++
100
- }
101
- continue
102
- }
103
-
104
- // Count braces only when not in strings or templates
105
- if (!inString && !inTemplate) {
106
- if (char === '{') braceCount++
107
- else if (char === '}') braceCount--
108
- }
109
-
110
- i++
111
- }
112
-
113
- return braceCount === 0 ? i : -1
114
- }
115
-
116
- /**
117
- * Normalize expressions before parsing
118
- * Replaces both attr={expr} and {textExpr} with placeholders so parse5 can parse the HTML correctly
119
- * without being confused by tags or braces inside expressions.
120
- *
121
- * Uses balanced brace parsing to correctly handle:
122
- * - String literals with braces inside
123
- * - Template literals with ${} interpolations
124
- * - Arrow functions with object returns
125
- * - Multi-line JSX expressions
126
- */
127
- function normalizeAllExpressions(html: string): { normalized: string; expressions: Map<string, string> } {
128
- const exprMap = new Map<string, string>()
129
- let exprCounter = 0
130
- let result = ''
131
- let lastPos = 0
132
-
133
- for (let i = 0; i < html.length; i++) {
134
- // Look for { and check if it's an expression
135
- // We handle both text expressions and attribute expressions: attr={...}
136
- if (html[i] === '{') {
137
- const j = findBalancedBraceEnd(html, i)
138
-
139
- if (j !== -1 && j > i + 1) {
140
- const expr = html.substring(i + 1, j - 1).trim()
141
-
142
- // Skip empty expressions
143
- if (expr.length === 0) {
144
- i++
145
- continue
146
- }
147
-
148
- const placeholder = `__ZEN_EXPR_${exprCounter++}`
149
- exprMap.set(placeholder, expr)
150
-
151
- result += html.substring(lastPos, i)
152
- result += placeholder
153
- lastPos = j
154
- i = j - 1
155
- }
156
- }
157
- }
158
- result += html.substring(lastPos)
159
-
160
- return { normalized: result, expressions: exprMap }
161
- }
162
-
163
-
164
- /**
165
- * Calculate source location from parse5 node
166
- */
167
- function getLocation(node: any, originalHtml: string): SourceLocation {
168
- // parse5 provides sourceCodeLocation if available
169
- if (node.sourceCodeLocation) {
170
- return {
171
- line: node.sourceCodeLocation.startLine || 1,
172
- column: node.sourceCodeLocation.startCol || 1
173
- }
174
- }
175
- // Fallback if location info not available
176
- return { line: 1, column: 1 }
177
- }
178
-
179
- /**
180
- * Extract expressions from text content
181
- * Returns array of { expression, location } and the text with expressions replaced
182
- * Phase 7: Supports loop context for expressions inside map iterations
183
- */
184
- function extractExpressionsFromText(
185
- text: string,
186
- baseLocation: SourceLocation,
187
- expressions: ExpressionIR[],
188
- normalizedExprs: Map<string, string>,
189
- loopContext?: LoopContext
190
- ): { processedText: string; nodes: (TextNode | ExpressionNode)[] } {
191
- const nodes: (TextNode | ExpressionNode)[] = []
192
- let processedText = ''
193
- let currentIndex = 0
194
-
195
- // Match __ZEN_EXPR_N placeholders
196
- const expressionRegex = /__ZEN_EXPR_\d+/g
197
- let match
198
-
199
- while ((match = expressionRegex.exec(text)) !== null) {
200
- const beforeExpr = text.substring(currentIndex, match.index)
201
- if (beforeExpr) {
202
- nodes.push({
203
- type: 'text',
204
- value: beforeExpr,
205
- location: {
206
- line: baseLocation.line,
207
- column: baseLocation.column + currentIndex
208
- }
209
- })
210
- processedText += beforeExpr
211
- }
212
-
213
- // Resolve placeholder to original expression code
214
- const placeholder = match[0]
215
- const exprCode = (normalizedExprs.get(placeholder) || '').trim()
216
- const exprId = generateExpressionId()
217
- const exprLocation: SourceLocation = {
218
- line: baseLocation.line,
219
- column: baseLocation.column + match.index
220
- }
221
-
222
- const exprIR: ExpressionIR = {
223
- id: exprId,
224
- code: exprCode,
225
- location: exprLocation
226
- }
227
- expressions.push(exprIR)
228
-
229
- // Phase 7: Loop context detection and attachment
230
- const mapLoopContext = extractLoopContextFromExpression(exprIR)
231
- const activeLoopContext = mergeLoopContext(loopContext, mapLoopContext)
232
- const attachedLoopContext = shouldAttachLoopContext(exprIR, activeLoopContext)
233
-
234
- nodes.push({
235
- type: 'expression',
236
- expression: exprId,
237
- location: exprLocation,
238
- loopContext: attachedLoopContext
239
- })
240
-
241
- processedText += `{${exprCode}}`
242
- currentIndex = match.index + match[0].length
243
- }
244
-
245
- // Add remaining text
246
- const remaining = text.substring(currentIndex)
247
- if (remaining) {
248
- nodes.push({
249
- type: 'text',
250
- value: remaining,
251
- location: {
252
- line: baseLocation.line,
253
- column: baseLocation.column + currentIndex
254
- }
255
- })
256
- processedText += remaining
257
- }
258
-
259
- // If no expressions found, return single text node
260
- if (nodes.length === 0) {
261
- nodes.push({
262
- type: 'text',
263
- value: text,
264
- location: baseLocation
265
- })
266
- processedText = text
267
- }
268
-
269
- return { processedText, nodes }
270
- }
271
-
272
- /**
273
- * Parse attribute value - may contain expressions
274
- * Phase 7: Supports loop context for expressions inside map iterations
275
- */
276
- function parseAttributeValue(
277
- value: string,
278
- baseLocation: SourceLocation,
279
- expressions: ExpressionIR[],
280
- normalizedExprs: Map<string, string>,
281
- loopContext?: LoopContext // Phase 7: Loop context from parent map expressions
282
- ): string | ExpressionIR {
283
- // Check if this is a normalized expression placeholder
284
- if (value.startsWith('__ZEN_EXPR_')) {
285
- const exprCode = normalizedExprs.get(value)
286
- if (!exprCode) {
287
- throw new Error(`Normalized expression placeholder not found: ${value}`)
288
- }
289
-
290
- const exprId = generateExpressionId()
291
-
292
- expressions.push({
293
- id: exprId,
294
- code: exprCode,
295
- location: baseLocation
296
- })
297
-
298
- return {
299
- id: exprId,
300
- code: exprCode,
301
- location: baseLocation
302
- }
303
- }
304
-
305
- // Check if attribute value is an expression { ... } (shouldn't happen after normalization)
306
- const exprMatch = value.match(/^\{([^}]+)\}$/)
307
- if (exprMatch && exprMatch[1]) {
308
- const exprCode = exprMatch[1].trim()
309
- const exprId = generateExpressionId()
310
-
311
- expressions.push({
312
- id: exprId,
313
- code: exprCode,
314
- location: baseLocation
315
- })
316
-
317
- return {
318
- id: exprId,
319
- code: exprCode,
320
- location: baseLocation
321
- }
322
- }
323
-
324
- // Regular string value
325
- return value
326
- }
327
-
328
- /**
329
- * Convert parse5 node to TemplateNode
330
- * Phase 7: Supports loop context propagation for map expressions
331
- */
332
- function parseNode(
333
- node: any,
334
- originalHtml: string,
335
- expressions: ExpressionIR[],
336
- normalizedExprs: Map<string, string>,
337
- parentLoopContext?: LoopContext // Phase 7: Loop context from parent map expressions
338
- ): TemplateNode | null {
339
- if (node.nodeName === '#text') {
340
- const text = node.value || ''
341
- const location = getLocation(node, originalHtml)
342
-
343
- // Extract expressions from text
344
- // Phase 7: Pass loop context to detect map expressions and attach context
345
- const { nodes } = extractExpressionsFromText(node.value, location, expressions, normalizedExprs, parentLoopContext)
346
-
347
- // If single text node with no expressions, return it
348
- if (nodes.length === 1 && nodes[0] && nodes[0].type === 'text') {
349
- return nodes[0]
350
- }
351
-
352
- // Otherwise, we need to handle multiple nodes
353
- // For Phase 1, we'll flatten to text for now (will be handled in future phases)
354
- // This is a limitation we accept for Phase 1
355
- const firstNode = nodes[0]
356
- if (firstNode) {
357
- return firstNode
358
- }
359
- return {
360
- type: 'text',
361
- value: text,
362
- location
363
- }
364
- }
365
-
366
- if (node.nodeName === '#comment') {
367
- // Skip comments for Phase 1
368
- return null
369
- }
370
-
371
- if (node.nodeName && node.nodeName !== '#text' && node.nodeName !== '#comment') {
372
- const location = getLocation(node, originalHtml)
373
- const tag = node.tagName?.toLowerCase() || node.nodeName
374
-
375
- // Extract original tag name from source HTML to preserve casing (parse5 lowercases everything)
376
- let originalTag = node.tagName || node.nodeName
377
- if (node.sourceCodeLocation && node.sourceCodeLocation.startOffset !== undefined) {
378
- const startOffset = node.sourceCodeLocation.startOffset
379
- // Find the tag name in original HTML (after '<')
380
- const tagMatch = originalHtml.slice(startOffset).match(/^<([a-zA-Z][a-zA-Z0-9._-]*)/)
381
- if (tagMatch && tagMatch[1]) {
382
- originalTag = tagMatch[1]
383
- }
384
- }
385
-
386
- // INV005: <template> tags are forbidden — use compound components instead
387
- if (tag === 'template') {
388
- throw new InvariantError(
389
- INVARIANT.TEMPLATE_TAG,
390
- `<template> tags are forbidden in Zenith. Use compound components (e.g., Card.Header) for named slots.`,
391
- 'Named slots use compound component pattern (Card.Header), not <template> tags.',
392
- 'unknown', // filePath passed to parseTemplate
393
- location.line,
394
- location.column
395
- )
396
- }
397
-
398
- // Parse attributes
399
- const attributes: AttributeIR[] = []
400
- if (node.attrs) {
401
- for (const attr of node.attrs) {
402
- const attrLocation = node.sourceCodeLocation?.attrs?.[attr.name]
403
- ? {
404
- line: node.sourceCodeLocation.attrs[attr.name].startLine || location.line,
405
- column: node.sourceCodeLocation.attrs[attr.name].startCol || location.column
406
- }
407
- : location
408
-
409
- // INV006: slot="" attributes are forbidden — use compound components instead
410
- if (attr.name === 'slot') {
411
- throw new InvariantError(
412
- INVARIANT.SLOT_ATTRIBUTE,
413
- `slot="${attr.value || ''}" attribute is forbidden. Use compound components (e.g., Card.Header) for named slots.`,
414
- 'Named slots use compound component pattern (Card.Header), not slot="" attributes.',
415
- 'unknown',
416
- attrLocation.line,
417
- attrLocation.column
418
- )
419
- }
420
-
421
- // Handle :attr="expr" syntax (colon-prefixed reactive attributes)
422
- let attrName = attr.name
423
- let attrValue = attr.value || ''
424
- let isReactive = false
425
-
426
- if (attrName.startsWith(':')) {
427
- // This is a reactive attribute like :class="expr"
428
- attrName = attrName.slice(1) // Remove the colon
429
- isReactive = true
430
- // The value is already a string expression (not in braces)
431
- // Treat it as an expression
432
- const exprId = generateExpressionId()
433
- const exprCode = attrValue.trim()
434
-
435
- const exprIR: ExpressionIR = {
436
- id: exprId,
437
- code: exprCode,
438
- location: attrLocation
439
- }
440
- expressions.push(exprIR)
441
-
442
- // Phase 7: Attach loop context if expression references loop variables
443
- const attachedLoopContext = shouldAttachLoopContext(exprIR, parentLoopContext)
444
-
445
- attributes.push({
446
- name: attrName, // Store without colon (e.g., "class" not ":class")
447
- value: exprIR,
448
- location: attrLocation,
449
- loopContext: attachedLoopContext
450
- })
451
- } else {
452
- // Regular attribute or attr={expr} syntax
453
- const attrValueResult = parseAttributeValue(attrValue, attrLocation, expressions, normalizedExprs, parentLoopContext)
454
-
455
- // Transform event attributes: onclick -> data-zen-click, onchange -> data-zen-change, etc.
456
- let finalAttrName = attrName
457
- if (attrName.startsWith('on') && attrName.length > 2) {
458
- const eventType = attrName.slice(2) // Remove "on" prefix
459
- finalAttrName = `data-zen-${eventType}`
460
- }
461
-
462
- if (typeof attrValueResult === 'string') {
463
- // Static attribute value
464
- attributes.push({
465
- name: finalAttrName,
466
- value: attrValueResult,
467
- location: attrLocation
468
- })
469
- } else {
470
- // Expression attribute value
471
- const exprIR = attrValueResult
472
-
473
- // Phase 7: Attach loop context if expression references loop variables
474
- const attachedLoopContext = shouldAttachLoopContext(exprIR, parentLoopContext)
475
-
476
- attributes.push({
477
- name: finalAttrName,
478
- value: exprIR,
479
- location: attrLocation,
480
- loopContext: attachedLoopContext
481
- })
482
- }
483
- }
484
- }
485
- }
486
-
487
- // Parse children
488
- const children: TemplateNode[] = []
489
- if (node.childNodes) {
490
- for (const child of node.childNodes) {
491
- if (child.nodeName === '#text') {
492
- // Handle text nodes that may contain expressions
493
- const text = child.value || ''
494
- const location = getLocation(child, originalHtml)
495
- const { nodes: textNodes } = extractExpressionsFromText(text, location, expressions, normalizedExprs, parentLoopContext)
496
-
497
- // Add all nodes from text (can be multiple: text + expression + text)
498
- for (const textNode of textNodes) {
499
- children.push(textNode)
500
- }
501
- } else {
502
- const childNode = parseNode(child, originalHtml, expressions, normalizedExprs, parentLoopContext)
503
- if (childNode) {
504
- children.push(childNode)
505
- }
506
- }
507
- }
508
- }
509
-
510
- // Phase 7: Check if any child expression is a map expression and extract its loop context
511
- // This allows nested loops to work correctly
512
- let elementLoopContext = parentLoopContext
513
-
514
- // Check children for map expressions (they create new loop contexts)
515
- for (const child of children) {
516
- if (child.type === 'expression' && child.loopContext) {
517
- // If we find a map expression child, merge its context
518
- elementLoopContext = mergeLoopContext(elementLoopContext, child.loopContext)
519
- }
520
- }
521
-
522
- // Check if this is a custom component (starts with uppercase)
523
- const isComponent = originalTag.length > 0 && originalTag[0] === originalTag[0].toUpperCase()
524
-
525
- if (isComponent) {
526
- // This is a component node
527
- return {
528
- type: 'component',
529
- name: originalTag,
530
- attributes,
531
- children,
532
- location,
533
- loopContext: elementLoopContext
534
- }
535
- } else {
536
- // This is a regular HTML element
537
- return {
538
- type: 'element',
539
- tag,
540
- attributes,
541
- children,
542
- location,
543
- loopContext: elementLoopContext
544
- }
545
- }
546
- }
547
-
548
- return null
549
- }
550
-
551
- /**
552
- * Convert self-closing component tags to properly closed tags
553
- *
554
- * HTML5/parse5 treats `<ComponentName />` as an opening tag (the `/` is ignored),
555
- * which causes following siblings to be incorrectly nested as children.
556
- *
557
- * This function converts `<ComponentName />` to `<ComponentName></ComponentName>`
558
- * for tags that start with uppercase (Zenith components).
559
- *
560
- * Example:
561
- * Input: `<Header /><Hero /><Footer />`
562
- * Output: `<Header></Header><Hero></Hero><Footer></Footer>`
563
- */
564
- function convertSelfClosingComponents(html: string): string {
565
- // Match self-closing tags that start with uppercase (component tags)
566
- // Pattern: <ComponentName ... />
567
- // Captures: ComponentName and any attributes
568
- const selfClosingPattern = /<([A-Z][a-zA-Z0-9._-]*)([^>]*?)\/>/g
569
-
570
- return html.replace(selfClosingPattern, (match, tagName, attributes) => {
571
- // Convert to properly closed tag
572
- return `<${tagName}${attributes}></${tagName}>`
573
- })
574
- }
575
-
576
- /**
577
- * Parse template from HTML string
578
- */
579
- export function parseTemplate(html: string, filePath: string): TemplateIR {
580
- // Strip script and style blocks
581
- let templateHtml = stripBlocks(html)
582
-
583
- // Convert self-closing component tags to properly closed tags
584
- // This fixes the component stacking bug where siblings become nested children
585
- templateHtml = convertSelfClosingComponents(templateHtml)
586
-
587
- // Normalize all expressions so parse5 can parse them safely
588
- const { normalized, expressions: normalizedExprs } = normalizeAllExpressions(templateHtml)
589
- templateHtml = normalized
590
-
591
- try {
592
- // Parse HTML using parseFragment
593
- const fragment = parseFragment(templateHtml, {
594
- sourceCodeLocationInfo: true
595
- })
596
-
597
- const expressions: ExpressionIR[] = []
598
- const nodes: TemplateNode[] = []
599
-
600
- // Parse fragment children
601
- if (fragment.childNodes) {
602
- for (const node of fragment.childNodes) {
603
- const parsed = parseNode(node, templateHtml, expressions, normalizedExprs, undefined)
604
- if (parsed) {
605
- nodes.push(parsed)
606
- }
607
- }
608
- }
609
-
610
- // Phase 8: Lower JSX expressions to structural fragments
611
- // This transforms expressions like {cond ? <A /> : <B />} into ConditionalFragmentNode
612
- const loweredNodes = lowerFragments(nodes, filePath, expressions)
613
-
614
- return {
615
- raw: templateHtml,
616
- nodes: loweredNodes,
617
- expressions
618
- }
619
- } catch (error: any) {
620
- throw new CompilerError(
621
- `Template parsing failed: ${error.message}`,
622
- filePath,
623
- 1,
624
- 1
625
- )
626
- }
627
- }
628
-
@@ -1,66 +0,0 @@
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
-