@zenithbuild/core 1.2.1 → 1.2.3

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