@zenithbuild/compiler 1.0.2
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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/build-analyzer.d.ts +44 -0
- package/dist/build-analyzer.js +87 -0
- package/dist/bundler.d.ts +31 -0
- package/dist/bundler.js +86 -0
- package/dist/core/components/index.d.ts +9 -0
- package/dist/core/components/index.js +13 -0
- package/dist/core/config/index.d.ts +11 -0
- package/dist/core/config/index.js +10 -0
- package/dist/core/config/loader.d.ts +17 -0
- package/dist/core/config/loader.js +60 -0
- package/dist/core/config/types.d.ts +98 -0
- package/dist/core/config/types.js +32 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +6 -0
- package/dist/core/lifecycle/index.d.ts +16 -0
- package/dist/core/lifecycle/index.js +19 -0
- package/dist/core/lifecycle/zen-mount.d.ts +66 -0
- package/dist/core/lifecycle/zen-mount.js +151 -0
- package/dist/core/lifecycle/zen-unmount.d.ts +54 -0
- package/dist/core/lifecycle/zen-unmount.js +76 -0
- package/dist/core/plugins/bridge.d.ts +116 -0
- package/dist/core/plugins/bridge.js +121 -0
- package/dist/core/plugins/index.d.ts +6 -0
- package/dist/core/plugins/index.js +6 -0
- package/dist/core/plugins/registry.d.ts +67 -0
- package/dist/core/plugins/registry.js +113 -0
- package/dist/core/reactivity/index.d.ts +30 -0
- package/dist/core/reactivity/index.js +33 -0
- package/dist/core/reactivity/tracking.d.ts +74 -0
- package/dist/core/reactivity/tracking.js +136 -0
- package/dist/core/reactivity/zen-batch.d.ts +45 -0
- package/dist/core/reactivity/zen-batch.js +54 -0
- package/dist/core/reactivity/zen-effect.d.ts +48 -0
- package/dist/core/reactivity/zen-effect.js +98 -0
- package/dist/core/reactivity/zen-memo.d.ts +43 -0
- package/dist/core/reactivity/zen-memo.js +100 -0
- package/dist/core/reactivity/zen-ref.d.ts +44 -0
- package/dist/core/reactivity/zen-ref.js +34 -0
- package/dist/core/reactivity/zen-signal.d.ts +48 -0
- package/dist/core/reactivity/zen-signal.js +84 -0
- package/dist/core/reactivity/zen-state.d.ts +35 -0
- package/dist/core/reactivity/zen-state.js +147 -0
- package/dist/core/reactivity/zen-untrack.d.ts +38 -0
- package/dist/core/reactivity/zen-untrack.js +41 -0
- package/dist/css/index.d.ts +73 -0
- package/dist/css/index.js +246 -0
- package/dist/discovery/componentDiscovery.d.ts +42 -0
- package/dist/discovery/componentDiscovery.js +56 -0
- package/dist/discovery/layouts.d.ts +13 -0
- package/dist/discovery/layouts.js +41 -0
- package/dist/errors/compilerError.d.ts +31 -0
- package/dist/errors/compilerError.js +51 -0
- package/dist/finalize/finalizeOutput.d.ts +32 -0
- package/dist/finalize/finalizeOutput.js +62 -0
- package/dist/finalize/generateFinalBundle.d.ts +24 -0
- package/dist/finalize/generateFinalBundle.js +68 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +51 -0
- package/dist/ir/types.d.ts +181 -0
- package/dist/ir/types.js +8 -0
- package/dist/output/types.d.ts +30 -0
- package/dist/output/types.js +6 -0
- package/dist/parse/detectMapExpressions.d.ts +45 -0
- package/dist/parse/detectMapExpressions.js +77 -0
- package/dist/parse/parseScript.d.ts +8 -0
- package/dist/parse/parseScript.js +36 -0
- package/dist/parse/parseTemplate.d.ts +11 -0
- package/dist/parse/parseTemplate.js +487 -0
- package/dist/parse/parseZenFile.d.ts +11 -0
- package/dist/parse/parseZenFile.js +50 -0
- package/dist/parse/scriptAnalysis.d.ts +25 -0
- package/dist/parse/scriptAnalysis.js +60 -0
- package/dist/parse/trackLoopContext.d.ts +20 -0
- package/dist/parse/trackLoopContext.js +62 -0
- package/dist/parseZenFile.d.ts +10 -0
- package/dist/parseZenFile.js +55 -0
- package/dist/runtime/analyzeAndEmit.d.ts +20 -0
- package/dist/runtime/analyzeAndEmit.js +70 -0
- package/dist/runtime/build.d.ts +6 -0
- package/dist/runtime/build.js +13 -0
- package/dist/runtime/bundle-generator.d.ts +27 -0
- package/dist/runtime/bundle-generator.js +1263 -0
- package/dist/runtime/client-runtime.d.ts +41 -0
- package/dist/runtime/client-runtime.js +397 -0
- package/dist/runtime/dataExposure.d.ts +52 -0
- package/dist/runtime/dataExposure.js +227 -0
- package/dist/runtime/generateDOM.d.ts +21 -0
- package/dist/runtime/generateDOM.js +194 -0
- package/dist/runtime/generateHydrationBundle.d.ts +15 -0
- package/dist/runtime/generateHydrationBundle.js +399 -0
- package/dist/runtime/hydration.d.ts +53 -0
- package/dist/runtime/hydration.js +271 -0
- package/dist/runtime/navigation.d.ts +58 -0
- package/dist/runtime/navigation.js +372 -0
- package/dist/runtime/serve.d.ts +13 -0
- package/dist/runtime/serve.js +76 -0
- package/dist/runtime/thinRuntime.d.ts +23 -0
- package/dist/runtime/thinRuntime.js +158 -0
- package/dist/runtime/transformIR.d.ts +19 -0
- package/dist/runtime/transformIR.js +285 -0
- package/dist/runtime/wrapExpression.d.ts +24 -0
- package/dist/runtime/wrapExpression.js +76 -0
- package/dist/runtime/wrapExpressionWithLoop.d.ts +17 -0
- package/dist/runtime/wrapExpressionWithLoop.js +75 -0
- package/dist/spa-build.d.ts +26 -0
- package/dist/spa-build.js +866 -0
- package/dist/ssg-build.d.ts +32 -0
- package/dist/ssg-build.js +408 -0
- package/dist/test/analyze-emit.test.d.ts +1 -0
- package/dist/test/analyze-emit.test.js +88 -0
- package/dist/test/bundler-contract.test.d.ts +1 -0
- package/dist/test/bundler-contract.test.js +137 -0
- package/dist/test/compiler-authority.test.d.ts +1 -0
- package/dist/test/compiler-authority.test.js +90 -0
- package/dist/test/component-instance-test.d.ts +1 -0
- package/dist/test/component-instance-test.js +115 -0
- package/dist/test/error-native-bridge.test.d.ts +1 -0
- package/dist/test/error-native-bridge.test.js +51 -0
- package/dist/test/error-serialization.test.d.ts +1 -0
- package/dist/test/error-serialization.test.js +38 -0
- package/dist/test/macro-inlining.test.d.ts +1 -0
- package/dist/test/macro-inlining.test.js +178 -0
- package/dist/test/validate-test.d.ts +6 -0
- package/dist/test/validate-test.js +95 -0
- package/dist/transform/classifyExpression.d.ts +46 -0
- package/dist/transform/classifyExpression.js +354 -0
- package/dist/transform/componentResolver.d.ts +15 -0
- package/dist/transform/componentResolver.js +30 -0
- package/dist/transform/expressionTransformer.d.ts +19 -0
- package/dist/transform/expressionTransformer.js +333 -0
- package/dist/transform/fragmentLowering.d.ts +25 -0
- package/dist/transform/fragmentLowering.js +468 -0
- package/dist/transform/layoutProcessor.d.ts +5 -0
- package/dist/transform/layoutProcessor.js +34 -0
- package/dist/transform/transformTemplate.d.ts +11 -0
- package/dist/transform/transformTemplate.js +33 -0
- package/dist/validate/invariants.d.ts +23 -0
- package/dist/validate/invariants.js +55 -0
- package/native/compiler-native/compiler-native.node +0 -0
- package/native/compiler-native/index.d.ts +113 -0
- package/native/compiler-native/index.js +19 -0
- package/native/compiler-native/package.json +19 -0
- package/package.json +49 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses HTML template and extracts expressions
|
|
5
|
+
* Phase 1: Only extracts, does not execute
|
|
6
|
+
*/
|
|
7
|
+
import { parseFragment } from 'parse5';
|
|
8
|
+
import { CompilerError, InvariantError } from '../errors/compilerError';
|
|
9
|
+
import { shouldAttachLoopContext, mergeLoopContext, extractLoopContextFromExpression } from './trackLoopContext';
|
|
10
|
+
import { INVARIANT } from '../validate/invariants';
|
|
11
|
+
import { lowerFragments } from '../transform/fragmentLowering';
|
|
12
|
+
// Generate stable IDs for expressions
|
|
13
|
+
let expressionIdCounter = 0;
|
|
14
|
+
function generateExpressionId() {
|
|
15
|
+
return `expr_${expressionIdCounter++}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Strip script and style blocks from HTML before parsing
|
|
19
|
+
*/
|
|
20
|
+
function stripBlocks(html) {
|
|
21
|
+
// Remove script blocks
|
|
22
|
+
let stripped = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
23
|
+
// Remove style blocks
|
|
24
|
+
stripped = stripped.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
25
|
+
return stripped;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Find the end of a balanced brace expression, handling strings and template literals
|
|
29
|
+
* Returns the index after the closing brace, or -1 if unbalanced
|
|
30
|
+
*/
|
|
31
|
+
function findBalancedBraceEnd(html, startIndex) {
|
|
32
|
+
let braceCount = 1;
|
|
33
|
+
let i = startIndex + 1;
|
|
34
|
+
let inString = false;
|
|
35
|
+
let stringChar = '';
|
|
36
|
+
let inTemplate = false;
|
|
37
|
+
while (i < html.length && braceCount > 0) {
|
|
38
|
+
const char = html[i];
|
|
39
|
+
const prevChar = i > 0 ? html[i - 1] : '';
|
|
40
|
+
// Handle escape sequences
|
|
41
|
+
if (prevChar === '\\') {
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Handle string literals (not inside template)
|
|
46
|
+
if (!inString && !inTemplate && (char === '"' || char === "'")) {
|
|
47
|
+
inString = true;
|
|
48
|
+
stringChar = char;
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (inString && char === stringChar) {
|
|
53
|
+
inString = false;
|
|
54
|
+
stringChar = '';
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// Handle template literals
|
|
59
|
+
if (!inString && !inTemplate && char === '`') {
|
|
60
|
+
inTemplate = true;
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (inTemplate && char === '`') {
|
|
65
|
+
inTemplate = false;
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Handle ${} inside template literals - need to track nested braces
|
|
70
|
+
if (inTemplate && char === '$' && html[i + 1] === '{') {
|
|
71
|
+
// Skip the ${ and count as opening brace
|
|
72
|
+
i += 2;
|
|
73
|
+
let templateBraceCount = 1;
|
|
74
|
+
while (i < html.length && templateBraceCount > 0) {
|
|
75
|
+
if (html[i] === '{')
|
|
76
|
+
templateBraceCount++;
|
|
77
|
+
else if (html[i] === '}')
|
|
78
|
+
templateBraceCount--;
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Count braces only when not in strings or templates
|
|
84
|
+
if (!inString && !inTemplate) {
|
|
85
|
+
if (char === '{')
|
|
86
|
+
braceCount++;
|
|
87
|
+
else if (char === '}')
|
|
88
|
+
braceCount--;
|
|
89
|
+
}
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
return braceCount === 0 ? i : -1;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Normalize expressions before parsing
|
|
96
|
+
* Replaces both attr={expr} and {textExpr} with placeholders so parse5 can parse the HTML correctly
|
|
97
|
+
* without being confused by tags or braces inside expressions.
|
|
98
|
+
*
|
|
99
|
+
* Uses balanced brace parsing to correctly handle:
|
|
100
|
+
* - String literals with braces inside
|
|
101
|
+
* - Template literals with ${} interpolations
|
|
102
|
+
* - Arrow functions with object returns
|
|
103
|
+
* - Multi-line JSX expressions
|
|
104
|
+
*/
|
|
105
|
+
function normalizeAllExpressions(html) {
|
|
106
|
+
const exprMap = new Map();
|
|
107
|
+
let exprCounter = 0;
|
|
108
|
+
let result = '';
|
|
109
|
+
let lastPos = 0;
|
|
110
|
+
for (let i = 0; i < html.length; i++) {
|
|
111
|
+
// Look for { and check if it's an expression
|
|
112
|
+
// We handle both text expressions and attribute expressions: attr={...}
|
|
113
|
+
if (html[i] === '{') {
|
|
114
|
+
const j = findBalancedBraceEnd(html, i);
|
|
115
|
+
if (j !== -1 && j > i + 1) {
|
|
116
|
+
const expr = html.substring(i + 1, j - 1).trim();
|
|
117
|
+
// Skip empty expressions
|
|
118
|
+
if (expr.length === 0) {
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const placeholder = `__ZEN_EXPR_${exprCounter++}`;
|
|
123
|
+
exprMap.set(placeholder, expr);
|
|
124
|
+
result += html.substring(lastPos, i);
|
|
125
|
+
result += placeholder;
|
|
126
|
+
lastPos = j;
|
|
127
|
+
i = j - 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
result += html.substring(lastPos);
|
|
132
|
+
return { normalized: result, expressions: exprMap };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Calculate source location from parse5 node
|
|
136
|
+
*/
|
|
137
|
+
function getLocation(node, originalHtml) {
|
|
138
|
+
// parse5 provides sourceCodeLocation if available
|
|
139
|
+
if (node.sourceCodeLocation) {
|
|
140
|
+
return {
|
|
141
|
+
line: node.sourceCodeLocation.startLine || 1,
|
|
142
|
+
column: node.sourceCodeLocation.startCol || 1
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Fallback if location info not available
|
|
146
|
+
return { line: 1, column: 1 };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Extract expressions from text content
|
|
150
|
+
* Returns array of { expression, location } and the text with expressions replaced
|
|
151
|
+
* Phase 7: Supports loop context for expressions inside map iterations
|
|
152
|
+
*/
|
|
153
|
+
function extractExpressionsFromText(text, baseLocation, expressions, normalizedExprs, loopContext) {
|
|
154
|
+
const nodes = [];
|
|
155
|
+
let processedText = '';
|
|
156
|
+
let currentIndex = 0;
|
|
157
|
+
// Match __ZEN_EXPR_N placeholders
|
|
158
|
+
const expressionRegex = /__ZEN_EXPR_\d+/g;
|
|
159
|
+
let match;
|
|
160
|
+
while ((match = expressionRegex.exec(text)) !== null) {
|
|
161
|
+
const beforeExpr = text.substring(currentIndex, match.index);
|
|
162
|
+
if (beforeExpr) {
|
|
163
|
+
nodes.push({
|
|
164
|
+
type: 'text',
|
|
165
|
+
value: beforeExpr,
|
|
166
|
+
location: {
|
|
167
|
+
line: baseLocation.line,
|
|
168
|
+
column: baseLocation.column + currentIndex
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
processedText += beforeExpr;
|
|
172
|
+
}
|
|
173
|
+
// Resolve placeholder to original expression code
|
|
174
|
+
const placeholder = match[0];
|
|
175
|
+
const exprCode = (normalizedExprs.get(placeholder) || '').trim();
|
|
176
|
+
const exprId = generateExpressionId();
|
|
177
|
+
const exprLocation = {
|
|
178
|
+
line: baseLocation.line,
|
|
179
|
+
column: baseLocation.column + match.index
|
|
180
|
+
};
|
|
181
|
+
const exprIR = {
|
|
182
|
+
id: exprId,
|
|
183
|
+
code: exprCode,
|
|
184
|
+
location: exprLocation
|
|
185
|
+
};
|
|
186
|
+
expressions.push(exprIR);
|
|
187
|
+
// Phase 7: Loop context detection and attachment
|
|
188
|
+
const mapLoopContext = extractLoopContextFromExpression(exprIR);
|
|
189
|
+
const activeLoopContext = mergeLoopContext(loopContext, mapLoopContext);
|
|
190
|
+
const attachedLoopContext = shouldAttachLoopContext(exprIR, activeLoopContext);
|
|
191
|
+
nodes.push({
|
|
192
|
+
type: 'expression',
|
|
193
|
+
expression: exprId,
|
|
194
|
+
location: exprLocation,
|
|
195
|
+
loopContext: attachedLoopContext
|
|
196
|
+
});
|
|
197
|
+
processedText += `{${exprCode}}`;
|
|
198
|
+
currentIndex = match.index + match[0].length;
|
|
199
|
+
}
|
|
200
|
+
// Add remaining text
|
|
201
|
+
const remaining = text.substring(currentIndex);
|
|
202
|
+
if (remaining) {
|
|
203
|
+
nodes.push({
|
|
204
|
+
type: 'text',
|
|
205
|
+
value: remaining,
|
|
206
|
+
location: {
|
|
207
|
+
line: baseLocation.line,
|
|
208
|
+
column: baseLocation.column + currentIndex
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
processedText += remaining;
|
|
212
|
+
}
|
|
213
|
+
// If no expressions found, return single text node
|
|
214
|
+
if (nodes.length === 0) {
|
|
215
|
+
nodes.push({
|
|
216
|
+
type: 'text',
|
|
217
|
+
value: text,
|
|
218
|
+
location: baseLocation
|
|
219
|
+
});
|
|
220
|
+
processedText = text;
|
|
221
|
+
}
|
|
222
|
+
return { processedText, nodes };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Parse attribute value - may contain expressions
|
|
226
|
+
* Phase 7: Supports loop context for expressions inside map iterations
|
|
227
|
+
*/
|
|
228
|
+
function parseAttributeValue(value, baseLocation, expressions, normalizedExprs, loopContext // Phase 7: Loop context from parent map expressions
|
|
229
|
+
) {
|
|
230
|
+
// Check if this is a normalized expression placeholder
|
|
231
|
+
if (value.startsWith('__ZEN_EXPR_')) {
|
|
232
|
+
const exprCode = normalizedExprs.get(value);
|
|
233
|
+
if (!exprCode) {
|
|
234
|
+
throw new Error(`Normalized expression placeholder not found: ${value}`);
|
|
235
|
+
}
|
|
236
|
+
const exprId = generateExpressionId();
|
|
237
|
+
expressions.push({
|
|
238
|
+
id: exprId,
|
|
239
|
+
code: exprCode,
|
|
240
|
+
location: baseLocation
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
id: exprId,
|
|
244
|
+
code: exprCode,
|
|
245
|
+
location: baseLocation
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// Check if attribute value is an expression { ... } (shouldn't happen after normalization)
|
|
249
|
+
const exprMatch = value.match(/^\{([^}]+)\}$/);
|
|
250
|
+
if (exprMatch && exprMatch[1]) {
|
|
251
|
+
const exprCode = exprMatch[1].trim();
|
|
252
|
+
const exprId = generateExpressionId();
|
|
253
|
+
expressions.push({
|
|
254
|
+
id: exprId,
|
|
255
|
+
code: exprCode,
|
|
256
|
+
location: baseLocation
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
id: exprId,
|
|
260
|
+
code: exprCode,
|
|
261
|
+
location: baseLocation
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// Regular string value
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Convert parse5 node to TemplateNode
|
|
269
|
+
* Phase 7: Supports loop context propagation for map expressions
|
|
270
|
+
*/
|
|
271
|
+
function parseNode(node, originalHtml, expressions, normalizedExprs, parentLoopContext // Phase 7: Loop context from parent map expressions
|
|
272
|
+
) {
|
|
273
|
+
if (node.nodeName === '#text') {
|
|
274
|
+
const text = node.value || '';
|
|
275
|
+
const location = getLocation(node, originalHtml);
|
|
276
|
+
// Extract expressions from text
|
|
277
|
+
// Phase 7: Pass loop context to detect map expressions and attach context
|
|
278
|
+
const { nodes } = extractExpressionsFromText(node.value, location, expressions, normalizedExprs, parentLoopContext);
|
|
279
|
+
// If single text node with no expressions, return it
|
|
280
|
+
if (nodes.length === 1 && nodes[0] && nodes[0].type === 'text') {
|
|
281
|
+
return nodes[0];
|
|
282
|
+
}
|
|
283
|
+
// Otherwise, we need to handle multiple nodes
|
|
284
|
+
// For Phase 1, we'll flatten to text for now (will be handled in future phases)
|
|
285
|
+
// This is a limitation we accept for Phase 1
|
|
286
|
+
const firstNode = nodes[0];
|
|
287
|
+
if (firstNode) {
|
|
288
|
+
return firstNode;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
type: 'text',
|
|
292
|
+
value: text,
|
|
293
|
+
location
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (node.nodeName === '#comment') {
|
|
297
|
+
// Skip comments for Phase 1
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
if (node.nodeName && node.nodeName !== '#text' && node.nodeName !== '#comment') {
|
|
301
|
+
const location = getLocation(node, originalHtml);
|
|
302
|
+
const tag = node.tagName?.toLowerCase() || node.nodeName;
|
|
303
|
+
// Extract original tag name from source HTML to preserve casing (parse5 lowercases everything)
|
|
304
|
+
let originalTag = node.tagName || node.nodeName;
|
|
305
|
+
if (node.sourceCodeLocation && node.sourceCodeLocation.startOffset !== undefined) {
|
|
306
|
+
const startOffset = node.sourceCodeLocation.startOffset;
|
|
307
|
+
// Find the tag name in original HTML (after '<')
|
|
308
|
+
const tagMatch = originalHtml.slice(startOffset).match(/^<([a-zA-Z][a-zA-Z0-9._-]*)/);
|
|
309
|
+
if (tagMatch && tagMatch[1]) {
|
|
310
|
+
originalTag = tagMatch[1];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// INV005: <template> tags are forbidden — use compound components instead
|
|
314
|
+
if (tag === 'template') {
|
|
315
|
+
throw new InvariantError(INVARIANT.TEMPLATE_TAG, `<template> tags are forbidden in Zenith. Use compound components (e.g., Card.Header) for named slots.`, 'Named slots use compound component pattern (Card.Header), not <template> tags.', 'unknown', // filePath passed to parseTemplate
|
|
316
|
+
location.line, location.column);
|
|
317
|
+
}
|
|
318
|
+
// Parse attributes
|
|
319
|
+
const attributes = [];
|
|
320
|
+
if (node.attrs) {
|
|
321
|
+
for (const attr of node.attrs) {
|
|
322
|
+
const attrLocation = node.sourceCodeLocation?.attrs?.[attr.name]
|
|
323
|
+
? {
|
|
324
|
+
line: node.sourceCodeLocation.attrs[attr.name].startLine || location.line,
|
|
325
|
+
column: node.sourceCodeLocation.attrs[attr.name].startCol || location.column
|
|
326
|
+
}
|
|
327
|
+
: location;
|
|
328
|
+
// INV006: slot="" attributes are forbidden — use compound components instead
|
|
329
|
+
if (attr.name === 'slot') {
|
|
330
|
+
throw new InvariantError(INVARIANT.SLOT_ATTRIBUTE, `slot="${attr.value || ''}" attribute is forbidden. Use compound components (e.g., Card.Header) for named slots.`, 'Named slots use compound component pattern (Card.Header), not slot="" attributes.', 'unknown', attrLocation.line, attrLocation.column);
|
|
331
|
+
}
|
|
332
|
+
// Handle :attr="expr" syntax (colon-prefixed reactive attributes)
|
|
333
|
+
let attrName = attr.name;
|
|
334
|
+
let attrValue = attr.value || '';
|
|
335
|
+
let isReactive = false;
|
|
336
|
+
if (attrName.startsWith(':')) {
|
|
337
|
+
// This is a reactive attribute like :class="expr"
|
|
338
|
+
attrName = attrName.slice(1); // Remove the colon
|
|
339
|
+
isReactive = true;
|
|
340
|
+
// The value is already a string expression (not in braces)
|
|
341
|
+
// Treat it as an expression
|
|
342
|
+
const exprId = generateExpressionId();
|
|
343
|
+
const exprCode = attrValue.trim();
|
|
344
|
+
const exprIR = {
|
|
345
|
+
id: exprId,
|
|
346
|
+
code: exprCode,
|
|
347
|
+
location: attrLocation
|
|
348
|
+
};
|
|
349
|
+
expressions.push(exprIR);
|
|
350
|
+
// Phase 7: Attach loop context if expression references loop variables
|
|
351
|
+
const attachedLoopContext = shouldAttachLoopContext(exprIR, parentLoopContext);
|
|
352
|
+
attributes.push({
|
|
353
|
+
name: attrName, // Store without colon (e.g., "class" not ":class")
|
|
354
|
+
value: exprIR,
|
|
355
|
+
location: attrLocation,
|
|
356
|
+
loopContext: attachedLoopContext
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// Regular attribute or attr={expr} syntax
|
|
361
|
+
const attrValueResult = parseAttributeValue(attrValue, attrLocation, expressions, normalizedExprs, parentLoopContext);
|
|
362
|
+
// Transform event attributes: onclick -> data-zen-click, onchange -> data-zen-change, etc.
|
|
363
|
+
let finalAttrName = attrName;
|
|
364
|
+
if (attrName.startsWith('on') && attrName.length > 2) {
|
|
365
|
+
const eventType = attrName.slice(2); // Remove "on" prefix
|
|
366
|
+
finalAttrName = `data-zen-${eventType}`;
|
|
367
|
+
}
|
|
368
|
+
if (typeof attrValueResult === 'string') {
|
|
369
|
+
// Static attribute value
|
|
370
|
+
attributes.push({
|
|
371
|
+
name: finalAttrName,
|
|
372
|
+
value: attrValueResult,
|
|
373
|
+
location: attrLocation
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// Expression attribute value
|
|
378
|
+
const exprIR = attrValueResult;
|
|
379
|
+
// Phase 7: Attach loop context if expression references loop variables
|
|
380
|
+
const attachedLoopContext = shouldAttachLoopContext(exprIR, parentLoopContext);
|
|
381
|
+
attributes.push({
|
|
382
|
+
name: finalAttrName,
|
|
383
|
+
value: exprIR,
|
|
384
|
+
location: attrLocation,
|
|
385
|
+
loopContext: attachedLoopContext
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Parse children
|
|
392
|
+
const children = [];
|
|
393
|
+
if (node.childNodes) {
|
|
394
|
+
for (const child of node.childNodes) {
|
|
395
|
+
if (child.nodeName === '#text') {
|
|
396
|
+
// Handle text nodes that may contain expressions
|
|
397
|
+
const text = child.value || '';
|
|
398
|
+
const location = getLocation(child, originalHtml);
|
|
399
|
+
const { nodes: textNodes } = extractExpressionsFromText(text, location, expressions, normalizedExprs, parentLoopContext);
|
|
400
|
+
// Add all nodes from text (can be multiple: text + expression + text)
|
|
401
|
+
for (const textNode of textNodes) {
|
|
402
|
+
children.push(textNode);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
const childNode = parseNode(child, originalHtml, expressions, normalizedExprs, parentLoopContext);
|
|
407
|
+
if (childNode) {
|
|
408
|
+
children.push(childNode);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Phase 7: Check if any child expression is a map expression and extract its loop context
|
|
414
|
+
// This allows nested loops to work correctly
|
|
415
|
+
let elementLoopContext = parentLoopContext;
|
|
416
|
+
// Check children for map expressions (they create new loop contexts)
|
|
417
|
+
for (const child of children) {
|
|
418
|
+
if (child.type === 'expression' && child.loopContext) {
|
|
419
|
+
// If we find a map expression child, merge its context
|
|
420
|
+
elementLoopContext = mergeLoopContext(elementLoopContext, child.loopContext);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Check if this is a custom component (starts with uppercase)
|
|
424
|
+
const isComponent = originalTag.length > 0 && originalTag[0] === originalTag[0].toUpperCase();
|
|
425
|
+
if (isComponent) {
|
|
426
|
+
// This is a component node
|
|
427
|
+
return {
|
|
428
|
+
type: 'component',
|
|
429
|
+
name: originalTag,
|
|
430
|
+
attributes,
|
|
431
|
+
children,
|
|
432
|
+
location,
|
|
433
|
+
loopContext: elementLoopContext
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// This is a regular HTML element
|
|
438
|
+
return {
|
|
439
|
+
type: 'element',
|
|
440
|
+
tag,
|
|
441
|
+
attributes,
|
|
442
|
+
children,
|
|
443
|
+
location,
|
|
444
|
+
loopContext: elementLoopContext
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Parse template from HTML string
|
|
452
|
+
*/
|
|
453
|
+
export function parseTemplate(html, filePath) {
|
|
454
|
+
// Strip script and style blocks
|
|
455
|
+
let templateHtml = stripBlocks(html);
|
|
456
|
+
// Normalize all expressions so parse5 can parse them safely
|
|
457
|
+
const { normalized, expressions: normalizedExprs } = normalizeAllExpressions(templateHtml);
|
|
458
|
+
templateHtml = normalized;
|
|
459
|
+
try {
|
|
460
|
+
// Parse HTML using parseFragment
|
|
461
|
+
const fragment = parseFragment(templateHtml, {
|
|
462
|
+
sourceCodeLocationInfo: true
|
|
463
|
+
});
|
|
464
|
+
const expressions = [];
|
|
465
|
+
const nodes = [];
|
|
466
|
+
// Parse fragment children
|
|
467
|
+
if (fragment.childNodes) {
|
|
468
|
+
for (const node of fragment.childNodes) {
|
|
469
|
+
const parsed = parseNode(node, templateHtml, expressions, normalizedExprs, undefined);
|
|
470
|
+
if (parsed) {
|
|
471
|
+
nodes.push(parsed);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Phase 8: Lower JSX expressions to structural fragments
|
|
476
|
+
// This transforms expressions like {cond ? <A /> : <B />} into ConditionalFragmentNode
|
|
477
|
+
const loweredNodes = lowerFragments(nodes, filePath, expressions);
|
|
478
|
+
return {
|
|
479
|
+
raw: templateHtml,
|
|
480
|
+
nodes: loweredNodes,
|
|
481
|
+
expressions
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
throw new CompilerError(`Template parsing failed: ${error.message}`, filePath, 1, 1);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith File Parser
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for parsing .zen files
|
|
5
|
+
* Phase 1: Parse & Extract only
|
|
6
|
+
*/
|
|
7
|
+
import type { ZenIR } from '../ir/types';
|
|
8
|
+
/**
|
|
9
|
+
* Parse a .zen file into IR
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseZenFile(filePath: string): ZenIR;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith File Parser
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for parsing .zen files
|
|
5
|
+
* Phase 1: Parse & Extract only
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { parseTemplate } from './parseTemplate';
|
|
9
|
+
import { parseScript } from './parseScript';
|
|
10
|
+
import { CompilerError } from '../errors/compilerError';
|
|
11
|
+
/**
|
|
12
|
+
* Extract style blocks from HTML
|
|
13
|
+
*/
|
|
14
|
+
function parseStyles(html) {
|
|
15
|
+
const styles = [];
|
|
16
|
+
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
17
|
+
let match;
|
|
18
|
+
while ((match = styleRegex.exec(html)) !== null) {
|
|
19
|
+
if (match[1]) {
|
|
20
|
+
styles.push({
|
|
21
|
+
raw: match[1].trim()
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return styles;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse a .zen file into IR
|
|
29
|
+
*/
|
|
30
|
+
export function parseZenFile(filePath) {
|
|
31
|
+
let source;
|
|
32
|
+
try {
|
|
33
|
+
source = readFileSync(filePath, 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new CompilerError(`Failed to read file: ${error.message}`, filePath, 1, 1);
|
|
37
|
+
}
|
|
38
|
+
// Parse template
|
|
39
|
+
const template = parseTemplate(source, filePath);
|
|
40
|
+
// Parse script
|
|
41
|
+
const script = parseScript(source);
|
|
42
|
+
// Parse styles
|
|
43
|
+
const styles = parseStyles(source);
|
|
44
|
+
return {
|
|
45
|
+
filePath,
|
|
46
|
+
template,
|
|
47
|
+
script,
|
|
48
|
+
styles
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script Analysis Utilities
|
|
3
|
+
*
|
|
4
|
+
* Extracts state and prop declarations from <script> blocks
|
|
5
|
+
*/
|
|
6
|
+
export interface StateInfo {
|
|
7
|
+
name: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Extract state declarations: state name = value
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractStateDeclarations(script: string): Map<string, string>;
|
|
14
|
+
/**
|
|
15
|
+
* Extract prop declarations: export let props: Props;
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractProps(script: string): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Transform script by removing state and prop declarations
|
|
20
|
+
*/
|
|
21
|
+
export declare function transformStateDeclarations(script: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Inject props into a setup script as top-level variables
|
|
24
|
+
*/
|
|
25
|
+
export declare function injectPropsIntoSetup(script: string, props: Record<string, any>): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script Analysis Utilities
|
|
3
|
+
*
|
|
4
|
+
* Extracts state and prop declarations from <script> blocks
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Extract state declarations: state name = value
|
|
8
|
+
*/
|
|
9
|
+
export function extractStateDeclarations(script) {
|
|
10
|
+
const states = new Map();
|
|
11
|
+
const statePattern = /state\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*([^;]+?)(?:\s*;|\s*$)/gm;
|
|
12
|
+
let match;
|
|
13
|
+
while ((match = statePattern.exec(script)) !== null) {
|
|
14
|
+
if (match[1] && match[2]) {
|
|
15
|
+
states.set(match[1], match[2].trim());
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return states;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract prop declarations: export let props: Props;
|
|
22
|
+
*/
|
|
23
|
+
export function extractProps(script) {
|
|
24
|
+
const props = [];
|
|
25
|
+
const propPattern = /export\s+let\s+props(?:\s*:\s*([^;]+))?[ \t]*;?/g;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = propPattern.exec(script)) !== null) {
|
|
28
|
+
if (!props.includes('props')) {
|
|
29
|
+
props.push('props');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return props;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Transform script by removing state and prop declarations
|
|
36
|
+
*/
|
|
37
|
+
export function transformStateDeclarations(script) {
|
|
38
|
+
let transformed = script;
|
|
39
|
+
// Remove state declarations (state count = 0)
|
|
40
|
+
transformed = transformed.replace(/state\s+([a-zA-Z_$][a-zA-Z0-9_$]*)[ \t]*=[ \t]*([^;]+?)(?:[ \t]*;|\s*$)/gm, '');
|
|
41
|
+
// Remove export let props (legacy)
|
|
42
|
+
transformed = transformed.replace(/export\s+let\s+props(?:\s*:\s*([^;]+))?\s*;?[ \t]*/g, '');
|
|
43
|
+
// Remove type/interface Props (carefully handling comments)
|
|
44
|
+
// We search for the start of the word 'type' or 'interface' and match until the closing brace
|
|
45
|
+
transformed = transformed.replace(/(?:type|interface)\s+Props\s*=?\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}[ \t]*;?/gs, '');
|
|
46
|
+
// Remove zenith/runtime imports
|
|
47
|
+
transformed = transformed.replace(/import\s+{[^}]+}\s+from\s+['"]zenith\/runtime['"]\s*;?[ \t]*/g, '');
|
|
48
|
+
// Transform zenith:content imports to global lookups
|
|
49
|
+
transformed = transformed.replace(/import\s*{\s*([^}]+)\s*}\s*from\s*['"]zenith:content['"]\s*;?/g, (_, imports) => `const { ${imports.trim()} } = window.__zenith;`);
|
|
50
|
+
return transformed.trim();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Inject props into a setup script as top-level variables
|
|
54
|
+
*/
|
|
55
|
+
export function injectPropsIntoSetup(script, props) {
|
|
56
|
+
const propDeclarations = Object.entries(props)
|
|
57
|
+
.map(([key, value]) => `const ${key} = ${typeof value === 'string' ? `'${value}'` : JSON.stringify(value)};`)
|
|
58
|
+
.join('\n');
|
|
59
|
+
return `${propDeclarations}\n\n${script}`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop Context Tracking
|
|
3
|
+
*
|
|
4
|
+
* Phase 7: Utilities for tracking and propagating loop context through the parse tree
|
|
5
|
+
*/
|
|
6
|
+
import type { LoopContext, ExpressionIR } from '../ir/types';
|
|
7
|
+
/**
|
|
8
|
+
* Check if an expression should have loop context attached
|
|
9
|
+
* Returns the loop context if the expression references loop variables
|
|
10
|
+
*/
|
|
11
|
+
export declare function shouldAttachLoopContext(expr: ExpressionIR, parentLoopContext?: LoopContext): LoopContext | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Merge loop contexts for nested loops
|
|
14
|
+
* Inner loops inherit outer loop variables
|
|
15
|
+
*/
|
|
16
|
+
export declare function mergeLoopContext(outer?: LoopContext, inner?: LoopContext): LoopContext | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Detect if an expression is a map expression and extract its loop context
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractLoopContextFromExpression(expr: ExpressionIR): LoopContext | undefined;
|