@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,1149 @@
|
|
|
1
|
+
// compiler/expression.ts
|
|
2
|
+
// Expression parser and analyzer for dynamic HTML expressions in Zenith
|
|
3
|
+
// Handles: conditionals (&&, ||), ternaries (?:), map iterations, and inline expressions
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Expression types that can appear in { } blocks in HTML
|
|
7
|
+
*/
|
|
8
|
+
export type ExpressionType =
|
|
9
|
+
| 'static' // Simple state reference: { count }
|
|
10
|
+
| 'conditional' // Boolean conditional: { isLoggedIn && <span>...</span> }
|
|
11
|
+
| 'ternary' // Ternary: { isLoading ? "Loading" : "Submit" }
|
|
12
|
+
| 'map' // Array map: { items.map(item => <li>{item}</li>) }
|
|
13
|
+
| 'complex' // Complex expression requiring runtime evaluation
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parsed expression information
|
|
17
|
+
*/
|
|
18
|
+
export interface ParsedExpression {
|
|
19
|
+
type: ExpressionType
|
|
20
|
+
raw: string // Original expression text
|
|
21
|
+
condition?: string // For conditionals/ternaries: the condition part
|
|
22
|
+
trueBranch?: string // For conditionals/ternaries: the true branch
|
|
23
|
+
falseBranch?: string // For ternaries: the false branch
|
|
24
|
+
arraySource?: string // For map: the array being mapped
|
|
25
|
+
itemName?: string // For map: the item variable name
|
|
26
|
+
indexName?: string // For map: the index variable name (optional)
|
|
27
|
+
mapBody?: string // For map: the body/template of the map
|
|
28
|
+
keyExpression?: string // For map: the key attribute expression
|
|
29
|
+
dependencies: string[] // State variables this expression depends on
|
|
30
|
+
isStatic: boolean // Can be evaluated at build time
|
|
31
|
+
hasJSX: boolean // Contains JSX/HTML elements
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Result of analyzing an expression block in HTML
|
|
36
|
+
*/
|
|
37
|
+
export interface ExpressionBlock {
|
|
38
|
+
startIndex: number // Position in source HTML
|
|
39
|
+
endIndex: number // End position in source HTML
|
|
40
|
+
expression: ParsedExpression
|
|
41
|
+
placeholderId: string // Unique ID for DOM placeholder
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Decode HTML entities in a string
|
|
46
|
+
*/
|
|
47
|
+
function decodeHtmlEntities(str: string): string {
|
|
48
|
+
return str
|
|
49
|
+
.replace(/&/g, '&')
|
|
50
|
+
.replace(/</g, '<')
|
|
51
|
+
.replace(/>/g, '>')
|
|
52
|
+
.replace(/"/g, '"')
|
|
53
|
+
.replace(/'/g, "'")
|
|
54
|
+
.replace(/'/g, "'")
|
|
55
|
+
.replace(/ /g, ' ')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse a single expression string (content inside { })
|
|
60
|
+
*/
|
|
61
|
+
export function parseExpression(raw: string, declaredStates: Set<string>): ParsedExpression {
|
|
62
|
+
// Decode HTML entities first (parse5 encodes them during serialization)
|
|
63
|
+
const decoded = decodeHtmlEntities(raw)
|
|
64
|
+
const trimmed = decoded.trim()
|
|
65
|
+
|
|
66
|
+
// Track dependencies
|
|
67
|
+
const dependencies: string[] = []
|
|
68
|
+
|
|
69
|
+
// Check if it references declared states
|
|
70
|
+
for (const state of declaredStates) {
|
|
71
|
+
const stateRegex = new RegExp(`\\b${state}\\b`, 'g')
|
|
72
|
+
if (stateRegex.test(trimmed)) {
|
|
73
|
+
dependencies.push(state)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for JSX/HTML in the expression
|
|
78
|
+
const hasJSX = /<[a-zA-Z][^>]*>/.test(trimmed) || /\/>/.test(trimmed)
|
|
79
|
+
|
|
80
|
+
// Determine expression type
|
|
81
|
+
const type = detectExpressionType(trimmed)
|
|
82
|
+
|
|
83
|
+
const result: ParsedExpression = {
|
|
84
|
+
type,
|
|
85
|
+
raw: trimmed,
|
|
86
|
+
dependencies,
|
|
87
|
+
isStatic: dependencies.length === 0 && !hasJSX,
|
|
88
|
+
hasJSX
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Parse based on type
|
|
92
|
+
switch (type) {
|
|
93
|
+
case 'conditional':
|
|
94
|
+
parseConditional(trimmed, result)
|
|
95
|
+
break
|
|
96
|
+
case 'ternary':
|
|
97
|
+
parseTernary(trimmed, result)
|
|
98
|
+
break
|
|
99
|
+
case 'map':
|
|
100
|
+
parseMap(trimmed, result)
|
|
101
|
+
break
|
|
102
|
+
case 'static':
|
|
103
|
+
// Simple identifier, no additional parsing needed
|
|
104
|
+
break
|
|
105
|
+
case 'complex':
|
|
106
|
+
// Complex expression, evaluate at runtime
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Detect the type of expression
|
|
115
|
+
*/
|
|
116
|
+
function detectExpressionType(expr: string): ExpressionType {
|
|
117
|
+
const trimmed = expr.trim()
|
|
118
|
+
|
|
119
|
+
// Check for .map() - most specific first
|
|
120
|
+
if (/\.\s*map\s*\(/.test(trimmed)) {
|
|
121
|
+
return 'map'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check for ternary operator (must handle nested parens/brackets)
|
|
125
|
+
if (hasTernary(trimmed)) {
|
|
126
|
+
return 'ternary'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check for && or || conditionals (with JSX)
|
|
130
|
+
if (/\s*&&\s*/.test(trimmed) || /\s*\|\|\s*/.test(trimmed)) {
|
|
131
|
+
return 'conditional'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Simple identifier - static reference
|
|
135
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmed)) {
|
|
136
|
+
return 'static'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Complex expression
|
|
140
|
+
return 'complex'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if expression contains a ternary at the top level (not nested)
|
|
145
|
+
*/
|
|
146
|
+
function hasTernary(expr: string): boolean {
|
|
147
|
+
let depth = 0
|
|
148
|
+
let inString = false
|
|
149
|
+
let stringChar = ''
|
|
150
|
+
let inTemplate = false
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < expr.length; i++) {
|
|
153
|
+
const char = expr[i]
|
|
154
|
+
const prevChar = i > 0 ? expr[i - 1] : ''
|
|
155
|
+
|
|
156
|
+
// Handle string literals
|
|
157
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
158
|
+
inString = true
|
|
159
|
+
stringChar = char
|
|
160
|
+
if (char === '`') inTemplate = true
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (inString && char === stringChar && prevChar !== '\\') {
|
|
165
|
+
inString = false
|
|
166
|
+
inTemplate = false
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (inString) continue
|
|
171
|
+
|
|
172
|
+
// Track depth for parens, brackets, braces
|
|
173
|
+
if (char === '(' || char === '[' || char === '{') {
|
|
174
|
+
depth++
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
if (char === ')' || char === ']' || char === '}') {
|
|
178
|
+
depth--
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for ? at depth 0 (top level)
|
|
183
|
+
if (char === '?' && depth === 0) {
|
|
184
|
+
// Make sure it's not ?. or ??
|
|
185
|
+
const nextChar = i < expr.length - 1 ? expr[i + 1] : ''
|
|
186
|
+
if (nextChar !== '.' && nextChar !== '?') {
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Parse a conditional expression (&&, ||)
|
|
197
|
+
*/
|
|
198
|
+
function parseConditional(expr: string, result: ParsedExpression): void {
|
|
199
|
+
// Handle both && and ||
|
|
200
|
+
// Pattern: condition && <element>
|
|
201
|
+
// Pattern: !condition && <element>
|
|
202
|
+
|
|
203
|
+
// Find the && or || operator at the top level
|
|
204
|
+
let depth = 0
|
|
205
|
+
let inString = false
|
|
206
|
+
let stringChar = ''
|
|
207
|
+
let operatorIndex = -1
|
|
208
|
+
let operatorType = ''
|
|
209
|
+
|
|
210
|
+
for (let i = 0; i < expr.length - 1; i++) {
|
|
211
|
+
const char = expr[i]
|
|
212
|
+
const nextChar = expr[i + 1]
|
|
213
|
+
|
|
214
|
+
// Handle string literals
|
|
215
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
216
|
+
inString = true
|
|
217
|
+
stringChar = char
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (inString && char === stringChar && (i === 0 || expr[i - 1] !== '\\')) {
|
|
222
|
+
inString = false
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (inString) continue
|
|
227
|
+
|
|
228
|
+
// Track depth
|
|
229
|
+
if (char === '(' || char === '[' || char === '{' || char === '<') {
|
|
230
|
+
depth++
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
if (char === ')' || char === ']' || char === '}' || char === '>') {
|
|
234
|
+
depth--
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Look for && or || at depth 0
|
|
239
|
+
if (depth === 0) {
|
|
240
|
+
if (char === '&' && nextChar === '&') {
|
|
241
|
+
operatorIndex = i
|
|
242
|
+
operatorType = '&&'
|
|
243
|
+
break
|
|
244
|
+
}
|
|
245
|
+
if (char === '|' && nextChar === '|') {
|
|
246
|
+
operatorIndex = i
|
|
247
|
+
operatorType = '||'
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (operatorIndex > -1) {
|
|
254
|
+
result.condition = expr.substring(0, operatorIndex).trim()
|
|
255
|
+
result.trueBranch = expr.substring(operatorIndex + 2).trim()
|
|
256
|
+
|
|
257
|
+
// For || operator, the semantics are reversed
|
|
258
|
+
if (operatorType === '||') {
|
|
259
|
+
// a || b means: if !a then b
|
|
260
|
+
// So we invert: condition becomes !condition, trueBranch stays
|
|
261
|
+
result.condition = `!(${result.condition})`
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Parse a ternary expression (condition ? true : false)
|
|
268
|
+
*/
|
|
269
|
+
function parseTernary(expr: string, result: ParsedExpression): void {
|
|
270
|
+
let depth = 0
|
|
271
|
+
let inString = false
|
|
272
|
+
let stringChar = ''
|
|
273
|
+
let questionIndex = -1
|
|
274
|
+
let colonIndex = -1
|
|
275
|
+
|
|
276
|
+
for (let i = 0; i < expr.length; i++) {
|
|
277
|
+
const char = expr[i]
|
|
278
|
+
const prevChar = i > 0 ? expr[i - 1] : ''
|
|
279
|
+
const nextChar = i < expr.length - 1 ? expr[i + 1] : ''
|
|
280
|
+
|
|
281
|
+
// Handle string literals
|
|
282
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
283
|
+
inString = true
|
|
284
|
+
stringChar = char
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (inString && char === stringChar && prevChar !== '\\') {
|
|
289
|
+
inString = false
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (inString) continue
|
|
294
|
+
|
|
295
|
+
// Track depth
|
|
296
|
+
if (char === '(' || char === '[' || char === '{') {
|
|
297
|
+
depth++
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
if (char === ')' || char === ']' || char === '}') {
|
|
301
|
+
depth--
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Find ? at depth 0
|
|
306
|
+
if (char === '?' && depth === 0 && questionIndex === -1) {
|
|
307
|
+
// Make sure it's not ?. or ??
|
|
308
|
+
if (nextChar !== '.' && nextChar !== '?') {
|
|
309
|
+
questionIndex = i
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Find : at depth 0 (after ?)
|
|
314
|
+
if (char === ':' && depth === 0 && questionIndex > -1 && colonIndex === -1) {
|
|
315
|
+
colonIndex = i
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (questionIndex > -1 && colonIndex > -1) {
|
|
320
|
+
result.condition = expr.substring(0, questionIndex).trim()
|
|
321
|
+
result.trueBranch = expr.substring(questionIndex + 1, colonIndex).trim()
|
|
322
|
+
result.falseBranch = expr.substring(colonIndex + 1).trim()
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Parse a map expression (array.map(item => <element>))
|
|
328
|
+
*/
|
|
329
|
+
function parseMap(expr: string, result: ParsedExpression): void {
|
|
330
|
+
// Pattern: arraySource.map((item, index?) => body)
|
|
331
|
+
// Pattern: arraySource.map(item => body)
|
|
332
|
+
|
|
333
|
+
const mapMatch = expr.match(/^(.+?)\s*\.\s*map\s*\(\s*\(?([^)=,]+)(?:\s*,\s*([^)=]+))?\)?\s*=>\s*(.+)\)$/s)
|
|
334
|
+
|
|
335
|
+
if (mapMatch) {
|
|
336
|
+
result.arraySource = mapMatch[1]?.trim()
|
|
337
|
+
result.itemName = mapMatch[2]?.trim()
|
|
338
|
+
result.indexName = mapMatch[3]?.trim()
|
|
339
|
+
result.mapBody = mapMatch[4]?.trim()
|
|
340
|
+
|
|
341
|
+
// Extract key from map body if present
|
|
342
|
+
const keyMatch = result.mapBody?.match(/key\s*=\s*\{([^}]+)\}|key\s*=\s*"([^"]+)"|key\s*=\s*'([^']+)'/)
|
|
343
|
+
if (keyMatch) {
|
|
344
|
+
result.keyExpression = (keyMatch[1] || keyMatch[2] || keyMatch[3])?.trim()
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
// Try alternate pattern without parens around params
|
|
348
|
+
const altMatch = expr.match(/^(.+?)\s*\.\s*map\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>\s*(.+)\)$/s)
|
|
349
|
+
if (altMatch) {
|
|
350
|
+
result.arraySource = altMatch[1]?.trim()
|
|
351
|
+
result.itemName = altMatch[2]?.trim()
|
|
352
|
+
result.mapBody = altMatch[3]?.trim()
|
|
353
|
+
|
|
354
|
+
// Extract key
|
|
355
|
+
const keyMatch = result.mapBody?.match(/key\s*=\s*\{([^}]+)\}|key\s*=\s*"([^"]+)"|key\s*=\s*'([^']+)'/)
|
|
356
|
+
if (keyMatch) {
|
|
357
|
+
result.keyExpression = (keyMatch[1] || keyMatch[2] || keyMatch[3])?.trim()
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Extract a balanced expression starting at position i in html
|
|
365
|
+
* Returns the content inside { } and the end position
|
|
366
|
+
*/
|
|
367
|
+
function extractBalancedExpression(html: string, startIndex: number): { content: string; endIndex: number } | null {
|
|
368
|
+
if (html[startIndex] !== '{') return null
|
|
369
|
+
|
|
370
|
+
let depth = 1
|
|
371
|
+
let i = startIndex + 1
|
|
372
|
+
let inString = false
|
|
373
|
+
let stringChar = ''
|
|
374
|
+
|
|
375
|
+
while (i < html.length && depth > 0) {
|
|
376
|
+
const char = html[i]
|
|
377
|
+
const prevChar = i > 0 ? html[i - 1] : ''
|
|
378
|
+
|
|
379
|
+
// Handle string literals
|
|
380
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
381
|
+
inString = true
|
|
382
|
+
stringChar = char
|
|
383
|
+
i++
|
|
384
|
+
continue
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (inString) {
|
|
388
|
+
if (char === stringChar && prevChar !== '\\') {
|
|
389
|
+
inString = false
|
|
390
|
+
}
|
|
391
|
+
i++
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Track brace depth
|
|
396
|
+
if (char === '{') {
|
|
397
|
+
depth++
|
|
398
|
+
} else if (char === '}') {
|
|
399
|
+
depth--
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
i++
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (depth !== 0) return null
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
content: html.substring(startIndex + 1, i - 1),
|
|
409
|
+
endIndex: i
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Extract all expression blocks from HTML content
|
|
415
|
+
*/
|
|
416
|
+
export function extractExpressionBlocks(
|
|
417
|
+
html: string,
|
|
418
|
+
declaredStates: Set<string>
|
|
419
|
+
): ExpressionBlock[] {
|
|
420
|
+
const blocks: ExpressionBlock[] = []
|
|
421
|
+
let placeholderCounter = 0
|
|
422
|
+
|
|
423
|
+
// First, mark regions to skip (script, style)
|
|
424
|
+
const skipRegions: Array<{ start: number; end: number }> = []
|
|
425
|
+
|
|
426
|
+
let match
|
|
427
|
+
const scriptRegex = /<script[^>]*>[\s\S]*?<\/script>/gi
|
|
428
|
+
while ((match = scriptRegex.exec(html)) !== null) {
|
|
429
|
+
skipRegions.push({ start: match.index, end: match.index + match[0].length })
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const styleRegex = /<style[^>]*>[\s\S]*?<\/style>/gi
|
|
433
|
+
while ((match = styleRegex.exec(html)) !== null) {
|
|
434
|
+
skipRegions.push({ start: match.index, end: match.index + match[0].length })
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Find all opening braces and extract balanced expressions
|
|
438
|
+
for (let i = 0; i < html.length; i++) {
|
|
439
|
+
if (html[i] !== '{') continue
|
|
440
|
+
|
|
441
|
+
// Check if this is in a skip region
|
|
442
|
+
const inSkipRegion = skipRegions.some(region =>
|
|
443
|
+
i >= region.start && i < region.end
|
|
444
|
+
)
|
|
445
|
+
if (inSkipRegion) continue
|
|
446
|
+
|
|
447
|
+
// Check if this is inside an attribute value
|
|
448
|
+
// Look backwards for attribute pattern: attrName="... or attrName='...
|
|
449
|
+
let j = i - 1
|
|
450
|
+
let inAttrValue = false
|
|
451
|
+
while (j >= 0 && html[j] !== '<' && html[j] !== '>') {
|
|
452
|
+
if (html[j] === '"' || html[j] === "'") {
|
|
453
|
+
// Found a quote, check if there's an = before it
|
|
454
|
+
let k = j - 1
|
|
455
|
+
while (k >= 0 && (html[k] === ' ' || html[k] === '\t' || html[k] === '\n')) k--
|
|
456
|
+
if (k >= 0 && html[k] === '=') {
|
|
457
|
+
inAttrValue = true
|
|
458
|
+
break
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
j--
|
|
462
|
+
}
|
|
463
|
+
if (inAttrValue) continue
|
|
464
|
+
|
|
465
|
+
// Extract balanced expression
|
|
466
|
+
const extracted = extractBalancedExpression(html, i)
|
|
467
|
+
if (!extracted) continue
|
|
468
|
+
|
|
469
|
+
const exprContent = extracted.content
|
|
470
|
+
const trimmed = exprContent.trim()
|
|
471
|
+
|
|
472
|
+
// Skip empty expressions
|
|
473
|
+
if (!trimmed) continue
|
|
474
|
+
|
|
475
|
+
// Skip simple state references - they're handled by the existing binding system
|
|
476
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmed) && declaredStates.has(trimmed)) {
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const expression = parseExpression(exprContent, declaredStates)
|
|
481
|
+
|
|
482
|
+
// Only process non-static expressions (static ones are simple bindings)
|
|
483
|
+
if (expression.type !== 'static' || expression.hasJSX) {
|
|
484
|
+
blocks.push({
|
|
485
|
+
startIndex: i,
|
|
486
|
+
endIndex: extracted.endIndex,
|
|
487
|
+
expression,
|
|
488
|
+
placeholderId: `zen-expr-${placeholderCounter++}`
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Skip to end of this expression
|
|
493
|
+
i = extracted.endIndex - 1
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return blocks
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Convert JSX-like syntax to DOM creation code
|
|
501
|
+
*/
|
|
502
|
+
export function jsxToCreateElement(jsx: string): string {
|
|
503
|
+
// Simple JSX to createElement conversion
|
|
504
|
+
// <div className="test">content</div> -> createElement('div', { className: 'test' }, 'content')
|
|
505
|
+
|
|
506
|
+
// Handle self-closing tags
|
|
507
|
+
jsx = jsx.replace(/<([a-zA-Z][a-zA-Z0-9]*)\s*([^>]*?)\/>/g, (_, tag, attrs) => {
|
|
508
|
+
return `<${tag} ${attrs}></${tag}>`
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
// Parse and convert
|
|
512
|
+
return `__zen_jsx(${JSON.stringify(jsx)})`
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Generate runtime code for an expression
|
|
517
|
+
*/
|
|
518
|
+
export function generateExpressionRuntime(expr: ParsedExpression, placeholderId: string): string {
|
|
519
|
+
switch (expr.type) {
|
|
520
|
+
case 'conditional':
|
|
521
|
+
return generateConditionalRuntime(expr, placeholderId)
|
|
522
|
+
case 'ternary':
|
|
523
|
+
return generateTernaryRuntime(expr, placeholderId)
|
|
524
|
+
case 'map':
|
|
525
|
+
return generateMapRuntime(expr, placeholderId)
|
|
526
|
+
case 'complex':
|
|
527
|
+
return generateComplexRuntime(expr, placeholderId)
|
|
528
|
+
default:
|
|
529
|
+
return ''
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function generateConditionalRuntime(expr: ParsedExpression, placeholderId: string): string {
|
|
534
|
+
const condition = expr.condition || 'false'
|
|
535
|
+
const trueBranch = expr.trueBranch || ''
|
|
536
|
+
|
|
537
|
+
// Escape the trueBranch for JSON
|
|
538
|
+
const escapedBranch = JSON.stringify(trueBranch)
|
|
539
|
+
|
|
540
|
+
// Use window.__zen_eval_expr to evaluate condition in global scope
|
|
541
|
+
const conditionCode = `window.__zen_eval_expr(${JSON.stringify(condition)})`
|
|
542
|
+
|
|
543
|
+
return `
|
|
544
|
+
// Conditional expression: ${condition.replace(/\n/g, ' ')} && ...
|
|
545
|
+
(function() {
|
|
546
|
+
const placeholder = document.querySelector('[data-zen-expr="${placeholderId}"]');
|
|
547
|
+
if (!placeholder) return;
|
|
548
|
+
|
|
549
|
+
let currentElement = null;
|
|
550
|
+
|
|
551
|
+
function updateConditional() {
|
|
552
|
+
try {
|
|
553
|
+
const show = Boolean(${conditionCode});
|
|
554
|
+
|
|
555
|
+
if (show && !currentElement) {
|
|
556
|
+
// Create and insert the element
|
|
557
|
+
currentElement = window.__zen_parse_jsx(${escapedBranch});
|
|
558
|
+
if (currentElement && placeholder.parentNode) {
|
|
559
|
+
placeholder.parentNode.insertBefore(currentElement, placeholder.nextSibling);
|
|
560
|
+
}
|
|
561
|
+
} else if (!show && currentElement) {
|
|
562
|
+
// Remove the element
|
|
563
|
+
if (currentElement.parentNode) {
|
|
564
|
+
currentElement.parentNode.removeChild(currentElement);
|
|
565
|
+
}
|
|
566
|
+
currentElement = null;
|
|
567
|
+
}
|
|
568
|
+
} catch (e) {
|
|
569
|
+
console.warn('[Zenith] Conditional evaluation error:', e);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Initial render after DOM is ready
|
|
574
|
+
if (document.readyState === 'loading') {
|
|
575
|
+
document.addEventListener('DOMContentLoaded', updateConditional);
|
|
576
|
+
} else {
|
|
577
|
+
setTimeout(updateConditional, 0);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Register for state updates
|
|
581
|
+
window.__zen_register_expression_update('${placeholderId}', updateConditional, ${JSON.stringify(expr.dependencies)});
|
|
582
|
+
})();
|
|
583
|
+
`
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function generateTernaryRuntime(expr: ParsedExpression, placeholderId: string): string {
|
|
587
|
+
const condition = expr.condition || 'false'
|
|
588
|
+
let trueBranch = expr.trueBranch || '""'
|
|
589
|
+
let falseBranch = expr.falseBranch || '""'
|
|
590
|
+
|
|
591
|
+
// Check if branches are string literals or expressions
|
|
592
|
+
const trueIsString = /^["']/.test(trueBranch.trim())
|
|
593
|
+
const falseIsString = /^["']/.test(falseBranch.trim())
|
|
594
|
+
|
|
595
|
+
// If branches contain JSX, wrap them for parsing
|
|
596
|
+
const trueHasJSX = /<[a-zA-Z]/.test(trueBranch)
|
|
597
|
+
const falseHasJSX = /<[a-zA-Z]/.test(falseBranch)
|
|
598
|
+
|
|
599
|
+
// Use window.__zen_eval_expr to evaluate expressions in global scope
|
|
600
|
+
const conditionCode = `window.__zen_eval_expr(${JSON.stringify(condition)})`
|
|
601
|
+
const trueBranchCode = trueHasJSX
|
|
602
|
+
? `window.__zen_parse_jsx(${JSON.stringify(trueBranch)})`
|
|
603
|
+
: trueIsString
|
|
604
|
+
? trueBranch
|
|
605
|
+
: `window.__zen_eval_expr(${JSON.stringify(trueBranch)})`
|
|
606
|
+
const falseBranchCode = falseHasJSX
|
|
607
|
+
? `window.__zen_parse_jsx(${JSON.stringify(falseBranch)})`
|
|
608
|
+
: falseIsString
|
|
609
|
+
? falseBranch
|
|
610
|
+
: `window.__zen_eval_expr(${JSON.stringify(falseBranch)})`
|
|
611
|
+
|
|
612
|
+
return `
|
|
613
|
+
// Ternary expression: ${condition.replace(/\n/g, ' ')} ? ... : ...
|
|
614
|
+
(function() {
|
|
615
|
+
const placeholder = document.querySelector('[data-zen-expr="${placeholderId}"]');
|
|
616
|
+
if (!placeholder) return;
|
|
617
|
+
|
|
618
|
+
function updateTernary() {
|
|
619
|
+
try {
|
|
620
|
+
const conditionResult = Boolean(${conditionCode});
|
|
621
|
+
let result = conditionResult ? ${trueBranchCode} : ${falseBranchCode};
|
|
622
|
+
|
|
623
|
+
// Clear placeholder content
|
|
624
|
+
placeholder.innerHTML = '';
|
|
625
|
+
|
|
626
|
+
if (result === null || result === undefined || result === false) {
|
|
627
|
+
// Empty
|
|
628
|
+
} else if (typeof result === 'string' || typeof result === 'number') {
|
|
629
|
+
placeholder.textContent = String(result);
|
|
630
|
+
} else if (result instanceof Node) {
|
|
631
|
+
placeholder.appendChild(result);
|
|
632
|
+
} else if (result && typeof result === 'object') {
|
|
633
|
+
// Try to render as JSX
|
|
634
|
+
const element = window.__zen_parse_jsx(String(result));
|
|
635
|
+
if (element) placeholder.appendChild(element);
|
|
636
|
+
}
|
|
637
|
+
} catch (e) {
|
|
638
|
+
console.warn('[Zenith] Ternary evaluation error:', e);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Initial render after DOM is ready
|
|
643
|
+
if (document.readyState === 'loading') {
|
|
644
|
+
document.addEventListener('DOMContentLoaded', updateTernary);
|
|
645
|
+
} else {
|
|
646
|
+
setTimeout(updateTernary, 0);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Register for state updates
|
|
650
|
+
window.__zen_register_expression_update('${placeholderId}', updateTernary, ${JSON.stringify(expr.dependencies)});
|
|
651
|
+
})();
|
|
652
|
+
`
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function generateMapRuntime(expr: ParsedExpression, placeholderId: string): string {
|
|
656
|
+
const arraySource = expr.arraySource || '[]'
|
|
657
|
+
const itemName = expr.itemName || 'item'
|
|
658
|
+
const indexName = expr.indexName || 'index'
|
|
659
|
+
const mapBody = expr.mapBody || '""'
|
|
660
|
+
const keyExpr = expr.keyExpression || indexName
|
|
661
|
+
|
|
662
|
+
// Process the map body to replace item/index references
|
|
663
|
+
const escapedMapBody = JSON.stringify(mapBody)
|
|
664
|
+
|
|
665
|
+
return `
|
|
666
|
+
// Map expression: ${arraySource}.map(${itemName} => ...)
|
|
667
|
+
(function() {
|
|
668
|
+
const placeholder = document.querySelector('[data-zen-expr="${placeholderId}"]');
|
|
669
|
+
if (!placeholder) return;
|
|
670
|
+
|
|
671
|
+
const itemCache = new Map(); // key -> element
|
|
672
|
+
const mapBodyTemplate = ${escapedMapBody};
|
|
673
|
+
|
|
674
|
+
function updateMap() {
|
|
675
|
+
try {
|
|
676
|
+
const array = window.__zen_eval_expr(${JSON.stringify(arraySource)});
|
|
677
|
+
if (!Array.isArray(array)) {
|
|
678
|
+
console.warn('[Zenith] Map source is not an array:', array);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const newKeys = new Set();
|
|
683
|
+
const parent = placeholder.parentNode;
|
|
684
|
+
if (!parent) return;
|
|
685
|
+
|
|
686
|
+
// Create fragment for new elements
|
|
687
|
+
const elementsInOrder = [];
|
|
688
|
+
|
|
689
|
+
array.forEach(function(${itemName}, ${indexName}) {
|
|
690
|
+
const key = String(${keyExpr.replace(/`/g, '\\`').replace(/\${/g, '\\${')});
|
|
691
|
+
newKeys.add(key);
|
|
692
|
+
|
|
693
|
+
// Create context for template processing
|
|
694
|
+
const context = {};
|
|
695
|
+
context['${itemName}'] = ${itemName};
|
|
696
|
+
context['${indexName}'] = ${indexName};
|
|
697
|
+
|
|
698
|
+
let element = itemCache.get(key);
|
|
699
|
+
if (!element) {
|
|
700
|
+
// Create new element by processing template with context
|
|
701
|
+
element = window.__zen_parse_jsx(mapBodyTemplate, context);
|
|
702
|
+
if (element && element.setAttribute) {
|
|
703
|
+
element.setAttribute('data-zen-key', key);
|
|
704
|
+
element.setAttribute('data-zen-map', '${placeholderId}');
|
|
705
|
+
}
|
|
706
|
+
itemCache.set(key, element);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (element) {
|
|
710
|
+
elementsInOrder.push(element);
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
// Remove old items no longer in array
|
|
715
|
+
for (const [key, element] of itemCache.entries()) {
|
|
716
|
+
if (!newKeys.has(key)) {
|
|
717
|
+
if (element && element.parentNode) {
|
|
718
|
+
element.parentNode.removeChild(element);
|
|
719
|
+
}
|
|
720
|
+
itemCache.delete(key);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Remove all existing mapped elements for this expression
|
|
725
|
+
const existingMapped = parent.querySelectorAll('[data-zen-map="${placeholderId}"]');
|
|
726
|
+
existingMapped.forEach(function(el) {
|
|
727
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Insert elements in order after placeholder
|
|
731
|
+
let insertPoint = placeholder;
|
|
732
|
+
for (const el of elementsInOrder) {
|
|
733
|
+
if (el && insertPoint.parentNode) {
|
|
734
|
+
insertPoint.parentNode.insertBefore(el, insertPoint.nextSibling);
|
|
735
|
+
insertPoint = el;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
} catch (e) {
|
|
739
|
+
console.warn('[Zenith] Map evaluation error:', e);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Initial render after DOM is ready
|
|
744
|
+
if (document.readyState === 'loading') {
|
|
745
|
+
document.addEventListener('DOMContentLoaded', updateMap);
|
|
746
|
+
} else {
|
|
747
|
+
updateMap();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Register for state updates
|
|
751
|
+
window.__zen_register_expression_update('${placeholderId}', updateMap, ${JSON.stringify(expr.dependencies)});
|
|
752
|
+
})();
|
|
753
|
+
`
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function generateComplexRuntime(expr: ParsedExpression, placeholderId: string): string {
|
|
757
|
+
const hasJSX = /<[a-zA-Z]/.test(expr.raw)
|
|
758
|
+
|
|
759
|
+
// For complex expressions, use window.__zen_eval_expr to evaluate in global scope
|
|
760
|
+
// or parse JSX if it contains HTML
|
|
761
|
+
const evalCode = hasJSX
|
|
762
|
+
? `window.__zen_parse_jsx(${JSON.stringify(expr.raw)})`
|
|
763
|
+
: `window.__zen_eval_expr(${JSON.stringify(expr.raw)})`
|
|
764
|
+
|
|
765
|
+
return `
|
|
766
|
+
// Complex expression: ${expr.raw.replace(/\n/g, ' ').substring(0, 50)}...
|
|
767
|
+
(function() {
|
|
768
|
+
const placeholder = document.querySelector('[data-zen-expr="${placeholderId}"]');
|
|
769
|
+
if (!placeholder) return;
|
|
770
|
+
|
|
771
|
+
function updateExpression() {
|
|
772
|
+
try {
|
|
773
|
+
const result = ${evalCode};
|
|
774
|
+
|
|
775
|
+
// Clear current content
|
|
776
|
+
placeholder.innerHTML = '';
|
|
777
|
+
|
|
778
|
+
if (result === null || result === undefined || result === false) {
|
|
779
|
+
// Empty - leave placeholder empty
|
|
780
|
+
} else if (typeof result === 'string' || typeof result === 'number') {
|
|
781
|
+
placeholder.textContent = String(result);
|
|
782
|
+
} else if (result instanceof Node) {
|
|
783
|
+
placeholder.appendChild(result);
|
|
784
|
+
} else if (Array.isArray(result)) {
|
|
785
|
+
// Handle array of elements
|
|
786
|
+
result.forEach(function(item) {
|
|
787
|
+
if (item instanceof Node) {
|
|
788
|
+
placeholder.appendChild(item);
|
|
789
|
+
} else if (typeof item === 'string' || typeof item === 'number') {
|
|
790
|
+
placeholder.appendChild(document.createTextNode(String(item)));
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
} else if (result && typeof result === 'object') {
|
|
794
|
+
// Try to render as JSX string
|
|
795
|
+
const element = window.__zen_parse_jsx(String(result));
|
|
796
|
+
if (element) placeholder.appendChild(element);
|
|
797
|
+
}
|
|
798
|
+
} catch (e) {
|
|
799
|
+
console.warn('[Zenith] Expression evaluation error:', e);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Initial render after DOM is ready
|
|
804
|
+
if (document.readyState === 'loading') {
|
|
805
|
+
document.addEventListener('DOMContentLoaded', updateExpression);
|
|
806
|
+
} else {
|
|
807
|
+
setTimeout(updateExpression, 0);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Register for state updates
|
|
811
|
+
window.__zen_register_expression_update('${placeholderId}', updateExpression, ${JSON.stringify(expr.dependencies)});
|
|
812
|
+
})();
|
|
813
|
+
`
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Transform HTML to replace expression blocks with placeholders
|
|
818
|
+
*/
|
|
819
|
+
export function transformExpressionBlocks(
|
|
820
|
+
html: string,
|
|
821
|
+
blocks: ExpressionBlock[]
|
|
822
|
+
): string {
|
|
823
|
+
if (blocks.length === 0) return html
|
|
824
|
+
|
|
825
|
+
// Sort blocks by start index in reverse order (to avoid index shifting)
|
|
826
|
+
const sortedBlocks = [...blocks].sort((a, b) => b.startIndex - a.startIndex)
|
|
827
|
+
|
|
828
|
+
let result = html
|
|
829
|
+
for (const block of sortedBlocks) {
|
|
830
|
+
const placeholder = `<span data-zen-expr="${block.placeholderId}" style="display:contents;"></span>`
|
|
831
|
+
result = result.substring(0, block.startIndex) + placeholder + result.substring(block.endIndex)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return result
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Attribute expression binding
|
|
839
|
+
*/
|
|
840
|
+
export interface AttributeExpressionBinding {
|
|
841
|
+
elementSelector: string // Selector to find the element
|
|
842
|
+
attributeName: string // Name of the attribute (class, src, href, etc.)
|
|
843
|
+
expression: string // The expression to evaluate
|
|
844
|
+
dependencies: string[] // State variables this expression depends on
|
|
845
|
+
bindingId: string // Unique binding ID
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Extract attribute expressions from HTML
|
|
850
|
+
* Handles: attr={expression}, className={expr ? "a" : "b"}, src={user.avatarUrl}
|
|
851
|
+
*/
|
|
852
|
+
export function extractAttributeExpressions(
|
|
853
|
+
html: string,
|
|
854
|
+
declaredStates: Set<string>
|
|
855
|
+
): { transformedHtml: string; bindings: AttributeExpressionBinding[] } {
|
|
856
|
+
const bindings: AttributeExpressionBinding[] = []
|
|
857
|
+
let bindingCounter = 0
|
|
858
|
+
|
|
859
|
+
// Skip script and style content
|
|
860
|
+
const skipRegions: Array<{ start: number; end: number }> = []
|
|
861
|
+
|
|
862
|
+
let match
|
|
863
|
+
const scriptRegex = /<script[^>]*>[\s\S]*?<\/script>/gi
|
|
864
|
+
while ((match = scriptRegex.exec(html)) !== null) {
|
|
865
|
+
skipRegions.push({ start: match.index, end: match.index + match[0].length })
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const styleRegex = /<style[^>]*>[\s\S]*?<\/style>/gi
|
|
869
|
+
while ((match = styleRegex.exec(html)) !== null) {
|
|
870
|
+
skipRegions.push({ start: match.index, end: match.index + match[0].length })
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Match attributes with expression values: attr={...}
|
|
874
|
+
// This includes: class={}, className={}, src={}, href={}, disabled={}, etc.
|
|
875
|
+
const attrExprRegex = /(\s+)([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*\{([^}]+)\}/g
|
|
876
|
+
|
|
877
|
+
let transformedHtml = html
|
|
878
|
+
const replacements: Array<{ start: number; end: number; replacement: string; binding: AttributeExpressionBinding }> = []
|
|
879
|
+
|
|
880
|
+
while ((match = attrExprRegex.exec(html)) !== null) {
|
|
881
|
+
const start = match.index
|
|
882
|
+
const end = start + match[0].length
|
|
883
|
+
|
|
884
|
+
// Check if in skip region
|
|
885
|
+
const shouldSkip = skipRegions.some(region =>
|
|
886
|
+
start >= region.start && start < region.end
|
|
887
|
+
)
|
|
888
|
+
if (shouldSkip) continue
|
|
889
|
+
|
|
890
|
+
const whitespace = match[1] || ' '
|
|
891
|
+
const attrName = match[2]
|
|
892
|
+
const expression = match[3]?.trim()
|
|
893
|
+
|
|
894
|
+
if (!attrName || !expression) continue
|
|
895
|
+
|
|
896
|
+
// Skip :class and :value (handled by existing binding system)
|
|
897
|
+
if (attrName === ':class' || attrName === ':value') continue
|
|
898
|
+
|
|
899
|
+
// Skip style attribute with object syntax for now
|
|
900
|
+
if (attrName === 'style' && expression.startsWith('{')) continue
|
|
901
|
+
|
|
902
|
+
// Find dependencies in the expression
|
|
903
|
+
const dependencies: string[] = []
|
|
904
|
+
for (const state of declaredStates) {
|
|
905
|
+
const stateRegex = new RegExp(`\\b${state}\\b`, 'g')
|
|
906
|
+
if (stateRegex.test(expression)) {
|
|
907
|
+
dependencies.push(state)
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Only create binding if there are dependencies (dynamic expression)
|
|
912
|
+
if (dependencies.length > 0) {
|
|
913
|
+
const bindingId = `zen-attr-${bindingCounter++}`
|
|
914
|
+
|
|
915
|
+
// Map className to class
|
|
916
|
+
const normalizedAttrName = attrName === 'className' ? 'class' : attrName
|
|
917
|
+
|
|
918
|
+
const binding: AttributeExpressionBinding = {
|
|
919
|
+
elementSelector: `[data-zen-attr-bind="${bindingId}"]`,
|
|
920
|
+
attributeName: normalizedAttrName,
|
|
921
|
+
expression,
|
|
922
|
+
dependencies,
|
|
923
|
+
bindingId
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
bindings.push(binding)
|
|
927
|
+
|
|
928
|
+
// Replace with static attribute and add binding marker
|
|
929
|
+
// For initial value, we'll evaluate it at runtime
|
|
930
|
+
const replacement = `${whitespace}data-zen-attr-bind="${bindingId}" data-zen-attr-name="${normalizedAttrName}" data-zen-attr-expr="${escapeAttrValue(expression)}"`
|
|
931
|
+
|
|
932
|
+
replacements.push({ start, end, replacement, binding })
|
|
933
|
+
} else {
|
|
934
|
+
// Static expression - just evaluate and inline
|
|
935
|
+
// For now, leave as-is and let the browser handle it
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Apply replacements in reverse order to preserve indices
|
|
940
|
+
replacements.sort((a, b) => b.start - a.start)
|
|
941
|
+
for (const r of replacements) {
|
|
942
|
+
transformedHtml = transformedHtml.substring(0, r.start) + r.replacement + transformedHtml.substring(r.end)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return { transformedHtml, bindings }
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Escape attribute value for HTML
|
|
950
|
+
*/
|
|
951
|
+
function escapeAttrValue(value: string): string {
|
|
952
|
+
return value
|
|
953
|
+
.replace(/&/g, '&')
|
|
954
|
+
.replace(/"/g, '"')
|
|
955
|
+
.replace(/'/g, ''')
|
|
956
|
+
.replace(/</g, '<')
|
|
957
|
+
.replace(/>/g, '>')
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Generate runtime code for attribute expression bindings
|
|
962
|
+
*/
|
|
963
|
+
export function generateAttributeExpressionRuntime(bindings: AttributeExpressionBinding[]): string {
|
|
964
|
+
if (bindings.length === 0) return ''
|
|
965
|
+
|
|
966
|
+
const bindingCodes = bindings.map(binding => {
|
|
967
|
+
const isBoolean = ['disabled', 'checked', 'readonly', 'required', 'hidden'].includes(binding.attributeName)
|
|
968
|
+
const isClass = binding.attributeName === 'class'
|
|
969
|
+
|
|
970
|
+
return `
|
|
971
|
+
// Attribute binding: ${binding.attributeName}={${binding.expression.substring(0, 30)}...}
|
|
972
|
+
(function() {
|
|
973
|
+
const el = document.querySelector('[data-zen-attr-bind="${binding.bindingId}"]');
|
|
974
|
+
if (!el) return;
|
|
975
|
+
|
|
976
|
+
function updateAttribute() {
|
|
977
|
+
try {
|
|
978
|
+
const value = window.__zen_eval_expr(${JSON.stringify(binding.expression)});
|
|
979
|
+
${isBoolean ? `
|
|
980
|
+
// Boolean attribute
|
|
981
|
+
if (value) {
|
|
982
|
+
el.setAttribute('${binding.attributeName}', '');
|
|
983
|
+
} else {
|
|
984
|
+
el.removeAttribute('${binding.attributeName}');
|
|
985
|
+
}` : isClass ? `
|
|
986
|
+
// Class attribute - handle string, object, or array
|
|
987
|
+
if (typeof value === 'string') {
|
|
988
|
+
el.className = value;
|
|
989
|
+
} else if (Array.isArray(value)) {
|
|
990
|
+
el.className = value.filter(Boolean).join(' ');
|
|
991
|
+
} else if (value && typeof value === 'object') {
|
|
992
|
+
el.className = Object.entries(value)
|
|
993
|
+
.filter(([_, v]) => v)
|
|
994
|
+
.map(([k]) => k)
|
|
995
|
+
.join(' ');
|
|
996
|
+
} else if (value === null || value === undefined || value === false) {
|
|
997
|
+
el.className = '';
|
|
998
|
+
} else {
|
|
999
|
+
el.className = String(value);
|
|
1000
|
+
}` : `
|
|
1001
|
+
// Regular attribute
|
|
1002
|
+
if (value === null || value === undefined || value === false) {
|
|
1003
|
+
el.removeAttribute('${binding.attributeName}');
|
|
1004
|
+
} else {
|
|
1005
|
+
el.setAttribute('${binding.attributeName}', String(value));
|
|
1006
|
+
}`}
|
|
1007
|
+
} catch (e) {
|
|
1008
|
+
console.warn('[Zenith] Attribute expression error:', e);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Initial update
|
|
1013
|
+
if (document.readyState === 'loading') {
|
|
1014
|
+
document.addEventListener('DOMContentLoaded', updateAttribute);
|
|
1015
|
+
} else {
|
|
1016
|
+
updateAttribute();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Register for state updates
|
|
1020
|
+
window.__zen_register_expression_update('${binding.bindingId}', updateAttribute, ${JSON.stringify(binding.dependencies)});
|
|
1021
|
+
})();`
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
return `
|
|
1025
|
+
// Attribute Expression Bindings
|
|
1026
|
+
${bindingCodes.join('\n')}
|
|
1027
|
+
`
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Generate the expression runtime helper code
|
|
1032
|
+
*/
|
|
1033
|
+
export function generateExpressionRuntimeHelpers(): string {
|
|
1034
|
+
return `
|
|
1035
|
+
// Zenith Dynamic Expression Runtime
|
|
1036
|
+
(function() {
|
|
1037
|
+
// Expression update registry
|
|
1038
|
+
const expressionUpdaters = new Map(); // placeholderId -> { update: fn, dependencies: string[] }
|
|
1039
|
+
|
|
1040
|
+
// Register an expression updater
|
|
1041
|
+
window.__zen_register_expression_update = function(placeholderId, updateFn, dependencies) {
|
|
1042
|
+
expressionUpdaters.set(placeholderId, { update: updateFn, dependencies });
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
// Trigger updates for expressions that depend on a state
|
|
1046
|
+
window.__zen_trigger_expression_updates = function(stateName) {
|
|
1047
|
+
for (const [id, info] of expressionUpdaters.entries()) {
|
|
1048
|
+
// Check if this expression depends on the changed state
|
|
1049
|
+
const deps = info.dependencies;
|
|
1050
|
+
// Also check for partial matches (instance-scoped state)
|
|
1051
|
+
const shouldUpdate = deps.some(dep =>
|
|
1052
|
+
dep === stateName ||
|
|
1053
|
+
stateName.includes(dep) ||
|
|
1054
|
+
dep.includes(stateName)
|
|
1055
|
+
);
|
|
1056
|
+
if (shouldUpdate) {
|
|
1057
|
+
try {
|
|
1058
|
+
info.update();
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
console.warn('[Zenith] Expression update error:', e);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
// Evaluate expression in global scope (accesses window properties)
|
|
1067
|
+
window.__zen_eval_expr = function(expr) {
|
|
1068
|
+
try {
|
|
1069
|
+
// Use Function constructor with 'with(window)' to access window properties
|
|
1070
|
+
// This allows expressions like 'users.length' to access window.users
|
|
1071
|
+
return (new Function('with(window) { return (' + expr + '); }'))();
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
console.warn('[Zenith] Expression evaluation error:', expr, e);
|
|
1074
|
+
return undefined;
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// Parse JSX/HTML string into DOM element(s)
|
|
1079
|
+
window.__zen_parse_jsx = function(jsx, context) {
|
|
1080
|
+
if (jsx === null || jsx === undefined || jsx === false) return null;
|
|
1081
|
+
if (typeof jsx === 'number') jsx = String(jsx);
|
|
1082
|
+
if (typeof jsx !== 'string') return null;
|
|
1083
|
+
|
|
1084
|
+
// Apply context (for map iterations)
|
|
1085
|
+
let processed = jsx;
|
|
1086
|
+
if (context) {
|
|
1087
|
+
for (const [key, value] of Object.entries(context)) {
|
|
1088
|
+
// Replace {key} with value in text content
|
|
1089
|
+
const textRegex = new RegExp('\\\\{\\\\s*' + key + '\\\\s*\\\\}', 'g');
|
|
1090
|
+
const safeValue = value === null || value === undefined ? '' : String(value);
|
|
1091
|
+
processed = processed.replace(textRegex, safeValue);
|
|
1092
|
+
|
|
1093
|
+
// Replace key references in attribute expressions
|
|
1094
|
+
const attrRegex = new RegExp('\\\\{' + key + '\\\\}', 'g');
|
|
1095
|
+
processed = processed.replace(attrRegex, safeValue);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Parse HTML string
|
|
1100
|
+
const template = document.createElement('template');
|
|
1101
|
+
template.innerHTML = processed.trim();
|
|
1102
|
+
|
|
1103
|
+
// Return single element or document fragment for multiple
|
|
1104
|
+
if (template.content.childNodes.length === 1) {
|
|
1105
|
+
return template.content.firstChild;
|
|
1106
|
+
}
|
|
1107
|
+
return template.content;
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
// Create element from tag, attributes, and children
|
|
1111
|
+
window.__zen_create_element = function(tag, attrs, ...children) {
|
|
1112
|
+
const el = document.createElement(tag);
|
|
1113
|
+
|
|
1114
|
+
// Set attributes
|
|
1115
|
+
if (attrs) {
|
|
1116
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
1117
|
+
if (key === 'className') {
|
|
1118
|
+
el.className = value;
|
|
1119
|
+
} else if (key === 'style' && typeof value === 'object') {
|
|
1120
|
+
Object.assign(el.style, value);
|
|
1121
|
+
} else if (key.startsWith('on') && typeof value === 'function') {
|
|
1122
|
+
el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
1123
|
+
} else if (value !== null && value !== undefined && value !== false) {
|
|
1124
|
+
el.setAttribute(key, String(value));
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Append children
|
|
1130
|
+
for (const child of children.flat()) {
|
|
1131
|
+
if (child === null || child === undefined || child === false) continue;
|
|
1132
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
1133
|
+
el.appendChild(document.createTextNode(String(child)));
|
|
1134
|
+
} else if (child instanceof Node) {
|
|
1135
|
+
el.appendChild(child);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return el;
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// Track state property access
|
|
1143
|
+
window.__zen_track_state = function(stateName) {
|
|
1144
|
+
// Used for dependency tracking
|
|
1145
|
+
};
|
|
1146
|
+
})();
|
|
1147
|
+
`
|
|
1148
|
+
}
|
|
1149
|
+
|