@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,819 +0,0 @@
1
- /**
2
- * Fragment Lowering
3
- *
4
- * Transforms JSX-returning expressions into structural fragment nodes.
5
- *
6
- * This phase runs AFTER parsing, BEFORE component resolution.
7
- * Transforms ExpressionNode → ConditionalFragmentNode | OptionalFragmentNode | LoopFragmentNode
8
- *
9
- * IMPORTANT: JSX in Zenith is compile-time sugar only.
10
- * The compiler enumerates all possible DOM shapes and lowers them at compile time.
11
- * Runtime never creates DOM — it only toggles visibility and binds values.
12
- */
13
-
14
- import type {
15
- TemplateNode,
16
- ExpressionNode,
17
- ConditionalFragmentNode,
18
- OptionalFragmentNode,
19
- LoopFragmentNode,
20
- LoopContext,
21
- AttributeIR,
22
- SourceLocation,
23
- ExpressionIR
24
- } from '../ir/types'
25
- import { classifyExpression, requiresStructuralLowering } from './classifyExpression'
26
- import { InvariantError } from '../errors/compilerError'
27
- import { INVARIANT } from '../validate/invariants'
28
-
29
- /**
30
- * Lower JSX-returning expressions into structural fragments
31
- *
32
- * Walks the node tree and transforms ExpressionNode instances
33
- * that return JSX into the appropriate fragment node types.
34
- *
35
- * @param nodes - Template nodes to process
36
- * @param filePath - Source file path for error reporting
37
- * @param expressions - Expression registry (mutated to add new expressions)
38
- * @returns Lowered nodes with fragment bindings
39
- */
40
- export function lowerFragments(
41
- nodes: TemplateNode[],
42
- filePath: string,
43
- expressions: ExpressionIR[]
44
- ): TemplateNode[] {
45
- const lowered = nodes.map(node => lowerNode(node, filePath, expressions))
46
-
47
- // INV-EXPR-REG-001 Enforcement
48
- lowered.forEach(node => assertNoRawExpressions(node, filePath))
49
-
50
- return lowered
51
- }
52
-
53
- /**
54
- * Lower a single node
55
- */
56
- function lowerNode(
57
- node: TemplateNode,
58
- filePath: string,
59
- expressions: ExpressionIR[]
60
- ): TemplateNode {
61
- switch (node.type) {
62
- case 'expression':
63
- return lowerExpressionNode(node, filePath, expressions)
64
-
65
- case 'element': {
66
- // Check if this is an <html-content> element directive
67
- if (node.tag === 'html-content') {
68
- return lowerHtmlContentElement(node, filePath, expressions)
69
- }
70
-
71
- return {
72
- ...node,
73
- children: lowerFragments(node.children, filePath, expressions)
74
- }
75
- }
76
-
77
- case 'component':
78
- return {
79
- ...node,
80
- children: lowerFragments(node.children, filePath, expressions)
81
- }
82
-
83
- case 'conditional-fragment':
84
- return {
85
- ...node,
86
- consequent: lowerFragments(node.consequent, filePath, expressions),
87
- alternate: lowerFragments(node.alternate, filePath, expressions)
88
- }
89
-
90
- case 'optional-fragment':
91
- return {
92
- ...node,
93
- fragment: lowerFragments(node.fragment, filePath, expressions)
94
- }
95
-
96
- case 'loop-fragment':
97
- return {
98
- ...node,
99
- body: lowerFragments(node.body, filePath, expressions)
100
- }
101
-
102
- case 'text':
103
- default:
104
- return node
105
- }
106
- }
107
-
108
- /**
109
- * Lower an expression node to a fragment if it returns JSX
110
- */
111
- function lowerExpressionNode(
112
- node: ExpressionNode,
113
- filePath: string,
114
- expressions: ExpressionIR[]
115
- ): TemplateNode {
116
- // Resolve the expression ID to its original code for classification
117
- const exprIR = expressions.find(e => e.id === node.expression)
118
- if (!exprIR) {
119
- // If not found in registry, it might already be an ID or an error
120
- // But for classification we need the code.
121
- return node
122
- }
123
-
124
- const classification = classifyExpression(exprIR.code)
125
-
126
- // Primitive expressions pass through unchanged
127
- if (classification.type === 'primitive') {
128
- return node
129
- }
130
-
131
- // Unknown expressions with JSX are compile errors
132
- if (classification.type === 'unknown') {
133
- throw new InvariantError(
134
- INVARIANT.NON_ENUMERABLE_JSX,
135
- `JSX expression output cannot be statically determined: ${node.expression.slice(0, 50)}...`,
136
- 'JSX expressions must have statically enumerable output. The compiler must know all possible DOM shapes at compile time.',
137
- filePath,
138
- node.location.line,
139
- node.location.column
140
- )
141
- }
142
-
143
- // Lower based on classification type
144
- switch (classification.type) {
145
- case 'conditional':
146
- return lowerConditionalExpression(
147
- node,
148
- classification.condition!,
149
- classification.consequent!,
150
- classification.alternate!,
151
- filePath,
152
- expressions
153
- )
154
-
155
- case 'optional':
156
- return lowerOptionalExpression(
157
- node,
158
- classification.optionalCondition!,
159
- classification.optionalFragment!,
160
- filePath,
161
- expressions
162
- )
163
-
164
- case 'loop':
165
- return lowerLoopExpression(
166
- node,
167
- classification.loopSource!,
168
- classification.loopItemVar!,
169
- classification.loopIndexVar,
170
- classification.loopBody!,
171
- filePath,
172
- expressions
173
- )
174
-
175
- case 'fragment':
176
- return lowerInlineFragment(
177
- node,
178
- classification.fragmentCode!,
179
- filePath,
180
- expressions
181
- )
182
-
183
- default:
184
- // Should not reach here
185
- return node
186
- }
187
- }
188
-
189
- /**
190
- * Lower conditional expression: condition ? <A /> : <B />
191
- *
192
- * Both branches are parsed and compiled at compile time.
193
- */
194
- function lowerConditionalExpression(
195
- node: ExpressionNode,
196
- condition: string,
197
- consequentCode: string,
198
- alternateCode: string,
199
- filePath: string,
200
- expressions: ExpressionIR[]
201
- ): ConditionalFragmentNode {
202
- // Register condition
203
- const conditionId = registerExpression(condition, node.location, expressions)
204
-
205
- // Parse both branches as JSX fragments
206
- const consequent = parseJSXToNodes(consequentCode, node.location, filePath, expressions, node.loopContext)
207
- const alternate = parseJSXToNodes(alternateCode, node.location, filePath, expressions, node.loopContext)
208
-
209
- return {
210
- type: 'conditional-fragment',
211
- condition: conditionId,
212
- consequent,
213
- alternate,
214
- location: node.location,
215
- loopContext: node.loopContext
216
- }
217
- }
218
-
219
- /**
220
- * Lower optional expression: condition && <A />
221
- *
222
- * Fragment is parsed and compiled at compile time.
223
- */
224
- function lowerOptionalExpression(
225
- node: ExpressionNode,
226
- condition: string,
227
- fragmentCode: string,
228
- filePath: string,
229
- expressions: ExpressionIR[]
230
- ): OptionalFragmentNode {
231
- // Register condition
232
- const conditionId = registerExpression(condition, node.location, expressions)
233
-
234
- const fragment = parseJSXToNodes(fragmentCode, node.location, filePath, expressions, node.loopContext)
235
-
236
- return {
237
- type: 'optional-fragment',
238
- condition: conditionId,
239
- fragment,
240
- location: node.location,
241
- loopContext: node.loopContext
242
- }
243
- }
244
-
245
- /**
246
- * Lower loop expression: items.map(item => <li>...</li>)
247
- *
248
- * Body is parsed and compiled once, instantiated per item at runtime.
249
- */
250
- function lowerLoopExpression(
251
- node: ExpressionNode,
252
- source: string,
253
- itemVar: string,
254
- indexVar: string | undefined,
255
- bodyCode: string,
256
- filePath: string,
257
- expressions: ExpressionIR[]
258
- ): LoopFragmentNode {
259
- // Register loop source as an expression ID
260
- const sourceId = registerExpression(source, node.location, expressions)
261
-
262
- // Create loop context for the body
263
- const loopVariables = [itemVar]
264
- if (indexVar) {
265
- loopVariables.push(indexVar)
266
- }
267
-
268
- const bodyLoopContext: LoopContext = {
269
- variables: node.loopContext
270
- ? [...node.loopContext.variables, ...loopVariables]
271
- : loopVariables,
272
- mapSource: sourceId // Use expression ID here
273
- }
274
-
275
- // Parse body with loop context
276
- const body = parseJSXToNodes(bodyCode, node.location, filePath, expressions, bodyLoopContext)
277
-
278
- return {
279
- type: 'loop-fragment',
280
- source: sourceId, // Use expression ID here
281
- itemVar,
282
- indexVar,
283
- body,
284
- location: node.location,
285
- loopContext: bodyLoopContext
286
- }
287
- }
288
-
289
- /**
290
- * Lower inline fragment: <A /> or <><A /><B /></>
291
- *
292
- * JSX is parsed and inlined directly into the node tree.
293
- * Returns the original expression node since inline JSX
294
- * is already handled by the expression transformer.
295
- */
296
- function lowerInlineFragment(
297
- node: ExpressionNode,
298
- fragmentCode: string,
299
- filePath: string,
300
- expressions: ExpressionIR[]
301
- ): TemplateNode {
302
- // For now, inline fragments are handled by the existing expression transformer
303
- // which converts JSX to __zenith.h() calls
304
- // In a future iteration, we could parse them to static nodes here
305
- return node
306
- }
307
-
308
- /**
309
- * Lower <html-content> element directive
310
- *
311
- * Syntax: <html-content content="expr" />
312
- *
313
- * This renders the expression value as raw HTML (innerHTML).
314
- * Use with caution - only for trusted content like icons defined in code.
315
- */
316
- function lowerHtmlContentElement(
317
- node: import('../ir/types').ElementNode,
318
- filePath: string,
319
- expressions: ExpressionIR[]
320
- ): TemplateNode {
321
- // Extract 'content' attribute
322
- const contentAttr = node.attributes.find(a => a.name === 'content')
323
-
324
- if (!contentAttr || typeof contentAttr.value !== 'string') {
325
- throw new InvariantError(
326
- 'ZEN001',
327
- `<html-content> element requires a 'content' attribute specifying the expression`,
328
- 'Usage: <html-content content="item.html" />',
329
- filePath,
330
- node.location.line,
331
- node.location.column
332
- )
333
- }
334
-
335
- const exprCode = contentAttr.value.trim()
336
-
337
- // Generate expression ID and register the expression
338
- const exprId = `expr_${expressions.length}`
339
- const exprIR: ExpressionIR = {
340
- id: exprId,
341
- code: exprCode,
342
- location: node.location
343
- }
344
- expressions.push(exprIR)
345
-
346
- // Create a span element with data-zen-html attribute for raw HTML binding
347
- return {
348
- type: 'element',
349
- tag: 'span',
350
- attributes: [
351
- { name: 'data-zen-html', value: exprIR, location: node.location, loopContext: node.loopContext },
352
- { name: 'style', value: 'display: contents;', location: node.location }
353
- ],
354
- children: [],
355
- location: node.location,
356
- loopContext: node.loopContext
357
- }
358
- }
359
-
360
- /**
361
- * Register a raw expression code string and return its ID
362
- */
363
- function registerExpression(
364
- code: string,
365
- location: SourceLocation,
366
- expressions: ExpressionIR[]
367
- ): string {
368
- const id = `expr_${expressions.length}`
369
- expressions.push({ id, code, location })
370
- return id
371
- }
372
-
373
- /**
374
- * INV-EXPR-REG-001: Assert no raw expressions exist beyond parse phase
375
- */
376
- function assertNoRawExpressions(node: TemplateNode, filePath: string): void {
377
- const errorPrefix = `Compiler invariant violated: runtime expression used without registration. This is a compiler bug, not a user error.`
378
-
379
- switch (node.type) {
380
- case 'expression':
381
- if (!node.expression.startsWith('expr_')) {
382
- throw new InvariantError(
383
- 'INV-EXPR-REG-001',
384
- `${errorPrefix}\nRaw expression found: "${node.expression}"`,
385
- 'Every expression must be registered and referenced by ID.',
386
- filePath,
387
- node.location.line,
388
- node.location.column
389
- )
390
- }
391
- break
392
-
393
- case 'loop-fragment':
394
- if (!node.source.startsWith('expr_')) {
395
- throw new InvariantError(
396
- 'INV-EXPR-REG-001',
397
- `${errorPrefix}\nRaw loop source found: "${node.source}"`,
398
- 'Loop sources must be registered and referenced by ID.',
399
- filePath,
400
- node.location.line,
401
- node.location.column
402
- )
403
- }
404
- node.body.forEach(child => assertNoRawExpressions(child, filePath))
405
- break
406
-
407
- case 'conditional-fragment':
408
- if (!node.condition.startsWith('expr_')) {
409
- throw new InvariantError(
410
- 'INV-EXPR-REG-001',
411
- `${errorPrefix}\nRaw condition found: "${node.condition}"`,
412
- 'Conditional expressions must be registered and referenced by ID.',
413
- filePath,
414
- node.location.line,
415
- node.location.column
416
- )
417
- }
418
- node.consequent.forEach(child => assertNoRawExpressions(child, filePath))
419
- node.alternate.forEach(child => assertNoRawExpressions(child, filePath))
420
- break
421
-
422
- case 'optional-fragment':
423
- if (!node.condition.startsWith('expr_')) {
424
- throw new InvariantError(
425
- 'INV-EXPR-REG-001',
426
- `${errorPrefix}\nRaw condition found: "${node.condition}"`,
427
- 'Optional expressions must be registered and referenced by ID.',
428
- filePath,
429
- node.location.line,
430
- node.location.column
431
- )
432
- }
433
- node.fragment.forEach(child => assertNoRawExpressions(child, filePath))
434
- break
435
-
436
- case 'element':
437
- case 'component':
438
- node.attributes.forEach(attr => {
439
- if (typeof attr.value !== 'string' && !attr.value.id.startsWith('expr_')) {
440
- throw new InvariantError(
441
- 'INV-EXPR-REG-001',
442
- `${errorPrefix}\nRaw attribute expression found: "${(attr.value as any).code}"`,
443
- 'Attribute expressions must be registered and referenced by ID.',
444
- filePath,
445
- attr.location.line,
446
- attr.location.column
447
- )
448
- }
449
- })
450
- node.children.forEach(child => assertNoRawExpressions(child, filePath))
451
- break
452
- }
453
- }
454
-
455
- /**
456
- * Parse JSX code string into TemplateNode[]
457
- *
458
- * This is a simplified parser for JSX fragments within expressions.
459
- * It handles basic JSX structure for lowering purposes.
460
- */
461
- function parseJSXToNodes(
462
- code: string,
463
- baseLocation: SourceLocation,
464
- filePath: string,
465
- expressions: ExpressionIR[],
466
- loopContext?: LoopContext
467
- ): TemplateNode[] {
468
- const trimmed = code.trim()
469
-
470
- // Handle fragment syntax <>...</>
471
- if (trimmed.startsWith('<>')) {
472
- const content = extractFragmentContent(trimmed)
473
- return parseJSXChildren(content, baseLocation, filePath, expressions, loopContext)
474
- }
475
-
476
- // Handle single element
477
- if (trimmed.startsWith('<')) {
478
- const element = parseJSXElement(trimmed, baseLocation, filePath, expressions, loopContext)
479
- return element ? [element] : []
480
- }
481
-
482
- // Handle parenthesized expression
483
- if (trimmed.startsWith('(')) {
484
- const inner = trimmed.slice(1, -1).trim()
485
- return parseJSXToNodes(inner, baseLocation, filePath, expressions, loopContext)
486
- }
487
-
488
- // Not JSX - return as expression node
489
- return [{
490
- type: 'expression',
491
- expression: trimmed,
492
- location: baseLocation,
493
- loopContext
494
- }]
495
- }
496
-
497
- /**
498
- * Extract content from fragment syntax <>content</>
499
- */
500
- function extractFragmentContent(code: string): string {
501
- // Remove <> prefix and </> suffix
502
- const withoutOpen = code.slice(2)
503
- const closeIndex = withoutOpen.lastIndexOf('</>')
504
- if (closeIndex === -1) {
505
- return withoutOpen
506
- }
507
- return withoutOpen.slice(0, closeIndex)
508
- }
509
-
510
- /**
511
- * Parse JSX children content
512
- */
513
- function parseJSXChildren(
514
- content: string,
515
- baseLocation: SourceLocation,
516
- filePath: string,
517
- expressions: ExpressionIR[],
518
- loopContext?: LoopContext
519
- ): TemplateNode[] {
520
- const nodes: TemplateNode[] = []
521
- let i = 0
522
- let currentText = ''
523
-
524
- while (i < content.length) {
525
- const char = content[i]
526
-
527
- // Check for JSX element
528
- if (char === '<' && /[a-zA-Z]/.test(content[i + 1] || '')) {
529
- // Save accumulated text
530
- if (currentText.trim()) {
531
- nodes.push({
532
- type: 'text',
533
- value: currentText.trim(),
534
- location: baseLocation
535
- })
536
- currentText = ''
537
- }
538
-
539
- // Parse element
540
- const result = parseJSXElementWithEnd(content, i, baseLocation, filePath, expressions, loopContext)
541
- if (result) {
542
- nodes.push(result.node)
543
- i = result.endIndex
544
- continue
545
- }
546
- }
547
-
548
- // Check for expression {expr}
549
- if (char === '{') {
550
- const endBrace = findBalancedBraceEnd(content, i)
551
- if (endBrace !== -1) {
552
- // Save accumulated text
553
- if (currentText.trim()) {
554
- nodes.push({
555
- type: 'text',
556
- value: currentText.trim(),
557
- location: baseLocation
558
- })
559
- currentText = ''
560
- }
561
-
562
- const exprCode = content.slice(i + 1, endBrace - 1).trim()
563
- if (exprCode) {
564
- // Register inner expression
565
- const exprId = registerExpression(exprCode, baseLocation, expressions)
566
- nodes.push({
567
- type: 'expression',
568
- expression: exprId,
569
- location: baseLocation,
570
- loopContext
571
- })
572
- }
573
- i = endBrace
574
- continue
575
- }
576
- }
577
-
578
- currentText += char
579
- i++
580
- }
581
-
582
- // Add remaining text
583
- if (currentText.trim()) {
584
- nodes.push({
585
- type: 'text',
586
- value: currentText.trim(),
587
- location: baseLocation
588
- })
589
- }
590
-
591
- return nodes
592
- }
593
-
594
- /**
595
- * Parse a single JSX element
596
- */
597
- function parseJSXElement(
598
- code: string,
599
- baseLocation: SourceLocation,
600
- filePath: string,
601
- expressions: ExpressionIR[],
602
- loopContext?: LoopContext
603
- ): TemplateNode | null {
604
- const result = parseJSXElementWithEnd(code, 0, baseLocation, filePath, expressions, loopContext)
605
- return result ? result.node : null
606
- }
607
-
608
- /**
609
- * Parse JSX element and return end index
610
- */
611
- function parseJSXElementWithEnd(
612
- code: string,
613
- startIndex: number,
614
- baseLocation: SourceLocation,
615
- filePath: string,
616
- expressions: ExpressionIR[],
617
- loopContext?: LoopContext
618
- ): { node: TemplateNode; endIndex: number } | null {
619
- // Extract tag name
620
- const tagMatch = code.slice(startIndex).match(/^<([a-zA-Z][a-zA-Z0-9.]*)/)
621
- if (!tagMatch) return null
622
-
623
- const tagName = tagMatch[1]!
624
- let i = startIndex + tagMatch[0].length
625
-
626
- // Parse attributes (simplified)
627
- const attributes: AttributeIR[] = []
628
-
629
- // Skip whitespace and parse attributes until > or />
630
- while (i < code.length) {
631
- // Skip whitespace
632
- while (i < code.length && /\s/.test(code[i]!)) i++
633
-
634
- // Check for end of opening tag
635
- if (code[i] === '>') {
636
- i++
637
- break
638
- }
639
- if (code[i] === '/' && code[i + 1] === '>') {
640
- // Self-closing tag
641
- const isComponent = tagName[0] === tagName[0]!.toUpperCase()
642
- const node: TemplateNode = isComponent ? {
643
- type: 'component',
644
- name: tagName,
645
- attributes: attributes.map(a => ({ ...a, value: a.value })),
646
- children: [],
647
- location: baseLocation,
648
- loopContext
649
- } : {
650
- type: 'element',
651
- tag: tagName.toLowerCase(),
652
- attributes: attributes.map(a => ({ ...a, value: a.value })),
653
- children: [],
654
- location: baseLocation,
655
- loopContext
656
- }
657
- return { node, endIndex: i + 2 }
658
- }
659
-
660
- // Parse attribute name
661
- const attrMatch = code.slice(i).match(/^([a-zA-Z_][a-zA-Z0-9_-]*)/)
662
- if (!attrMatch) {
663
- i++
664
- continue
665
- }
666
-
667
- const attrName = attrMatch[1]!
668
- i += attrName.length
669
-
670
- // Skip whitespace
671
- while (i < code.length && /\s/.test(code[i]!)) i++
672
-
673
- // Check for value
674
- if (code[i] !== '=') {
675
- attributes.push({ name: attrName, value: 'true', location: baseLocation })
676
- continue
677
- }
678
- i++ // Skip =
679
-
680
- // Skip whitespace
681
- while (i < code.length && /\s/.test(code[i]!)) i++
682
-
683
- // Parse value
684
- if (code[i] === '"' || code[i] === "'") {
685
- const quote = code[i]
686
- let endQuote = i + 1
687
- while (endQuote < code.length && code[endQuote] !== quote) {
688
- if (code[endQuote] === '\\') endQuote++
689
- endQuote++
690
- }
691
- attributes.push({ name: attrName, value: code.slice(i + 1, endQuote), location: baseLocation })
692
- i = endQuote + 1
693
- } else if (code[i] === '{') {
694
- const endBrace = findBalancedBraceEnd(code, i)
695
- if (endBrace !== -1) {
696
- const exprCode = code.slice(i + 1, endBrace - 1).trim()
697
- const exprId = registerExpression(exprCode, baseLocation, expressions)
698
-
699
- attributes.push({
700
- name: attrName,
701
- value: { id: exprId, code: exprCode, location: baseLocation },
702
- location: baseLocation
703
- })
704
- i = endBrace
705
- }
706
- }
707
- }
708
-
709
- // Parse children until closing tag
710
- const closeTag = `</${tagName}>`
711
- const closeIndex = findClosingTag(code, i, tagName)
712
-
713
- let children: TemplateNode[] = []
714
- if (closeIndex !== -1 && closeIndex > i) {
715
- const childContent = code.slice(i, closeIndex)
716
- children = parseJSXChildren(childContent, baseLocation, filePath, expressions, loopContext)
717
- i = closeIndex + closeTag.length
718
- }
719
-
720
- const isComponent = tagName[0] === tagName[0]!.toUpperCase()
721
- const node: TemplateNode = isComponent ? {
722
- type: 'component',
723
- name: tagName,
724
- attributes: attributes.map(a => ({ ...a, value: a.value })),
725
- children,
726
- location: baseLocation,
727
- loopContext
728
- } : {
729
- type: 'element',
730
- tag: tagName.toLowerCase(),
731
- attributes: attributes.map(a => ({ ...a, value: a.value })),
732
- children,
733
- location: baseLocation,
734
- loopContext
735
- }
736
-
737
- return { node, endIndex: i }
738
- }
739
-
740
- /**
741
- * Find closing tag for an element
742
- */
743
- function findClosingTag(code: string, startIndex: number, tagName: string): number {
744
- const closeTag = `</${tagName}>`
745
- let depth = 1
746
- let i = startIndex
747
-
748
- while (i < code.length && depth > 0) {
749
- // Check for closing tag
750
- if (code.slice(i, i + closeTag.length) === closeTag) {
751
- depth--
752
- if (depth === 0) return i
753
- i += closeTag.length
754
- continue
755
- }
756
-
757
- // Check for opening tag (same name, nested)
758
- const openPattern = new RegExp(`^<${tagName}(?:\\s|>|/>)`)
759
- const match = code.slice(i).match(openPattern)
760
- if (match) {
761
- // Check if self-closing
762
- const selfClosing = code.slice(i).match(new RegExp(`^<${tagName}[^>]*/>`))
763
- if (!selfClosing) {
764
- depth++
765
- }
766
- i += match[0].length
767
- continue
768
- }
769
-
770
- i++
771
- }
772
-
773
- return -1
774
- }
775
-
776
- /**
777
- * Find balanced brace end
778
- */
779
- function findBalancedBraceEnd(code: string, startIndex: number): number {
780
- if (code[startIndex] !== '{') return -1
781
-
782
- let depth = 1
783
- let i = startIndex + 1
784
- let inString = false
785
- let stringChar = ''
786
-
787
- while (i < code.length && depth > 0) {
788
- const char = code[i]
789
- const prevChar = code[i - 1]
790
-
791
- // Handle escape
792
- if (prevChar === '\\') {
793
- i++
794
- continue
795
- }
796
-
797
- // Handle strings
798
- if (!inString && (char === '"' || char === "'")) {
799
- inString = true
800
- stringChar = char
801
- i++
802
- continue
803
- }
804
- if (inString && char === stringChar) {
805
- inString = false
806
- i++
807
- continue
808
- }
809
-
810
- if (!inString) {
811
- if (char === '{') depth++
812
- else if (char === '}') depth--
813
- }
814
-
815
- i++
816
- }
817
-
818
- return depth === 0 ? i : -1
819
- }