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