@zenithbuild/core 0.1.0
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/.eslintignore +15 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
- package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
- package/.github/pull_request_template.md +15 -0
- package/.github/workflows/discord-changelog.yml +141 -0
- package/.github/workflows/discord-notify.yml +242 -0
- package/.github/workflows/discord-version.yml +195 -0
- package/.prettierignore +13 -0
- package/.prettierrc +21 -0
- package/.zen.d.ts +15 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/app/components/Button.zen +46 -0
- package/app/components/Link.zen +11 -0
- package/app/favicon.ico +0 -0
- package/app/layouts/Main.zen +59 -0
- package/app/pages/about.zen +23 -0
- package/app/pages/blog/[id].zen +53 -0
- package/app/pages/blog/index.zen +32 -0
- package/app/pages/dynamic-dx.zen +712 -0
- package/app/pages/dynamic-primitives.zen +453 -0
- package/app/pages/index.zen +154 -0
- package/app/pages/navigation-demo.zen +229 -0
- package/app/pages/posts/[...slug].zen +61 -0
- package/app/pages/primitives-demo.zen +273 -0
- package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
- package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
- package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
- package/assets/logos/README.md +54 -0
- package/assets/logos/zen.icns +0 -0
- package/bun.lock +39 -0
- package/compiler/README.md +380 -0
- package/compiler/errors/compilerError.ts +24 -0
- package/compiler/finalize/finalizeOutput.ts +163 -0
- package/compiler/finalize/generateFinalBundle.ts +82 -0
- package/compiler/index.ts +44 -0
- package/compiler/ir/types.ts +83 -0
- package/compiler/legacy/binding.ts +254 -0
- package/compiler/legacy/bindings.ts +338 -0
- package/compiler/legacy/component-process.ts +1208 -0
- package/compiler/legacy/component.ts +301 -0
- package/compiler/legacy/event.ts +50 -0
- package/compiler/legacy/expression.ts +1149 -0
- package/compiler/legacy/mutation.ts +280 -0
- package/compiler/legacy/parse.ts +299 -0
- package/compiler/legacy/split.ts +608 -0
- package/compiler/legacy/types.ts +32 -0
- package/compiler/output/types.ts +34 -0
- package/compiler/parse/detectMapExpressions.ts +102 -0
- package/compiler/parse/parseScript.ts +22 -0
- package/compiler/parse/parseTemplate.ts +425 -0
- package/compiler/parse/parseZenFile.ts +66 -0
- package/compiler/parse/trackLoopContext.ts +82 -0
- package/compiler/runtime/dataExposure.ts +291 -0
- package/compiler/runtime/generateDOM.ts +144 -0
- package/compiler/runtime/generateHydrationBundle.ts +383 -0
- package/compiler/runtime/hydration.ts +309 -0
- package/compiler/runtime/navigation.ts +432 -0
- package/compiler/runtime/thinRuntime.ts +160 -0
- package/compiler/runtime/transformIR.ts +256 -0
- package/compiler/runtime/wrapExpression.ts +84 -0
- package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
- package/compiler/spa-build.ts +1000 -0
- package/compiler/test/validate-test.ts +104 -0
- package/compiler/transform/generateBindings.ts +47 -0
- package/compiler/transform/generateHTML.ts +28 -0
- package/compiler/transform/transformNode.ts +126 -0
- package/compiler/transform/transformTemplate.ts +38 -0
- package/compiler/validate/validateExpressions.ts +168 -0
- package/core/index.ts +135 -0
- package/core/lifecycle/index.ts +49 -0
- package/core/lifecycle/zen-mount.ts +182 -0
- package/core/lifecycle/zen-unmount.ts +88 -0
- package/core/reactivity/index.ts +54 -0
- package/core/reactivity/tracking.ts +167 -0
- package/core/reactivity/zen-batch.ts +57 -0
- package/core/reactivity/zen-effect.ts +139 -0
- package/core/reactivity/zen-memo.ts +146 -0
- package/core/reactivity/zen-ref.ts +52 -0
- package/core/reactivity/zen-signal.ts +121 -0
- package/core/reactivity/zen-state.ts +180 -0
- package/core/reactivity/zen-untrack.ts +44 -0
- package/docs/COMMENTS.md +111 -0
- package/docs/COMMITS.md +36 -0
- package/docs/CONTRIBUTING.md +116 -0
- package/docs/STYLEGUIDE.md +62 -0
- package/package.json +44 -0
- package/router/index.ts +76 -0
- package/router/manifest.ts +314 -0
- package/router/navigation/ZenLink.zen +231 -0
- package/router/navigation/index.ts +78 -0
- package/router/navigation/zen-link.ts +584 -0
- package/router/runtime.ts +458 -0
- package/router/types.ts +168 -0
- package/runtime/build.ts +17 -0
- package/runtime/serve.ts +93 -0
- package/scripts/webhook-proxy.ts +213 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map Expression Detection
|
|
3
|
+
*
|
|
4
|
+
* Phase 7: Detects .map() expressions and extracts loop context information
|
|
5
|
+
*
|
|
6
|
+
* This module analyzes expression code to detect map expressions like:
|
|
7
|
+
* - todoItems.map(todo => ...)
|
|
8
|
+
* - notifications.map((n, index) => ...)
|
|
9
|
+
*
|
|
10
|
+
* It extracts:
|
|
11
|
+
* - The array source (todoItems, notifications)
|
|
12
|
+
* - Loop variable names (todo, n, index)
|
|
13
|
+
* - The map body/template
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExpressionIR } from '../ir/types'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detected map expression information
|
|
20
|
+
*/
|
|
21
|
+
export interface MapExpressionInfo {
|
|
22
|
+
isMap: boolean
|
|
23
|
+
arraySource?: string // e.g., 'todoItems'
|
|
24
|
+
itemVariable?: string // e.g., 'todo'
|
|
25
|
+
indexVariable?: string // e.g., 'index'
|
|
26
|
+
mapBody?: string // The template/body inside the map
|
|
27
|
+
fullExpression?: string // The full expression code
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect if an expression is a map expression and extract loop context
|
|
32
|
+
*
|
|
33
|
+
* Patterns detected:
|
|
34
|
+
* - arraySource.map(item => body)
|
|
35
|
+
* - arraySource.map((item, index) => body)
|
|
36
|
+
* - arraySource.map(item => (body))
|
|
37
|
+
*/
|
|
38
|
+
export function detectMapExpression(expr: ExpressionIR): MapExpressionInfo {
|
|
39
|
+
const { code } = expr
|
|
40
|
+
|
|
41
|
+
// Pattern: arraySource.map(item => body)
|
|
42
|
+
// Pattern: arraySource.map((item, index) => body)
|
|
43
|
+
// Pattern: arraySource.map(item => (body))
|
|
44
|
+
const mapPattern = /^(.+?)\.\s*map\s*\(\s*\(?([^)=,\s]+)(?:\s*,\s*([^)=,\s]+))?\s*\)?\s*=>\s*(.+?)\)?$/s
|
|
45
|
+
|
|
46
|
+
const match = code.match(mapPattern)
|
|
47
|
+
if (!match) {
|
|
48
|
+
return { isMap: false }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const arraySource = match[1]?.trim()
|
|
52
|
+
const itemVariable = match[2]?.trim()
|
|
53
|
+
const indexVariable = match[3]?.trim()
|
|
54
|
+
const mapBody = match[4]?.trim()
|
|
55
|
+
|
|
56
|
+
if (!arraySource || !itemVariable || !mapBody) {
|
|
57
|
+
return { isMap: false }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
isMap: true,
|
|
62
|
+
arraySource,
|
|
63
|
+
itemVariable,
|
|
64
|
+
indexVariable,
|
|
65
|
+
mapBody,
|
|
66
|
+
fullExpression: code
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extract loop variables from a map expression
|
|
72
|
+
* Returns array of variable names in order: [itemVariable, indexVariable?]
|
|
73
|
+
*/
|
|
74
|
+
export function extractLoopVariables(mapInfo: MapExpressionInfo): string[] {
|
|
75
|
+
if (!mapInfo.isMap || !mapInfo.itemVariable) {
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const vars = [mapInfo.itemVariable]
|
|
80
|
+
if (mapInfo.indexVariable) {
|
|
81
|
+
vars.push(mapInfo.indexVariable)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return vars
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if an expression references a loop variable
|
|
89
|
+
* Used to determine if an expression needs loop context
|
|
90
|
+
*/
|
|
91
|
+
export function referencesLoopVariable(exprCode: string, loopVars: string[]): boolean {
|
|
92
|
+
for (const loopVar of loopVars) {
|
|
93
|
+
// Match variable references: loopVar.property, loopVar, etc.
|
|
94
|
+
// Use word boundaries to avoid partial matches
|
|
95
|
+
const pattern = new RegExp(`\\b${loopVar}\\b`)
|
|
96
|
+
if (pattern.test(exprCode)) {
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script Parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts <script> blocks from .zen files
|
|
5
|
+
* Phase 1: Only extracts raw content, no evaluation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ScriptIR } from '../ir/types'
|
|
9
|
+
|
|
10
|
+
export function parseScript(html: string): ScriptIR | null {
|
|
11
|
+
// Extract script content using regex (simple extraction for Phase 1)
|
|
12
|
+
const scriptMatch = html.match(/<script[^>]*>([\s\S]*?)<\/script>/i)
|
|
13
|
+
|
|
14
|
+
if (!scriptMatch || !scriptMatch[1]) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
raw: scriptMatch[1].trim()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,425 @@
|
|
|
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 } from '../errors/compilerError'
|
|
11
|
+
import { parseScript } from './parseScript'
|
|
12
|
+
import { detectMapExpression, extractLoopVariables, referencesLoopVariable } from './detectMapExpressions'
|
|
13
|
+
import { shouldAttachLoopContext, mergeLoopContext, extractLoopContextFromExpression } from './trackLoopContext'
|
|
14
|
+
|
|
15
|
+
// Generate stable IDs for expressions
|
|
16
|
+
let expressionIdCounter = 0
|
|
17
|
+
function generateExpressionId(): string {
|
|
18
|
+
return `expr_${expressionIdCounter++}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Strip script and style blocks from HTML before parsing
|
|
23
|
+
*/
|
|
24
|
+
function stripBlocks(html: string): string {
|
|
25
|
+
// Remove script blocks
|
|
26
|
+
let stripped = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
27
|
+
// Remove style blocks
|
|
28
|
+
stripped = stripped.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
29
|
+
return stripped
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize attribute expressions before parsing
|
|
34
|
+
* Replaces attr={expr} with attr="__ZEN_EXPR_base64" so parse5 can parse it
|
|
35
|
+
*/
|
|
36
|
+
function normalizeAttributeExpressions(html: string): { normalized: string; expressions: Map<string, string> } {
|
|
37
|
+
const exprMap = new Map<string, string>()
|
|
38
|
+
let exprCounter = 0
|
|
39
|
+
|
|
40
|
+
// Match attributes with expression values: attr={...}
|
|
41
|
+
// Use a more sophisticated regex to handle nested braces and quotes
|
|
42
|
+
const normalized = html.replace(/(\w+)=\{([^}]+)\}/g, (match, attrName, expr) => {
|
|
43
|
+
const placeholder = `__ZEN_EXPR_${exprCounter++}`
|
|
44
|
+
exprMap.set(placeholder, expr.trim())
|
|
45
|
+
return `${attrName}="${placeholder}"`
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return { normalized, expressions: exprMap }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Calculate source location from parse5 node
|
|
53
|
+
*/
|
|
54
|
+
function getLocation(node: any, originalHtml: string): SourceLocation {
|
|
55
|
+
// parse5 provides sourceCodeLocation if available
|
|
56
|
+
if (node.sourceCodeLocation) {
|
|
57
|
+
return {
|
|
58
|
+
line: node.sourceCodeLocation.startLine || 1,
|
|
59
|
+
column: node.sourceCodeLocation.startCol || 1
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Fallback if location info not available
|
|
63
|
+
return { line: 1, column: 1 }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract expressions from text content
|
|
68
|
+
* Returns array of { expression, location } and the text with expressions replaced
|
|
69
|
+
* Phase 7: Supports loop context for expressions inside map iterations
|
|
70
|
+
*/
|
|
71
|
+
function extractExpressionsFromText(
|
|
72
|
+
text: string,
|
|
73
|
+
baseLocation: SourceLocation,
|
|
74
|
+
expressions: ExpressionIR[],
|
|
75
|
+
loopContext?: LoopContext // Phase 7: Loop context from parent map expressions
|
|
76
|
+
): { processedText: string; nodes: (TextNode | ExpressionNode)[] } {
|
|
77
|
+
const nodes: (TextNode | ExpressionNode)[] = []
|
|
78
|
+
let processedText = ''
|
|
79
|
+
let currentIndex = 0
|
|
80
|
+
|
|
81
|
+
// Match { ... } expressions (non-greedy)
|
|
82
|
+
const expressionRegex = /\{([^}]+)\}/g
|
|
83
|
+
let match
|
|
84
|
+
|
|
85
|
+
while ((match = expressionRegex.exec(text)) !== null) {
|
|
86
|
+
const beforeExpr = text.substring(currentIndex, match.index)
|
|
87
|
+
if (beforeExpr) {
|
|
88
|
+
nodes.push({
|
|
89
|
+
type: 'text',
|
|
90
|
+
value: beforeExpr,
|
|
91
|
+
location: {
|
|
92
|
+
line: baseLocation.line,
|
|
93
|
+
column: baseLocation.column + currentIndex
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
processedText += beforeExpr
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Extract expression
|
|
100
|
+
const exprCode = (match[1] || '').trim()
|
|
101
|
+
const exprId = generateExpressionId()
|
|
102
|
+
const exprLocation: SourceLocation = {
|
|
103
|
+
line: baseLocation.line,
|
|
104
|
+
column: baseLocation.column + match.index + 1 // +1 for opening brace
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const exprIR: ExpressionIR = {
|
|
108
|
+
id: exprId,
|
|
109
|
+
code: exprCode,
|
|
110
|
+
location: exprLocation
|
|
111
|
+
}
|
|
112
|
+
expressions.push(exprIR)
|
|
113
|
+
|
|
114
|
+
// Phase 7: Detect if this is a map expression and extract loop context
|
|
115
|
+
const mapLoopContext = extractLoopContextFromExpression(exprIR)
|
|
116
|
+
const activeLoopContext = mergeLoopContext(loopContext, mapLoopContext)
|
|
117
|
+
|
|
118
|
+
// Phase 7: Attach loop context if expression references loop variables
|
|
119
|
+
const attachedLoopContext = shouldAttachLoopContext(exprIR, activeLoopContext)
|
|
120
|
+
|
|
121
|
+
nodes.push({
|
|
122
|
+
type: 'expression',
|
|
123
|
+
expression: exprId,
|
|
124
|
+
location: exprLocation,
|
|
125
|
+
loopContext: attachedLoopContext
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
processedText += `{${exprCode}}` // Keep in processed text for now
|
|
129
|
+
currentIndex = match.index + match[0].length
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Add remaining text
|
|
133
|
+
const remaining = text.substring(currentIndex)
|
|
134
|
+
if (remaining) {
|
|
135
|
+
nodes.push({
|
|
136
|
+
type: 'text',
|
|
137
|
+
value: remaining,
|
|
138
|
+
location: {
|
|
139
|
+
line: baseLocation.line,
|
|
140
|
+
column: baseLocation.column + currentIndex
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
processedText += remaining
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If no expressions found, return single text node
|
|
147
|
+
if (nodes.length === 0) {
|
|
148
|
+
nodes.push({
|
|
149
|
+
type: 'text',
|
|
150
|
+
value: text,
|
|
151
|
+
location: baseLocation
|
|
152
|
+
})
|
|
153
|
+
processedText = text
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { processedText, nodes }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parse attribute value - may contain expressions
|
|
161
|
+
* Phase 7: Supports loop context for expressions inside map iterations
|
|
162
|
+
*/
|
|
163
|
+
function parseAttributeValue(
|
|
164
|
+
value: string,
|
|
165
|
+
baseLocation: SourceLocation,
|
|
166
|
+
expressions: ExpressionIR[],
|
|
167
|
+
normalizedExprs: Map<string, string>,
|
|
168
|
+
loopContext?: LoopContext // Phase 7: Loop context from parent map expressions
|
|
169
|
+
): string | ExpressionIR {
|
|
170
|
+
// Check if this is a normalized expression placeholder
|
|
171
|
+
if (value.startsWith('__ZEN_EXPR_')) {
|
|
172
|
+
const exprCode = normalizedExprs.get(value)
|
|
173
|
+
if (!exprCode) {
|
|
174
|
+
throw new Error(`Normalized expression placeholder not found: ${value}`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const exprId = generateExpressionId()
|
|
178
|
+
|
|
179
|
+
expressions.push({
|
|
180
|
+
id: exprId,
|
|
181
|
+
code: exprCode,
|
|
182
|
+
location: baseLocation
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
id: exprId,
|
|
187
|
+
code: exprCode,
|
|
188
|
+
location: baseLocation
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check if attribute value is an expression { ... } (shouldn't happen after normalization)
|
|
193
|
+
const exprMatch = value.match(/^\{([^}]+)\}$/)
|
|
194
|
+
if (exprMatch && exprMatch[1]) {
|
|
195
|
+
const exprCode = exprMatch[1].trim()
|
|
196
|
+
const exprId = generateExpressionId()
|
|
197
|
+
|
|
198
|
+
expressions.push({
|
|
199
|
+
id: exprId,
|
|
200
|
+
code: exprCode,
|
|
201
|
+
location: baseLocation
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
id: exprId,
|
|
206
|
+
code: exprCode,
|
|
207
|
+
location: baseLocation
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Regular string value
|
|
212
|
+
return value
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Convert parse5 node to TemplateNode
|
|
217
|
+
* Phase 7: Supports loop context propagation for map expressions
|
|
218
|
+
*/
|
|
219
|
+
function parseNode(
|
|
220
|
+
node: any,
|
|
221
|
+
originalHtml: string,
|
|
222
|
+
expressions: ExpressionIR[],
|
|
223
|
+
normalizedExprs: Map<string, string>,
|
|
224
|
+
parentLoopContext?: LoopContext // Phase 7: Loop context from parent map expressions
|
|
225
|
+
): TemplateNode | null {
|
|
226
|
+
if (node.nodeName === '#text') {
|
|
227
|
+
const text = node.value || ''
|
|
228
|
+
const location = getLocation(node, originalHtml)
|
|
229
|
+
|
|
230
|
+
// Extract expressions from text
|
|
231
|
+
// Phase 7: Pass loop context to detect map expressions and attach context
|
|
232
|
+
const { nodes } = extractExpressionsFromText(text, location, expressions, parentLoopContext)
|
|
233
|
+
|
|
234
|
+
// If single text node with no expressions, return it
|
|
235
|
+
if (nodes.length === 1 && nodes[0] && nodes[0].type === 'text') {
|
|
236
|
+
return nodes[0]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Otherwise, we need to handle multiple nodes
|
|
240
|
+
// For Phase 1, we'll flatten to text for now (will be handled in future phases)
|
|
241
|
+
// This is a limitation we accept for Phase 1
|
|
242
|
+
const firstNode = nodes[0]
|
|
243
|
+
if (firstNode) {
|
|
244
|
+
return firstNode
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
type: 'text',
|
|
248
|
+
value: text,
|
|
249
|
+
location
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (node.nodeName === '#comment') {
|
|
254
|
+
// Skip comments for Phase 1
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (node.nodeName && node.nodeName !== '#text' && node.nodeName !== '#comment') {
|
|
259
|
+
const location = getLocation(node, originalHtml)
|
|
260
|
+
const tag = node.tagName?.toLowerCase() || node.nodeName
|
|
261
|
+
|
|
262
|
+
// Parse attributes
|
|
263
|
+
const attributes: AttributeIR[] = []
|
|
264
|
+
if (node.attrs) {
|
|
265
|
+
for (const attr of node.attrs) {
|
|
266
|
+
const attrLocation = node.sourceCodeLocation?.attrs?.[attr.name]
|
|
267
|
+
? {
|
|
268
|
+
line: node.sourceCodeLocation.attrs[attr.name].startLine || location.line,
|
|
269
|
+
column: node.sourceCodeLocation.attrs[attr.name].startCol || location.column
|
|
270
|
+
}
|
|
271
|
+
: location
|
|
272
|
+
|
|
273
|
+
// Handle :attr="expr" syntax (colon-prefixed reactive attributes)
|
|
274
|
+
let attrName = attr.name
|
|
275
|
+
let attrValue = attr.value || ''
|
|
276
|
+
let isReactive = false
|
|
277
|
+
|
|
278
|
+
if (attrName.startsWith(':')) {
|
|
279
|
+
// This is a reactive attribute like :class="expr"
|
|
280
|
+
attrName = attrName.slice(1) // Remove the colon
|
|
281
|
+
isReactive = true
|
|
282
|
+
// The value is already a string expression (not in braces)
|
|
283
|
+
// Treat it as an expression
|
|
284
|
+
const exprId = generateExpressionId()
|
|
285
|
+
const exprCode = attrValue.trim()
|
|
286
|
+
|
|
287
|
+
const exprIR: ExpressionIR = {
|
|
288
|
+
id: exprId,
|
|
289
|
+
code: exprCode,
|
|
290
|
+
location: attrLocation
|
|
291
|
+
}
|
|
292
|
+
expressions.push(exprIR)
|
|
293
|
+
|
|
294
|
+
// Phase 7: Attach loop context if expression references loop variables
|
|
295
|
+
const attachedLoopContext = shouldAttachLoopContext(exprIR, parentLoopContext)
|
|
296
|
+
|
|
297
|
+
attributes.push({
|
|
298
|
+
name: attrName, // Store without colon (e.g., "class" not ":class")
|
|
299
|
+
value: exprIR,
|
|
300
|
+
location: attrLocation,
|
|
301
|
+
loopContext: attachedLoopContext
|
|
302
|
+
})
|
|
303
|
+
} else {
|
|
304
|
+
// Regular attribute or attr={expr} syntax
|
|
305
|
+
const attrValueResult = parseAttributeValue(attrValue, attrLocation, expressions, normalizedExprs, parentLoopContext)
|
|
306
|
+
|
|
307
|
+
if (typeof attrValueResult === 'string') {
|
|
308
|
+
// Static attribute value
|
|
309
|
+
attributes.push({
|
|
310
|
+
name: attrName,
|
|
311
|
+
value: attrValueResult,
|
|
312
|
+
location: attrLocation
|
|
313
|
+
})
|
|
314
|
+
} else {
|
|
315
|
+
// Expression attribute value
|
|
316
|
+
const exprIR = attrValueResult
|
|
317
|
+
|
|
318
|
+
// Phase 7: Attach loop context if expression references loop variables
|
|
319
|
+
const attachedLoopContext = shouldAttachLoopContext(exprIR, parentLoopContext)
|
|
320
|
+
|
|
321
|
+
attributes.push({
|
|
322
|
+
name: attrName,
|
|
323
|
+
value: exprIR,
|
|
324
|
+
location: attrLocation,
|
|
325
|
+
loopContext: attachedLoopContext
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Parse children
|
|
333
|
+
const children: TemplateNode[] = []
|
|
334
|
+
if (node.childNodes) {
|
|
335
|
+
for (const child of node.childNodes) {
|
|
336
|
+
if (child.nodeName === '#text') {
|
|
337
|
+
// Handle text nodes that may contain expressions
|
|
338
|
+
const text = child.value || ''
|
|
339
|
+
const location = getLocation(child, originalHtml)
|
|
340
|
+
const { nodes: textNodes } = extractExpressionsFromText(text, location, expressions, parentLoopContext)
|
|
341
|
+
|
|
342
|
+
// Add all nodes from text (can be multiple: text + expression + text)
|
|
343
|
+
for (const textNode of textNodes) {
|
|
344
|
+
children.push(textNode)
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
const childNode = parseNode(child, originalHtml, expressions, normalizedExprs, parentLoopContext)
|
|
348
|
+
if (childNode) {
|
|
349
|
+
children.push(childNode)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Phase 7: Check if any child expression is a map expression and extract its loop context
|
|
356
|
+
// This allows nested loops to work correctly
|
|
357
|
+
let elementLoopContext = parentLoopContext
|
|
358
|
+
|
|
359
|
+
// Check children for map expressions (they create new loop contexts)
|
|
360
|
+
for (const child of children) {
|
|
361
|
+
if (child.type === 'expression' && child.loopContext) {
|
|
362
|
+
// If we find a map expression child, merge its context
|
|
363
|
+
elementLoopContext = mergeLoopContext(elementLoopContext, child.loopContext)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
type: 'element',
|
|
369
|
+
tag,
|
|
370
|
+
attributes,
|
|
371
|
+
children,
|
|
372
|
+
location,
|
|
373
|
+
loopContext: elementLoopContext // Phase 7: Inherited loop context for child processing
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return null
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Parse template from HTML string
|
|
382
|
+
*/
|
|
383
|
+
export function parseTemplate(html: string, filePath: string): TemplateIR {
|
|
384
|
+
// Strip script and style blocks
|
|
385
|
+
let templateHtml = stripBlocks(html)
|
|
386
|
+
|
|
387
|
+
// Normalize attribute expressions so parse5 can parse them
|
|
388
|
+
const { normalized, expressions: normalizedExprs } = normalizeAttributeExpressions(templateHtml)
|
|
389
|
+
templateHtml = normalized
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
// Parse HTML using parseFragment (handles fragments without html/body wrapper)
|
|
393
|
+
const fragment = parseFragment(templateHtml, {
|
|
394
|
+
sourceCodeLocationInfo: true
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
const expressions: ExpressionIR[] = []
|
|
398
|
+
const nodes: TemplateNode[] = []
|
|
399
|
+
|
|
400
|
+
// Parse fragment children
|
|
401
|
+
// Phase 7: Start with no loop context (top-level expressions)
|
|
402
|
+
if (fragment.childNodes) {
|
|
403
|
+
for (const node of fragment.childNodes) {
|
|
404
|
+
const parsed = parseNode(node, templateHtml, expressions, normalizedExprs, undefined)
|
|
405
|
+
if (parsed) {
|
|
406
|
+
nodes.push(parsed)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
raw: templateHtml,
|
|
413
|
+
nodes,
|
|
414
|
+
expressions
|
|
415
|
+
}
|
|
416
|
+
} catch (error: any) {
|
|
417
|
+
throw new CompilerError(
|
|
418
|
+
`Template parsing failed: ${error.message}`,
|
|
419
|
+
filePath,
|
|
420
|
+
1,
|
|
421
|
+
1
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop Context Tracking
|
|
3
|
+
*
|
|
4
|
+
* Phase 7: Utilities for tracking and propagating loop context through the parse tree
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { LoopContext, ExpressionIR } from '../ir/types'
|
|
8
|
+
import { detectMapExpression, referencesLoopVariable } from './detectMapExpressions'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if an expression should have loop context attached
|
|
12
|
+
* Returns the loop context if the expression references loop variables
|
|
13
|
+
*/
|
|
14
|
+
export function shouldAttachLoopContext(
|
|
15
|
+
expr: ExpressionIR,
|
|
16
|
+
parentLoopContext?: LoopContext
|
|
17
|
+
): LoopContext | undefined {
|
|
18
|
+
if (!parentLoopContext) {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check if this expression references any loop variables
|
|
23
|
+
if (referencesLoopVariable(expr.code, parentLoopContext.variables)) {
|
|
24
|
+
return parentLoopContext
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Merge loop contexts for nested loops
|
|
32
|
+
* Inner loops inherit outer loop variables
|
|
33
|
+
*/
|
|
34
|
+
export function mergeLoopContext(
|
|
35
|
+
outer?: LoopContext,
|
|
36
|
+
inner?: LoopContext
|
|
37
|
+
): LoopContext | undefined {
|
|
38
|
+
if (!inner) {
|
|
39
|
+
return outer
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!outer) {
|
|
43
|
+
return inner
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Merge variables: outer variables come first, then inner
|
|
47
|
+
// This allows expressions to reference both outer and inner loop variables
|
|
48
|
+
return {
|
|
49
|
+
variables: [...outer.variables, ...inner.variables],
|
|
50
|
+
mapSource: inner.mapSource || outer.mapSource
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detect if an expression is a map expression and extract its loop context
|
|
56
|
+
*/
|
|
57
|
+
export function extractLoopContextFromExpression(expr: ExpressionIR): LoopContext | undefined {
|
|
58
|
+
const mapInfo = detectMapExpression(expr)
|
|
59
|
+
|
|
60
|
+
if (!mapInfo.isMap) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// extractLoopVariables expects a MapExpressionInfo, not a string
|
|
65
|
+
const variables: string[] = []
|
|
66
|
+
if (mapInfo.itemVariable) {
|
|
67
|
+
variables.push(mapInfo.itemVariable)
|
|
68
|
+
}
|
|
69
|
+
if (mapInfo.indexVariable) {
|
|
70
|
+
variables.push(mapInfo.indexVariable)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (variables.length === 0) {
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
variables,
|
|
79
|
+
mapSource: mapInfo.arraySource
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|