@zenithbuild/core 0.4.6 → 0.5.0-beta.2.1

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 (109) hide show
  1. package/CORE_CONTRACT.md +143 -0
  2. package/README.md +11 -31
  3. package/bin/zenith.js +68 -0
  4. package/package.json +40 -52
  5. package/src/config.js +134 -0
  6. package/src/core-template.js +30 -0
  7. package/src/errors.js +54 -0
  8. package/src/guards.js +61 -0
  9. package/src/hash.js +52 -0
  10. package/src/index.js +26 -0
  11. package/src/ir/index.js +1 -0
  12. package/src/order.js +69 -0
  13. package/src/path.js +131 -0
  14. package/src/schema.js +28 -0
  15. package/src/version.js +67 -0
  16. package/bin/zen-build.ts +0 -2
  17. package/bin/zen-dev.ts +0 -2
  18. package/bin/zen-preview.ts +0 -2
  19. package/bin/zenith.ts +0 -2
  20. package/cli/commands/add.ts +0 -37
  21. package/cli/commands/build.ts +0 -37
  22. package/cli/commands/create.ts +0 -702
  23. package/cli/commands/dev.ts +0 -335
  24. package/cli/commands/index.ts +0 -112
  25. package/cli/commands/preview.ts +0 -62
  26. package/cli/commands/remove.ts +0 -33
  27. package/cli/index.ts +0 -10
  28. package/cli/main.ts +0 -101
  29. package/cli/utils/branding.ts +0 -178
  30. package/cli/utils/content.ts +0 -112
  31. package/cli/utils/logger.ts +0 -46
  32. package/cli/utils/plugin-manager.ts +0 -114
  33. package/cli/utils/project.ts +0 -77
  34. package/compiler/README.md +0 -380
  35. package/compiler/build-analyzer.ts +0 -122
  36. package/compiler/css/index.ts +0 -317
  37. package/compiler/discovery/componentDiscovery.ts +0 -174
  38. package/compiler/discovery/layouts.ts +0 -61
  39. package/compiler/errors/compilerError.ts +0 -56
  40. package/compiler/finalize/finalizeOutput.ts +0 -192
  41. package/compiler/finalize/generateFinalBundle.ts +0 -82
  42. package/compiler/index.ts +0 -81
  43. package/compiler/ir/types.ts +0 -150
  44. package/compiler/output/types.ts +0 -34
  45. package/compiler/parse/detectMapExpressions.ts +0 -102
  46. package/compiler/parse/parseScript.ts +0 -46
  47. package/compiler/parse/parseTemplate.ts +0 -591
  48. package/compiler/parse/parseZenFile.ts +0 -66
  49. package/compiler/parse/scriptAnalysis.ts +0 -83
  50. package/compiler/parse/trackLoopContext.ts +0 -82
  51. package/compiler/runtime/dataExposure.ts +0 -317
  52. package/compiler/runtime/generateDOM.ts +0 -246
  53. package/compiler/runtime/generateHydrationBundle.ts +0 -407
  54. package/compiler/runtime/hydration.ts +0 -309
  55. package/compiler/runtime/navigation.ts +0 -432
  56. package/compiler/runtime/thinRuntime.ts +0 -160
  57. package/compiler/runtime/transformIR.ts +0 -343
  58. package/compiler/runtime/wrapExpression.ts +0 -95
  59. package/compiler/runtime/wrapExpressionWithLoop.ts +0 -83
  60. package/compiler/spa-build.ts +0 -917
  61. package/compiler/ssg-build.ts +0 -422
  62. package/compiler/test/validate-test.ts +0 -104
  63. package/compiler/transform/classifyExpression.ts +0 -444
  64. package/compiler/transform/componentResolver.ts +0 -289
  65. package/compiler/transform/expressionTransformer.ts +0 -385
  66. package/compiler/transform/fragmentLowering.ts +0 -634
  67. package/compiler/transform/generateBindings.ts +0 -47
  68. package/compiler/transform/generateHTML.ts +0 -28
  69. package/compiler/transform/layoutProcessor.ts +0 -132
  70. package/compiler/transform/slotResolver.ts +0 -292
  71. package/compiler/transform/transformNode.ts +0 -126
  72. package/compiler/transform/transformTemplate.ts +0 -38
  73. package/compiler/validate/invariants.ts +0 -292
  74. package/compiler/validate/validateExpressions.ts +0 -168
  75. package/core/config/index.ts +0 -16
  76. package/core/config/loader.ts +0 -69
  77. package/core/config/types.ts +0 -89
  78. package/core/index.ts +0 -135
  79. package/core/lifecycle/index.ts +0 -49
  80. package/core/lifecycle/zen-mount.ts +0 -182
  81. package/core/lifecycle/zen-unmount.ts +0 -88
  82. package/core/plugins/index.ts +0 -7
  83. package/core/plugins/registry.ts +0 -81
  84. package/core/reactivity/index.ts +0 -54
  85. package/core/reactivity/tracking.ts +0 -167
  86. package/core/reactivity/zen-batch.ts +0 -57
  87. package/core/reactivity/zen-effect.ts +0 -139
  88. package/core/reactivity/zen-memo.ts +0 -146
  89. package/core/reactivity/zen-ref.ts +0 -52
  90. package/core/reactivity/zen-signal.ts +0 -121
  91. package/core/reactivity/zen-state.ts +0 -180
  92. package/core/reactivity/zen-untrack.ts +0 -44
  93. package/dist/cli.js +0 -11653
  94. package/dist/zen-build.js +0 -15388
  95. package/dist/zen-dev.js +0 -15388
  96. package/dist/zen-preview.js +0 -15388
  97. package/dist/zenith.js +0 -15388
  98. package/router/index.ts +0 -76
  99. package/router/manifest.ts +0 -314
  100. package/router/navigation/ZenLink.zen +0 -231
  101. package/router/navigation/index.ts +0 -78
  102. package/router/navigation/zen-link.ts +0 -584
  103. package/router/runtime.ts +0 -458
  104. package/router/types.ts +0 -168
  105. package/runtime/build.ts +0 -17
  106. package/runtime/bundle-generator.ts +0 -800
  107. package/runtime/client-runtime.ts +0 -549
  108. package/runtime/serve.ts +0 -93
  109. package/tsconfig.json +0 -28
@@ -1,289 +0,0 @@
1
- /**
2
- * Component Resolution
3
- *
4
- * Resolves component nodes in IR by inlining component templates with slot substitution.
5
- * Uses compound component pattern for named slots (Card.Header, Card.Footer).
6
- */
7
-
8
- import type { TemplateNode, ComponentNode, ElementNode, ZenIR, LoopContext } from '../ir/types'
9
- import type { ComponentMetadata } from '../discovery/componentDiscovery'
10
- import { extractSlotsFromChildren, resolveSlots } from './slotResolver'
11
- import { throwOrphanCompoundError, throwUnresolvedComponentError } from '../validate/invariants'
12
-
13
- // Track which components have been used (for style collection)
14
- const usedComponents = new Set<string>()
15
-
16
- /**
17
- * Resolve all component nodes in a template IR
18
- *
19
- * Recursively replaces ComponentNode instances with their resolved templates
20
- * Also collects styles from used components and adds them to the IR
21
- */
22
- export function resolveComponentsInIR(
23
- ir: ZenIR,
24
- components: Map<string, ComponentMetadata>
25
- ): ZenIR {
26
- // Clear used components tracking for this compilation
27
- usedComponents.clear()
28
-
29
- // Resolve components in template nodes
30
- const resolvedNodes = resolveComponentsInNodes(ir.template.nodes, components)
31
-
32
- // Collect styles from all used components
33
- const componentStyles = Array.from(usedComponents)
34
- .map(name => components.get(name))
35
- .filter((meta): meta is ComponentMetadata => meta !== undefined && meta.styles.length > 0)
36
- .flatMap(meta => meta.styles.map(raw => ({ raw })))
37
-
38
- return {
39
- ...ir,
40
- template: {
41
- ...ir.template,
42
- nodes: resolvedNodes
43
- },
44
- // Merge component styles with existing page styles
45
- styles: [...ir.styles, ...componentStyles]
46
- }
47
- }
48
-
49
- /**
50
- * Resolve component nodes in a list of template nodes
51
- */
52
- function resolveComponentsInNodes(
53
- nodes: TemplateNode[],
54
- components: Map<string, ComponentMetadata>,
55
- depth: number = 0
56
- ): TemplateNode[] {
57
- const resolved: TemplateNode[] = []
58
-
59
- for (const node of nodes) {
60
- const resolvedNode = resolveComponentNode(node, components, depth)
61
-
62
- if (Array.isArray(resolvedNode)) {
63
- resolved.push(...resolvedNode)
64
- } else {
65
- resolved.push(resolvedNode)
66
- }
67
- }
68
-
69
- return resolved
70
- }
71
-
72
- /**
73
- * Resolve a single component node
74
- *
75
- * If the node is a component, look up its definition and inline it with slot resolution.
76
- * Otherwise, recursively process children.
77
- */
78
- function resolveComponentNode(
79
- node: TemplateNode,
80
- components: Map<string, ComponentMetadata>,
81
- depth: number = 0
82
- ): TemplateNode | TemplateNode[] {
83
- // Handle component nodes
84
- if (node.type === 'component') {
85
- return resolveComponent(node, components, depth)
86
- }
87
-
88
- // Handle element nodes - recursively resolve children
89
- if (node.type === 'element') {
90
- const resolvedChildren = resolveComponentsInNodes(node.children, components, depth + 1)
91
-
92
- return {
93
- ...node,
94
- children: resolvedChildren
95
- }
96
- }
97
-
98
- // Text and expression nodes pass through unchanged
99
- return node
100
- }
101
-
102
- /**
103
- * Get base component name from compound name
104
- *
105
- * "Card.Header" -> "Card"
106
- * "Button" -> "Button"
107
- */
108
- function getBaseComponentName(name: string): string {
109
- const dotIndex = name.indexOf('.')
110
- return dotIndex > 0 ? name.slice(0, dotIndex) : name
111
- }
112
-
113
- /**
114
- * Check if a component name is a compound slot marker
115
- *
116
- * "Card.Header" -> true (if Card exists)
117
- * "Card" -> false
118
- * "Button" -> false
119
- */
120
- function isCompoundSlotMarker(name: string, components: Map<string, ComponentMetadata>): boolean {
121
- const dotIndex = name.indexOf('.')
122
- if (dotIndex <= 0) return false
123
-
124
- const baseName = name.slice(0, dotIndex)
125
- return components.has(baseName)
126
- }
127
-
128
- /**
129
- * Resolve a component by inlining its template with slot substitution
130
- */
131
- function resolveComponent(
132
- componentNode: ComponentNode,
133
- components: Map<string, ComponentMetadata>,
134
- depth: number = 0
135
- ): TemplateNode | TemplateNode[] {
136
- const componentName = componentNode.name
137
-
138
- // Check if this is a compound slot marker (Card.Header, Card.Footer)
139
- // These are handled by the parent component, not resolved directly
140
- // INV007: Orphan compound slot markers are a compile-time error
141
- if (isCompoundSlotMarker(componentName, components)) {
142
- throwOrphanCompoundError(
143
- componentName,
144
- getBaseComponentName(componentName),
145
- 'component', // filePath not available here, will be caught by caller
146
- componentNode.location.line,
147
- componentNode.location.column
148
- )
149
- }
150
-
151
- // Look up component metadata
152
- const componentMeta = components.get(componentName)
153
-
154
- // INV003: Unresolved components are a compile-time error
155
- if (!componentMeta) {
156
- throwUnresolvedComponentError(
157
- componentName,
158
- 'component', // filePath not available here, will be caught by caller
159
- componentNode.location.line,
160
- componentNode.location.column
161
- )
162
- }
163
-
164
- // Track this component as used (for style collection)
165
- usedComponents.add(componentName)
166
-
167
- // Extract slots from component children FIRST (before resolving nested components)
168
- // This preserves compound component structure (Card.Header, Card.Footer)
169
- // IMPORTANT: Pass parent's loopContext to preserve reactive scope
170
- // Components are purely structural - they don't create new reactive boundaries
171
- const slots = extractSlotsFromChildren(
172
- componentName,
173
- componentNode.children,
174
- componentNode.loopContext // Preserve parent's reactive scope
175
- )
176
-
177
- // Now resolve nested components within the extracted slot content
178
- const resolvedSlots = {
179
- default: resolveComponentsInNodes(slots.default, components, depth + 1),
180
- named: new Map<string, TemplateNode[]>(),
181
- parentLoopContext: slots.parentLoopContext // Carry through the parent scope
182
- }
183
-
184
- for (const [slotName, slotContent] of slots.named) {
185
- resolvedSlots.named.set(slotName, resolveComponentsInNodes(slotContent, components, depth + 1))
186
- }
187
-
188
- // Deep clone the component template nodes to avoid mutation
189
- const templateNodes = JSON.parse(JSON.stringify(componentMeta.nodes)) as TemplateNode[]
190
-
191
- // Resolve slots in component template
192
- const resolvedTemplate = resolveSlots(templateNodes, resolvedSlots)
193
-
194
- // Forward attributes from component usage to the root element
195
- // This ensures onclick, class, and other attributes are preserved
196
- const forwardedTemplate = forwardAttributesToRoot(
197
- resolvedTemplate,
198
- componentNode.attributes,
199
- componentNode.loopContext
200
- )
201
-
202
- // Recursively resolve any nested components in the resolved template
203
- const fullyResolved = resolveComponentsInNodes(forwardedTemplate, components, depth + 1)
204
-
205
- return fullyResolved
206
- }
207
-
208
- /**
209
- * Forward attributes from component usage to the template's root element
210
- *
211
- * When using <Button onclick="increment">Text</Button>,
212
- * the onclick should be applied to the <button> element in Button.zen template.
213
- */
214
- function forwardAttributesToRoot(
215
- nodes: TemplateNode[],
216
- attributes: ComponentNode['attributes'],
217
- loopContext?: LoopContext
218
- ): TemplateNode[] {
219
- if (attributes.length === 0) {
220
- return nodes
221
- }
222
-
223
- // Find the first non-text element (the root element)
224
- const rootIndex = nodes.findIndex(n => n.type === 'element')
225
- if (rootIndex === -1) {
226
- return nodes
227
- }
228
-
229
- const root = nodes[rootIndex] as ElementNode
230
-
231
- // Merge attributes: component usage attributes override template defaults
232
- // Also preserve the parent's loopContext on forwarded attributes
233
- const mergedAttributes = [...root.attributes]
234
-
235
- for (const attr of attributes) {
236
- const existingIndex = mergedAttributes.findIndex(a => a.name === attr.name)
237
-
238
- // Attach parent's loopContext to forwarded attributes to preserve reactivity
239
- const forwardedAttr = {
240
- ...attr,
241
- loopContext: attr.loopContext || loopContext
242
- }
243
-
244
- if (existingIndex >= 0) {
245
- const existingAttr = mergedAttributes[existingIndex]!
246
- // Special handling for class: merge classes
247
- if (attr.name === 'class' && typeof attr.value === 'string' && typeof existingAttr.value === 'string') {
248
- mergedAttributes[existingIndex] = {
249
- ...existingAttr,
250
- value: `${existingAttr.value} ${attr.value}`
251
- }
252
- } else {
253
- // Override other attributes
254
- mergedAttributes[existingIndex] = forwardedAttr
255
- }
256
- } else {
257
- // Add new attribute
258
- mergedAttributes.push(forwardedAttr)
259
- }
260
- }
261
-
262
- // Return updated nodes with root element having merged attributes
263
- return [
264
- ...nodes.slice(0, rootIndex),
265
- {
266
- ...root,
267
- attributes: mergedAttributes,
268
- loopContext: root.loopContext || loopContext
269
- },
270
- ...nodes.slice(rootIndex + 1)
271
- ]
272
- }
273
-
274
- /**
275
- * Check if an IR contains any component nodes
276
- */
277
- export function hasComponents(nodes: TemplateNode[]): boolean {
278
- function checkNode(node: TemplateNode): boolean {
279
- if (node.type === 'component') {
280
- return true
281
- }
282
- if (node.type === 'element') {
283
- return node.children.some(checkNode)
284
- }
285
- return false
286
- }
287
-
288
- return nodes.some(checkNode)
289
- }
@@ -1,385 +0,0 @@
1
- /**
2
- * Expression JSX Transformer
3
- *
4
- * Transforms JSX-like tags inside Zenith expressions into __zenith.h() calls.
5
- * This allows Zenith to support JSX semantics without a full JSX compiler like Babel.
6
- *
7
- * Handles:
8
- * - Multi-line JSX expressions
9
- * - Nested elements
10
- * - Complex event handlers like onclick={() => fn(item)}
11
- * - Expression attributes {expr}
12
- * - Text interpolation {item.title}
13
- */
14
-
15
- /**
16
- * Find the end of a balanced brace expression
17
- */
18
- function findBalancedBraceEnd(code: string, startIndex: number): number {
19
- let braceCount = 1
20
- let i = startIndex + 1
21
- let inString = false
22
- let stringChar = ''
23
- let inTemplate = false
24
-
25
- while (i < code.length && braceCount > 0) {
26
- const char = code[i]
27
- const prevChar = i > 0 ? code[i - 1] : ''
28
-
29
- // Handle escape sequences
30
- if (prevChar === '\\') {
31
- i++
32
- continue
33
- }
34
-
35
- // Handle string literals
36
- if (!inString && !inTemplate && (char === '"' || char === "'")) {
37
- inString = true
38
- stringChar = char
39
- i++
40
- continue
41
- }
42
-
43
- if (inString && char === stringChar) {
44
- inString = false
45
- stringChar = ''
46
- i++
47
- continue
48
- }
49
-
50
- // Handle template literals
51
- if (!inString && !inTemplate && char === '`') {
52
- inTemplate = true
53
- i++
54
- continue
55
- }
56
-
57
- if (inTemplate && char === '`') {
58
- inTemplate = false
59
- i++
60
- continue
61
- }
62
-
63
- // Count braces only when not in strings
64
- if (!inString && !inTemplate) {
65
- if (char === '{') braceCount++
66
- else if (char === '}') braceCount--
67
- }
68
-
69
- i++
70
- }
71
-
72
- return braceCount === 0 ? i : -1
73
- }
74
-
75
- /**
76
- * Parse JSX attributes using balanced parsing for expression values
77
- */
78
- function parseJSXAttributes(code: string, startIndex: number): {
79
- attrs: string;
80
- endIndex: number;
81
- isSelfClosing: boolean
82
- } {
83
- const attrPairs: string[] = []
84
- let i = startIndex
85
-
86
- // Skip whitespace
87
- while (i < code.length && /\s/.test(code[i]!)) i++
88
-
89
- while (i < code.length) {
90
- const char = code[i]
91
-
92
- // Check for end of opening tag
93
- if (char === '>') {
94
- return { attrs: formatAttrs(attrPairs), endIndex: i + 1, isSelfClosing: false }
95
- }
96
- if (char === '/' && code[i + 1] === '>') {
97
- return { attrs: formatAttrs(attrPairs), endIndex: i + 2, isSelfClosing: true }
98
- }
99
-
100
- // Parse attribute name
101
- const nameMatch = code.slice(i).match(/^([a-zA-Z_][a-zA-Z0-9_-]*)/)
102
- if (!nameMatch) {
103
- i++
104
- continue
105
- }
106
-
107
- const attrName = nameMatch[1]!
108
- i += attrName.length
109
-
110
- // Skip whitespace
111
- while (i < code.length && /\s/.test(code[i]!)) i++
112
-
113
- // Check for value
114
- if (code[i] !== '=') {
115
- attrPairs.push(`"${attrName}": true`)
116
- continue
117
- }
118
-
119
- i++ // Skip '='
120
-
121
- // Skip whitespace
122
- while (i < code.length && /\s/.test(code[i]!)) i++
123
-
124
- // Parse value
125
- if (code[i] === '"' || code[i] === "'") {
126
- const quote = code[i]
127
- let endQuote = i + 1
128
- while (endQuote < code.length && code[endQuote] !== quote) {
129
- if (code[endQuote] === '\\') endQuote++ // Skip escaped chars
130
- endQuote++
131
- }
132
- const value = code.slice(i + 1, endQuote)
133
- attrPairs.push(`"${attrName}": "${value}"`)
134
- i = endQuote + 1
135
- } else if (code[i] === '{') {
136
- // Expression value - find balanced end
137
- const endBrace = findBalancedBraceEnd(code, i)
138
- if (endBrace === -1) {
139
- i++
140
- continue
141
- }
142
- const expr = code.slice(i + 1, endBrace - 1).trim()
143
- attrPairs.push(`"${attrName}": ${expr}`)
144
- i = endBrace
145
- } else {
146
- // Unquoted value (rare in JSX, but support it)
147
- const unquotedMatch = code.slice(i).match(/^([^\s/>]+)/)
148
- if (unquotedMatch) {
149
- attrPairs.push(`"${attrName}": "${unquotedMatch[1]}"`)
150
- i += unquotedMatch[1]!.length
151
- }
152
- }
153
-
154
- // Skip whitespace
155
- while (i < code.length && /\s/.test(code[i]!)) i++
156
- }
157
-
158
- return { attrs: formatAttrs(attrPairs), endIndex: i, isSelfClosing: false }
159
- }
160
-
161
- function formatAttrs(pairs: string[]): string {
162
- return pairs.length > 0 ? `{ ${pairs.join(', ')} }` : 'null'
163
- }
164
-
165
- /**
166
- * Find the matching closing tag for an element
167
- */
168
- function findClosingTag(code: string, startIndex: number, tagName: string): number {
169
- let depth = 1
170
- let i = startIndex
171
- const openPattern = new RegExp(`<${tagName}(?:\\s|>|/>)`, 'i')
172
- const closeTag = `</${tagName}>`
173
-
174
- while (i < code.length && depth > 0) {
175
- // Check for closing tag
176
- if (code.slice(i, i + closeTag.length).toLowerCase() === closeTag.toLowerCase()) {
177
- depth--
178
- if (depth === 0) return i
179
- i += closeTag.length
180
- continue
181
- }
182
-
183
- // Check for opening tag (same name, nested)
184
- const openMatch = code.slice(i).match(openPattern)
185
- if (openMatch && openMatch.index === 0) {
186
- // Check if it's self-closing
187
- const selfClosing = code.slice(i).match(new RegExp(`<${tagName}[^>]*/>`, 'i'))
188
- if (!selfClosing || selfClosing.index !== 0) {
189
- depth++
190
- }
191
- i += openMatch[0].length
192
- continue
193
- }
194
-
195
- i++
196
- }
197
-
198
- return -1
199
- }
200
-
201
- /**
202
- * Parse JSX children content
203
- */
204
- function parseJSXChildren(code: string, startIndex: number, tagName: string): {
205
- children: string;
206
- endIndex: number
207
- } {
208
- const closingIndex = findClosingTag(code, startIndex, tagName)
209
- if (closingIndex === -1) {
210
- return { children: 'null', endIndex: code.length }
211
- }
212
-
213
- const content = code.slice(startIndex, closingIndex)
214
-
215
- if (!content.trim()) {
216
- return { children: 'null', endIndex: closingIndex }
217
- }
218
-
219
- // Transform the children content
220
- const transformedContent = transformChildContent(content)
221
-
222
- return { children: transformedContent, endIndex: closingIndex }
223
- }
224
-
225
- /**
226
- * Transform content that may contain text, expressions, and nested JSX
227
- */
228
- function transformChildContent(content: string): string {
229
- const parts: string[] = []
230
- let i = 0
231
- let currentText = ''
232
-
233
- while (i < content.length) {
234
- const char = content[i]
235
-
236
- // Check for JSX element
237
- if (char === '<' && /[a-zA-Z]/.test(content[i + 1] || '')) {
238
- // Save any accumulated text
239
- if (currentText.trim()) {
240
- parts.push(`"${escapeString(currentText.trim())}"`)
241
- currentText = ''
242
- }
243
-
244
- // Try to parse as JSX element
245
- const parsed = parseJSXElement(content, i)
246
- if (parsed) {
247
- parts.push(parsed.hCall)
248
- i = parsed.endIndex
249
- continue
250
- }
251
- }
252
-
253
- // Check for expression {expr}
254
- if (char === '{') {
255
- const endBrace = findBalancedBraceEnd(content, i)
256
- if (endBrace !== -1) {
257
- // Save any accumulated text
258
- if (currentText.trim()) {
259
- parts.push(`"${escapeString(currentText.trim())}"`)
260
- currentText = ''
261
- }
262
-
263
- // Extract and add expression
264
- const expr = content.slice(i + 1, endBrace - 1).trim()
265
- if (expr) {
266
- // Transform any JSX inside the expression
267
- const transformedExpr = transformExpressionJSX(expr)
268
- parts.push(transformedExpr)
269
- }
270
- i = endBrace
271
- continue
272
- }
273
- }
274
-
275
- // Accumulate text
276
- currentText += char
277
- i++
278
- }
279
-
280
- // Add remaining text
281
- if (currentText.trim()) {
282
- parts.push(`"${escapeString(currentText.trim())}"`)
283
- }
284
-
285
- if (parts.length === 0) return 'null'
286
- if (parts.length === 1) return parts[0]!
287
- return `[${parts.join(', ')}]`
288
- }
289
-
290
- /**
291
- * Escape a string for use in JavaScript
292
- */
293
- function escapeString(str: string): string {
294
- return str
295
- .replace(/\\/g, '\\\\')
296
- .replace(/"/g, '\\"')
297
- .replace(/\n/g, '\\n')
298
- .replace(/\r/g, '\\r')
299
- .replace(/\t/g, '\\t')
300
- }
301
-
302
- /**
303
- * Parse a single JSX element starting at the given index
304
- */
305
- function parseJSXElement(code: string, startIndex: number): { hCall: string; endIndex: number } | null {
306
- // Extract tag name
307
- const tagMatch = code.slice(startIndex).match(/^<([a-zA-Z][a-zA-Z0-9]*)/)
308
- if (!tagMatch) return null
309
-
310
- const tagName = tagMatch[1]!
311
- let i = startIndex + tagMatch[0].length
312
-
313
- // Parse attributes
314
- const { attrs, endIndex: attrEnd, isSelfClosing } = parseJSXAttributes(code, i)
315
- i = attrEnd
316
-
317
- if (isSelfClosing) {
318
- return {
319
- hCall: `__zenith.h("${tagName}", ${attrs}, null)`,
320
- endIndex: i
321
- }
322
- }
323
-
324
- // Parse children until closing tag
325
- const { children, endIndex: childEnd } = parseJSXChildren(code, i, tagName)
326
- i = childEnd
327
-
328
- // Skip closing tag
329
- const closeTag = `</${tagName}>`
330
- if (code.slice(i, i + closeTag.length).toLowerCase() === closeTag.toLowerCase()) {
331
- i += closeTag.length
332
- }
333
-
334
- return {
335
- hCall: `__zenith.h("${tagName}", ${attrs}, ${children})`,
336
- endIndex: i
337
- }
338
- }
339
-
340
- /**
341
- * Main transformer function
342
- *
343
- * Transforms JSX-like tags inside Zenith expressions into __zenith.h() calls.
344
- */
345
- export function transformExpressionJSX(code: string): string {
346
- // Skip if no JSX-like content (optimization)
347
- if (!/<[a-zA-Z]/.test(code)) {
348
- return code
349
- }
350
-
351
- let result = ''
352
- let i = 0
353
-
354
- while (i < code.length) {
355
- // Look for potential JSX tag start
356
- // Only treat as JSX if it follows common JSX contexts: (, return, =, :, ,, [, ?
357
- if (code[i] === '<' && /[a-zA-Z]/.test(code[i + 1] || '')) {
358
- // Check if this looks like a JSX context
359
- const beforeChar = i > 0 ? code[i - 1] : ''
360
- const beforeTrimmed = code.slice(0, i).trimEnd()
361
- const lastChar = beforeTrimmed[beforeTrimmed.length - 1] || ''
362
-
363
- // Common JSX-starting contexts
364
- const jsxContexts = ['(', '=', ':', ',', '[', '?', '{', 'n'] // 'n' for 'return'
365
- const isJSXContext = jsxContexts.includes(lastChar) ||
366
- beforeTrimmed.endsWith('return') ||
367
- beforeTrimmed === '' ||
368
- (beforeChar && /\s/.test(beforeChar))
369
-
370
- if (isJSXContext) {
371
- const parsed = parseJSXElement(code, i)
372
- if (parsed) {
373
- result += parsed.hCall
374
- i = parsed.endIndex
375
- continue
376
- }
377
- }
378
- }
379
-
380
- result += code[i]
381
- i++
382
- }
383
-
384
- return result
385
- }