@zenithbuild/core 0.4.2 → 0.4.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.
- package/cli/commands/dev.ts +4 -1
- package/compiler/discovery/componentDiscovery.ts +174 -0
- package/compiler/errors/compilerError.ts +32 -0
- package/compiler/finalize/finalizeOutput.ts +37 -8
- package/compiler/index.ts +26 -5
- package/compiler/ir/types.ts +66 -0
- package/compiler/parse/parseTemplate.ts +66 -9
- package/compiler/runtime/generateDOM.ts +102 -1
- package/compiler/runtime/transformIR.ts +2 -2
- package/compiler/transform/classifyExpression.ts +444 -0
- package/compiler/transform/componentResolver.ts +289 -0
- package/compiler/transform/fragmentLowering.ts +634 -0
- package/compiler/transform/slotResolver.ts +292 -0
- package/compiler/validate/invariants.ts +292 -0
- package/package.json +1 -1
|
@@ -4,7 +4,16 @@
|
|
|
4
4
|
* Generates JavaScript code that creates DOM elements from template nodes
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
TemplateNode,
|
|
9
|
+
ElementNode,
|
|
10
|
+
TextNode,
|
|
11
|
+
ExpressionNode,
|
|
12
|
+
ExpressionIR,
|
|
13
|
+
ConditionalFragmentNode,
|
|
14
|
+
OptionalFragmentNode,
|
|
15
|
+
LoopFragmentNode
|
|
16
|
+
} from '../ir/types'
|
|
8
17
|
|
|
9
18
|
/**
|
|
10
19
|
* Generate DOM creation code from a template node
|
|
@@ -95,6 +104,98 @@ ${indent}}\n`
|
|
|
95
104
|
|
|
96
105
|
return { code, varName }
|
|
97
106
|
}
|
|
107
|
+
|
|
108
|
+
case 'component': {
|
|
109
|
+
// Components should be resolved before reaching DOM generation
|
|
110
|
+
// If we get here, it means component resolution failed
|
|
111
|
+
throw new Error(`[Zenith] Unresolved component: ${(node as any).name}. Components must be resolved before DOM generation.`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case 'conditional-fragment': {
|
|
115
|
+
// Conditional fragment: {condition ? <A /> : <B />}
|
|
116
|
+
// Both branches are precompiled, runtime toggles visibility
|
|
117
|
+
const condNode = node as ConditionalFragmentNode
|
|
118
|
+
const containerVar = varName
|
|
119
|
+
const conditionId = `cond_${varCounter.count++}`
|
|
120
|
+
|
|
121
|
+
let code = `${indent}const ${containerVar} = document.createDocumentFragment();\n`
|
|
122
|
+
code += `${indent}const ${conditionId}_result = (function() { with (state) { return ${condNode.condition}; } })();\n`
|
|
123
|
+
|
|
124
|
+
// Generate consequent branch
|
|
125
|
+
code += `${indent}if (${conditionId}_result) {\n`
|
|
126
|
+
for (const child of condNode.consequent) {
|
|
127
|
+
const childResult = generateDOMCode(child, expressions, indent + ' ', varCounter)
|
|
128
|
+
code += `${childResult.code}\n`
|
|
129
|
+
code += `${indent} ${containerVar}.appendChild(${childResult.varName});\n`
|
|
130
|
+
}
|
|
131
|
+
code += `${indent}} else {\n`
|
|
132
|
+
|
|
133
|
+
// Generate alternate branch
|
|
134
|
+
for (const child of condNode.alternate) {
|
|
135
|
+
const childResult = generateDOMCode(child, expressions, indent + ' ', varCounter)
|
|
136
|
+
code += `${childResult.code}\n`
|
|
137
|
+
code += `${indent} ${containerVar}.appendChild(${childResult.varName});\n`
|
|
138
|
+
}
|
|
139
|
+
code += `${indent}}\n`
|
|
140
|
+
|
|
141
|
+
return { code, varName: containerVar }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'optional-fragment': {
|
|
145
|
+
// Optional fragment: {condition && <A />}
|
|
146
|
+
// Fragment is precompiled, runtime mounts/unmounts based on condition
|
|
147
|
+
const optNode = node as OptionalFragmentNode
|
|
148
|
+
const containerVar = varName
|
|
149
|
+
const conditionId = `opt_${varCounter.count++}`
|
|
150
|
+
|
|
151
|
+
let code = `${indent}const ${containerVar} = document.createDocumentFragment();\n`
|
|
152
|
+
code += `${indent}const ${conditionId}_result = (function() { with (state) { return ${optNode.condition}; } })();\n`
|
|
153
|
+
code += `${indent}if (${conditionId}_result) {\n`
|
|
154
|
+
|
|
155
|
+
for (const child of optNode.fragment) {
|
|
156
|
+
const childResult = generateDOMCode(child, expressions, indent + ' ', varCounter)
|
|
157
|
+
code += `${childResult.code}\n`
|
|
158
|
+
code += `${indent} ${containerVar}.appendChild(${childResult.varName});\n`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
code += `${indent}}\n`
|
|
162
|
+
|
|
163
|
+
return { code, varName: containerVar }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case 'loop-fragment': {
|
|
167
|
+
// Loop fragment: {items.map(item => <li>...</li>)}
|
|
168
|
+
// Body is precompiled once, instantiated per item at runtime
|
|
169
|
+
const loopNode = node as LoopFragmentNode
|
|
170
|
+
const containerVar = varName
|
|
171
|
+
const loopId = `loop_${varCounter.count++}`
|
|
172
|
+
|
|
173
|
+
let code = `${indent}const ${containerVar} = document.createDocumentFragment();\n`
|
|
174
|
+
code += `${indent}const ${loopId}_items = (function() { with (state) { return ${loopNode.source}; } })() || [];\n`
|
|
175
|
+
|
|
176
|
+
// Loop parameters
|
|
177
|
+
const itemVar = loopNode.itemVar
|
|
178
|
+
const indexVar = loopNode.indexVar || `${loopId}_idx`
|
|
179
|
+
|
|
180
|
+
code += `${indent}${loopId}_items.forEach(function(${itemVar}, ${indexVar}) {\n`
|
|
181
|
+
|
|
182
|
+
// Generate loop body with loop context variables in scope
|
|
183
|
+
for (const child of loopNode.body) {
|
|
184
|
+
const childResult = generateDOMCode(child, expressions, indent + ' ', varCounter)
|
|
185
|
+
// Inject loop variables into the child code
|
|
186
|
+
let childCode = childResult.code
|
|
187
|
+
code += `${childCode}\n`
|
|
188
|
+
code += `${indent} ${containerVar}.appendChild(${childResult.varName});\n`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
code += `${indent}});\n`
|
|
192
|
+
|
|
193
|
+
return { code, varName: containerVar }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
default: {
|
|
197
|
+
throw new Error(`[Zenith] Unknown node type: ${(node as any).type}`)
|
|
198
|
+
}
|
|
98
199
|
}
|
|
99
200
|
}
|
|
100
201
|
|
|
@@ -197,8 +197,8 @@ if (typeof window !== 'undefined') {
|
|
|
197
197
|
if (document.readyState === 'loading') {
|
|
198
198
|
document.addEventListener('DOMContentLoaded', autoHydrate);
|
|
199
199
|
} else {
|
|
200
|
-
// DOM already loaded,
|
|
201
|
-
|
|
200
|
+
// DOM already loaded, hydrate immediately
|
|
201
|
+
autoHydrate();
|
|
202
202
|
}
|
|
203
203
|
})();
|
|
204
204
|
`
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression Classification
|
|
3
|
+
*
|
|
4
|
+
* Analyzes expression code to determine output type for structural lowering.
|
|
5
|
+
*
|
|
6
|
+
* JSX expressions are allowed if — and only if — the compiler can statically
|
|
7
|
+
* enumerate all possible DOM shapes and lower them at compile time.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Expression output types
|
|
12
|
+
*
|
|
13
|
+
* - primitive: string, number, boolean → text binding
|
|
14
|
+
* - conditional: cond ? <A /> : <B /> → ConditionalFragmentNode
|
|
15
|
+
* - optional: cond && <A /> → OptionalFragmentNode
|
|
16
|
+
* - loop: arr.map(i => <JSX />) → LoopFragmentNode
|
|
17
|
+
* - fragment: <A /> or <><A /><B /></> → inline fragment
|
|
18
|
+
* - unknown: cannot be statically determined → COMPILE ERROR
|
|
19
|
+
*/
|
|
20
|
+
export type ExpressionOutputType =
|
|
21
|
+
| 'primitive'
|
|
22
|
+
| 'conditional'
|
|
23
|
+
| 'optional'
|
|
24
|
+
| 'loop'
|
|
25
|
+
| 'fragment'
|
|
26
|
+
| 'unknown'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Classification result with extracted metadata
|
|
30
|
+
*/
|
|
31
|
+
export interface ExpressionClassification {
|
|
32
|
+
type: ExpressionOutputType
|
|
33
|
+
// For conditional expressions
|
|
34
|
+
condition?: string
|
|
35
|
+
consequent?: string
|
|
36
|
+
alternate?: string
|
|
37
|
+
// For optional expressions
|
|
38
|
+
optionalCondition?: string
|
|
39
|
+
optionalFragment?: string
|
|
40
|
+
// For loop expressions
|
|
41
|
+
loopSource?: string
|
|
42
|
+
loopItemVar?: string
|
|
43
|
+
loopIndexVar?: string
|
|
44
|
+
loopBody?: string
|
|
45
|
+
// For inline fragments
|
|
46
|
+
fragmentCode?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if code contains JSX-like tags
|
|
51
|
+
*/
|
|
52
|
+
function containsJSX(code: string): boolean {
|
|
53
|
+
// Match opening JSX tags: <Tag or <tag
|
|
54
|
+
return /<[a-zA-Z]/.test(code)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if expression starts with a JSX element
|
|
59
|
+
*/
|
|
60
|
+
function startsWithJSX(code: string): boolean {
|
|
61
|
+
const trimmed = code.trim()
|
|
62
|
+
return /^<[a-zA-Z]/.test(trimmed) || /^<>/.test(trimmed)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Classify expression output type
|
|
67
|
+
*
|
|
68
|
+
* @param code - The expression code to classify
|
|
69
|
+
* @returns Classification result with metadata
|
|
70
|
+
*/
|
|
71
|
+
export function classifyExpression(code: string): ExpressionClassification {
|
|
72
|
+
const trimmed = code.trim()
|
|
73
|
+
|
|
74
|
+
// Check for .map() expressions with JSX body
|
|
75
|
+
const mapMatch = parseMapExpression(trimmed)
|
|
76
|
+
if (mapMatch) {
|
|
77
|
+
return {
|
|
78
|
+
type: 'loop',
|
|
79
|
+
loopSource: mapMatch.source,
|
|
80
|
+
loopItemVar: mapMatch.itemVar,
|
|
81
|
+
loopIndexVar: mapMatch.indexVar,
|
|
82
|
+
loopBody: mapMatch.body
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for ternary with JSX branches: condition ? <A /> : <B />
|
|
87
|
+
const ternaryMatch = parseTernaryExpression(trimmed)
|
|
88
|
+
if (ternaryMatch && (containsJSX(ternaryMatch.consequent) || containsJSX(ternaryMatch.alternate))) {
|
|
89
|
+
return {
|
|
90
|
+
type: 'conditional',
|
|
91
|
+
condition: ternaryMatch.condition,
|
|
92
|
+
consequent: ternaryMatch.consequent,
|
|
93
|
+
alternate: ternaryMatch.alternate
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check for logical AND with JSX: condition && <A />
|
|
98
|
+
const logicalAndMatch = parseLogicalAndExpression(trimmed)
|
|
99
|
+
if (logicalAndMatch && containsJSX(logicalAndMatch.fragment)) {
|
|
100
|
+
return {
|
|
101
|
+
type: 'optional',
|
|
102
|
+
optionalCondition: logicalAndMatch.condition,
|
|
103
|
+
optionalFragment: logicalAndMatch.fragment
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// All other expressions (including inline JSX like {<span>text</span>})
|
|
108
|
+
// are treated as primitive and handled by the existing expression transformer
|
|
109
|
+
// which converts JSX to __zenith.h() calls at runtime
|
|
110
|
+
return { type: 'primitive' }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse .map() expression
|
|
115
|
+
*
|
|
116
|
+
* Matches:
|
|
117
|
+
* - source.map(item => body)
|
|
118
|
+
* - source.map((item, index) => body)
|
|
119
|
+
*/
|
|
120
|
+
function parseMapExpression(code: string): {
|
|
121
|
+
source: string
|
|
122
|
+
itemVar: string
|
|
123
|
+
indexVar?: string
|
|
124
|
+
body: string
|
|
125
|
+
} | null {
|
|
126
|
+
// Pattern: source.map(item => body)
|
|
127
|
+
// Pattern: source.map((item) => body)
|
|
128
|
+
// Pattern: source.map((item, index) => body)
|
|
129
|
+
|
|
130
|
+
// Find .map(
|
|
131
|
+
const mapIndex = code.indexOf('.map(')
|
|
132
|
+
if (mapIndex === -1) return null
|
|
133
|
+
|
|
134
|
+
const source = code.slice(0, mapIndex).trim()
|
|
135
|
+
if (!source) return null
|
|
136
|
+
|
|
137
|
+
// Find the arrow function parameters
|
|
138
|
+
let afterMap = code.slice(mapIndex + 5) // after ".map("
|
|
139
|
+
|
|
140
|
+
// Skip whitespace
|
|
141
|
+
afterMap = afterMap.trimStart()
|
|
142
|
+
|
|
143
|
+
// Check for parenthesized params: (item) or (item, index)
|
|
144
|
+
let itemVar: string
|
|
145
|
+
let indexVar: string | undefined
|
|
146
|
+
let bodyStart: number
|
|
147
|
+
|
|
148
|
+
if (afterMap.startsWith('(')) {
|
|
149
|
+
// Find closing paren
|
|
150
|
+
const closeParenIndex = findBalancedParen(afterMap, 0)
|
|
151
|
+
if (closeParenIndex === -1) return null
|
|
152
|
+
|
|
153
|
+
const paramsStr = afterMap.slice(1, closeParenIndex)
|
|
154
|
+
const params = paramsStr.split(',').map(p => p.trim())
|
|
155
|
+
|
|
156
|
+
itemVar = params[0] || ''
|
|
157
|
+
indexVar = params[1]
|
|
158
|
+
|
|
159
|
+
// Find arrow
|
|
160
|
+
const afterParams = afterMap.slice(closeParenIndex + 1).trimStart()
|
|
161
|
+
if (!afterParams.startsWith('=>')) return null
|
|
162
|
+
|
|
163
|
+
bodyStart = mapIndex + 5 + (afterMap.length - afterParams.length) + 2
|
|
164
|
+
} else {
|
|
165
|
+
// Simple param: item => body
|
|
166
|
+
const arrowIndex = afterMap.indexOf('=>')
|
|
167
|
+
if (arrowIndex === -1) return null
|
|
168
|
+
|
|
169
|
+
itemVar = afterMap.slice(0, arrowIndex).trim()
|
|
170
|
+
bodyStart = mapIndex + 5 + arrowIndex + 2
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!itemVar) return null
|
|
174
|
+
|
|
175
|
+
// Extract body (everything after => until the closing paren of .map())
|
|
176
|
+
let body = code.slice(bodyStart).trim()
|
|
177
|
+
|
|
178
|
+
// Remove trailing ) from .map()
|
|
179
|
+
if (body.endsWith(')')) {
|
|
180
|
+
body = body.slice(0, -1).trim()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check if body contains JSX
|
|
184
|
+
if (!containsJSX(body)) return null
|
|
185
|
+
|
|
186
|
+
return { source, itemVar, indexVar, body }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Find matching closing parenthesis
|
|
191
|
+
*/
|
|
192
|
+
function findBalancedParen(code: string, startIndex: number): number {
|
|
193
|
+
if (code[startIndex] !== '(') return -1
|
|
194
|
+
|
|
195
|
+
let depth = 1
|
|
196
|
+
let i = startIndex + 1
|
|
197
|
+
|
|
198
|
+
while (i < code.length && depth > 0) {
|
|
199
|
+
if (code[i] === '(') depth++
|
|
200
|
+
else if (code[i] === ')') depth--
|
|
201
|
+
i++
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return depth === 0 ? i - 1 : -1
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parse ternary expression
|
|
209
|
+
*
|
|
210
|
+
* Matches: condition ? consequent : alternate
|
|
211
|
+
*/
|
|
212
|
+
function parseTernaryExpression(code: string): {
|
|
213
|
+
condition: string
|
|
214
|
+
consequent: string
|
|
215
|
+
alternate: string
|
|
216
|
+
} | null {
|
|
217
|
+
// Find the ? that's not inside JSX or strings
|
|
218
|
+
const questionIndex = findTernaryOperator(code)
|
|
219
|
+
if (questionIndex === -1) return null
|
|
220
|
+
|
|
221
|
+
const condition = code.slice(0, questionIndex).trim()
|
|
222
|
+
const afterQuestion = code.slice(questionIndex + 1)
|
|
223
|
+
|
|
224
|
+
// Find the : that matches this ternary
|
|
225
|
+
const colonIndex = findTernaryColon(afterQuestion)
|
|
226
|
+
if (colonIndex === -1) return null
|
|
227
|
+
|
|
228
|
+
const consequent = afterQuestion.slice(0, colonIndex).trim()
|
|
229
|
+
const alternate = afterQuestion.slice(colonIndex + 1).trim()
|
|
230
|
+
|
|
231
|
+
if (!condition || !consequent || !alternate) return null
|
|
232
|
+
|
|
233
|
+
return { condition, consequent, alternate }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Find ternary ? operator (not inside JSX or nested ternaries)
|
|
238
|
+
*/
|
|
239
|
+
function findTernaryOperator(code: string): number {
|
|
240
|
+
let depth = 0
|
|
241
|
+
let inString = false
|
|
242
|
+
let stringChar = ''
|
|
243
|
+
let inTemplate = false
|
|
244
|
+
let jsxDepth = 0
|
|
245
|
+
|
|
246
|
+
for (let i = 0; i < code.length; i++) {
|
|
247
|
+
const char = code[i]
|
|
248
|
+
const prevChar = i > 0 ? code[i - 1] : ''
|
|
249
|
+
|
|
250
|
+
// Handle escape
|
|
251
|
+
if (prevChar === '\\') continue
|
|
252
|
+
|
|
253
|
+
// Handle strings
|
|
254
|
+
if (!inString && !inTemplate && (char === '"' || char === "'")) {
|
|
255
|
+
inString = true
|
|
256
|
+
stringChar = char
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
if (inString && char === stringChar) {
|
|
260
|
+
inString = false
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Handle template literals
|
|
265
|
+
if (!inString && !inTemplate && char === '`') {
|
|
266
|
+
inTemplate = true
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
if (inTemplate && char === '`') {
|
|
270
|
+
inTemplate = false
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (inString || inTemplate) continue
|
|
275
|
+
|
|
276
|
+
// Track JSX depth
|
|
277
|
+
if (char === '<' && /[a-zA-Z>]/.test(code[i + 1] || '')) {
|
|
278
|
+
jsxDepth++
|
|
279
|
+
}
|
|
280
|
+
if (char === '>' && prevChar === '/') {
|
|
281
|
+
jsxDepth = Math.max(0, jsxDepth - 1)
|
|
282
|
+
}
|
|
283
|
+
if (char === '/' && code[i + 1] === '>') {
|
|
284
|
+
// self-closing tag, depth handled when we see >
|
|
285
|
+
}
|
|
286
|
+
if (char === '<' && code[i + 1] === '/') {
|
|
287
|
+
// closing tag coming
|
|
288
|
+
}
|
|
289
|
+
if (char === '>' && jsxDepth > 0 && prevChar !== '/' && code.slice(0, i).includes('</')) {
|
|
290
|
+
jsxDepth = Math.max(0, jsxDepth - 1)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Track parens
|
|
294
|
+
if (char === '(' || char === '{' || char === '[') depth++
|
|
295
|
+
if (char === ')' || char === '}' || char === ']') depth--
|
|
296
|
+
|
|
297
|
+
// Found ternary operator at top level
|
|
298
|
+
if (char === '?' && depth === 0 && jsxDepth === 0) {
|
|
299
|
+
return i
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return -1
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Find ternary : operator (matching the ?)
|
|
308
|
+
*/
|
|
309
|
+
function findTernaryColon(code: string): number {
|
|
310
|
+
let depth = 0
|
|
311
|
+
let ternaryDepth = 0
|
|
312
|
+
let inString = false
|
|
313
|
+
let stringChar = ''
|
|
314
|
+
let inTemplate = false
|
|
315
|
+
let jsxDepth = 0
|
|
316
|
+
|
|
317
|
+
for (let i = 0; i < code.length; i++) {
|
|
318
|
+
const char = code[i]
|
|
319
|
+
const prevChar = i > 0 ? code[i - 1] : ''
|
|
320
|
+
|
|
321
|
+
// Handle escape
|
|
322
|
+
if (prevChar === '\\') continue
|
|
323
|
+
|
|
324
|
+
// Handle strings
|
|
325
|
+
if (!inString && !inTemplate && (char === '"' || char === "'")) {
|
|
326
|
+
inString = true
|
|
327
|
+
stringChar = char
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
330
|
+
if (inString && char === stringChar) {
|
|
331
|
+
inString = false
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Handle template literals
|
|
336
|
+
if (!inString && !inTemplate && char === '`') {
|
|
337
|
+
inTemplate = true
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
if (inTemplate && char === '`') {
|
|
341
|
+
inTemplate = false
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (inString || inTemplate) continue
|
|
346
|
+
|
|
347
|
+
// Track JSX depth (simplified)
|
|
348
|
+
if (char === '<' && /[a-zA-Z>]/.test(code[i + 1] || '')) {
|
|
349
|
+
jsxDepth++
|
|
350
|
+
}
|
|
351
|
+
if (char === '>' && (prevChar === '/' || jsxDepth > 0)) {
|
|
352
|
+
jsxDepth = Math.max(0, jsxDepth - 1)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Track parens
|
|
356
|
+
if (char === '(' || char === '{' || char === '[') depth++
|
|
357
|
+
if (char === ')' || char === '}' || char === ']') depth--
|
|
358
|
+
|
|
359
|
+
// Track nested ternaries
|
|
360
|
+
if (char === '?') ternaryDepth++
|
|
361
|
+
if (char === ':' && ternaryDepth > 0) {
|
|
362
|
+
ternaryDepth--
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Found matching colon at top level
|
|
367
|
+
if (char === ':' && depth === 0 && ternaryDepth === 0 && jsxDepth === 0) {
|
|
368
|
+
return i
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return -1
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Parse logical AND expression
|
|
377
|
+
*
|
|
378
|
+
* Matches: condition && fragment
|
|
379
|
+
*/
|
|
380
|
+
function parseLogicalAndExpression(code: string): {
|
|
381
|
+
condition: string
|
|
382
|
+
fragment: string
|
|
383
|
+
} | null {
|
|
384
|
+
// Find && at top level
|
|
385
|
+
let depth = 0
|
|
386
|
+
let inString = false
|
|
387
|
+
let stringChar = ''
|
|
388
|
+
let inTemplate = false
|
|
389
|
+
|
|
390
|
+
for (let i = 0; i < code.length - 1; i++) {
|
|
391
|
+
const char = code[i]
|
|
392
|
+
const nextChar = code[i + 1]
|
|
393
|
+
const prevChar = i > 0 ? code[i - 1] : ''
|
|
394
|
+
|
|
395
|
+
// Handle escape
|
|
396
|
+
if (prevChar === '\\') continue
|
|
397
|
+
|
|
398
|
+
// Handle strings
|
|
399
|
+
if (!inString && !inTemplate && (char === '"' || char === "'")) {
|
|
400
|
+
inString = true
|
|
401
|
+
stringChar = char
|
|
402
|
+
continue
|
|
403
|
+
}
|
|
404
|
+
if (inString && char === stringChar) {
|
|
405
|
+
inString = false
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Handle template literals
|
|
410
|
+
if (!inString && !inTemplate && char === '`') {
|
|
411
|
+
inTemplate = true
|
|
412
|
+
continue
|
|
413
|
+
}
|
|
414
|
+
if (inTemplate && char === '`') {
|
|
415
|
+
inTemplate = false
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (inString || inTemplate) continue
|
|
420
|
+
|
|
421
|
+
// Track parens
|
|
422
|
+
if (char === '(' || char === '{' || char === '[') depth++
|
|
423
|
+
if (char === ')' || char === '}' || char === ']') depth--
|
|
424
|
+
|
|
425
|
+
// Found && at top level
|
|
426
|
+
if (char === '&' && nextChar === '&' && depth === 0) {
|
|
427
|
+
const condition = code.slice(0, i).trim()
|
|
428
|
+
const fragment = code.slice(i + 2).trim()
|
|
429
|
+
|
|
430
|
+
if (condition && fragment) {
|
|
431
|
+
return { condition, fragment }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return null
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Check if an expression type requires structural lowering
|
|
441
|
+
*/
|
|
442
|
+
export function requiresStructuralLowering(type: ExpressionOutputType): boolean {
|
|
443
|
+
return type === 'conditional' || type === 'optional' || type === 'loop' || type === 'fragment'
|
|
444
|
+
}
|