@zenithbuild/core 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CORE_CONTRACT.md +145 -0
  2. package/README.md +14 -29
  3. package/bin/zenith.js +89 -0
  4. package/package.json +39 -56
  5. package/src/config.js +136 -0
  6. package/src/core-template.js +30 -0
  7. package/src/errors.js +54 -0
  8. package/src/guards.js +61 -0
  9. package/src/hash.js +52 -0
  10. package/src/index.js +26 -0
  11. package/src/ir/index.js +1 -0
  12. package/src/order.js +69 -0
  13. package/src/path.js +131 -0
  14. package/src/schema.js +28 -0
  15. package/src/version.js +67 -0
  16. package/bin/zen-build.ts +0 -2
  17. package/bin/zen-dev.ts +0 -2
  18. package/bin/zen-preview.ts +0 -2
  19. package/bin/zenith.ts +0 -2
  20. package/cli/commands/add.ts +0 -37
  21. package/cli/commands/build.ts +0 -37
  22. package/cli/commands/create.ts +0 -702
  23. package/cli/commands/dev.ts +0 -388
  24. package/cli/commands/index.ts +0 -112
  25. package/cli/commands/preview.ts +0 -62
  26. package/cli/commands/remove.ts +0 -33
  27. package/cli/index.ts +0 -10
  28. package/cli/main.ts +0 -101
  29. package/cli/utils/branding.ts +0 -178
  30. package/cli/utils/content.ts +0 -112
  31. package/cli/utils/logger.ts +0 -46
  32. package/cli/utils/plugin-manager.ts +0 -114
  33. package/cli/utils/project.ts +0 -77
  34. package/compiler/README.md +0 -380
  35. package/compiler/build-analyzer.ts +0 -122
  36. package/compiler/css/index.ts +0 -317
  37. package/compiler/discovery/componentDiscovery.ts +0 -178
  38. package/compiler/discovery/layouts.ts +0 -70
  39. package/compiler/errors/compilerError.ts +0 -56
  40. package/compiler/finalize/finalizeOutput.ts +0 -192
  41. package/compiler/finalize/generateFinalBundle.ts +0 -82
  42. package/compiler/index.ts +0 -83
  43. package/compiler/ir/types.ts +0 -174
  44. package/compiler/output/types.ts +0 -34
  45. package/compiler/parse/detectMapExpressions.ts +0 -102
  46. package/compiler/parse/importTypes.ts +0 -78
  47. package/compiler/parse/parseImports.ts +0 -309
  48. package/compiler/parse/parseScript.ts +0 -46
  49. package/compiler/parse/parseTemplate.ts +0 -599
  50. package/compiler/parse/parseZenFile.ts +0 -66
  51. package/compiler/parse/scriptAnalysis.ts +0 -91
  52. package/compiler/parse/trackLoopContext.ts +0 -82
  53. package/compiler/runtime/dataExposure.ts +0 -317
  54. package/compiler/runtime/generateDOM.ts +0 -246
  55. package/compiler/runtime/generateHydrationBundle.ts +0 -407
  56. package/compiler/runtime/hydration.ts +0 -309
  57. package/compiler/runtime/navigation.ts +0 -432
  58. package/compiler/runtime/thinRuntime.ts +0 -160
  59. package/compiler/runtime/transformIR.ts +0 -370
  60. package/compiler/runtime/wrapExpression.ts +0 -95
  61. package/compiler/runtime/wrapExpressionWithLoop.ts +0 -83
  62. package/compiler/spa-build.ts +0 -917
  63. package/compiler/ssg-build.ts +0 -422
  64. package/compiler/test/validate-test.ts +0 -104
  65. package/compiler/transform/classifyExpression.ts +0 -444
  66. package/compiler/transform/componentResolver.ts +0 -312
  67. package/compiler/transform/componentScriptTransformer.ts +0 -303
  68. package/compiler/transform/expressionTransformer.ts +0 -385
  69. package/compiler/transform/fragmentLowering.ts +0 -634
  70. package/compiler/transform/generateBindings.ts +0 -47
  71. package/compiler/transform/generateHTML.ts +0 -28
  72. package/compiler/transform/layoutProcessor.ts +0 -132
  73. package/compiler/transform/slotResolver.ts +0 -292
  74. package/compiler/transform/transformNode.ts +0 -126
  75. package/compiler/transform/transformTemplate.ts +0 -38
  76. package/compiler/validate/invariants.ts +0 -292
  77. package/compiler/validate/validateExpressions.ts +0 -168
  78. package/core/config/index.ts +0 -16
  79. package/core/config/loader.ts +0 -69
  80. package/core/config/types.ts +0 -89
  81. package/core/index.ts +0 -135
  82. package/core/lifecycle/index.ts +0 -49
  83. package/core/lifecycle/zen-mount.ts +0 -182
  84. package/core/lifecycle/zen-unmount.ts +0 -88
  85. package/core/plugins/index.ts +0 -7
  86. package/core/plugins/registry.ts +0 -81
  87. package/core/reactivity/index.ts +0 -54
  88. package/core/reactivity/tracking.ts +0 -167
  89. package/core/reactivity/zen-batch.ts +0 -57
  90. package/core/reactivity/zen-effect.ts +0 -139
  91. package/core/reactivity/zen-memo.ts +0 -146
  92. package/core/reactivity/zen-ref.ts +0 -52
  93. package/core/reactivity/zen-signal.ts +0 -121
  94. package/core/reactivity/zen-state.ts +0 -180
  95. package/core/reactivity/zen-untrack.ts +0 -44
  96. package/dist/cli.js +0 -11665
  97. package/dist/zen-build.js +0 -21172
  98. package/dist/zen-dev.js +0 -21172
  99. package/dist/zen-preview.js +0 -21172
  100. package/dist/zenith.js +0 -21172
  101. package/router/index.ts +0 -28
  102. package/router/manifest.ts +0 -314
  103. package/router/navigation/ZenLink.zen +0 -231
  104. package/router/navigation/index.ts +0 -78
  105. package/router/navigation/zen-link.ts +0 -584
  106. package/router/runtime.ts +0 -458
  107. package/router/types.ts +0 -168
  108. package/runtime/build.ts +0 -17
  109. package/runtime/bundle-generator.ts +0 -1247
  110. package/runtime/client-runtime.ts +0 -549
  111. package/runtime/serve.ts +0 -93
  112. package/tsconfig.json +0 -28
@@ -1,132 +0,0 @@
1
- import type { LayoutMetadata } from '../discovery/layouts'
2
-
3
- /**
4
- * Process a page by inlining a layout
5
- */
6
- export function processLayout(
7
- source: string,
8
- layout: LayoutMetadata,
9
- props: Record<string, any> = {}
10
- ): string {
11
- // 1. Extract scripts and styles from the page source
12
- const pageScripts: string[] = []
13
- const pageStyles: string[] = []
14
- let isTypeScript = false
15
-
16
- // Extract script blocks
17
- const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi
18
- let scriptMatch
19
- while ((scriptMatch = scriptRegex.exec(source)) !== null) {
20
- const attrString = scriptMatch[1] || ''
21
- const content = scriptMatch[2] || ''
22
- if (attrString.includes('lang="ts"') || attrString.includes('setup="ts"')) {
23
- isTypeScript = true
24
- }
25
- if (content) pageScripts.push(content.trim())
26
- }
27
-
28
- // Extract style blocks
29
- const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi
30
- let styleMatch
31
- while ((styleMatch = styleRegex.exec(source)) !== null) {
32
- if (styleMatch[1]) pageStyles.push(styleMatch[1].trim())
33
- }
34
-
35
- // 2. Extract content from page source and parse props
36
- const layoutTag = layout.name
37
- // Support both <DefaultLayout ...> and <DefaultLayout>...</DefaultLayout>
38
- const layoutRegex = new RegExp(`<${layoutTag}\\b([^>]*)>(?:([\\s\\S]*?)</${layoutTag}>)?`, 'i')
39
- const match = source.match(layoutRegex)
40
-
41
- let pageHtml = ''
42
- let layoutPropsStr = ''
43
-
44
- if (match) {
45
- layoutPropsStr = match[1] || ''
46
- pageHtml = match[2] || ''
47
-
48
- // If it's a self-closing tag or empty, it might not have captured content correctly if regex failed
49
- if (!pageHtml && !source.includes(`</${layoutTag}>`)) {
50
- // Self-closing check? No, Zenith usually expects explicit tags or the layout to wrap everything.
51
- }
52
- } else {
53
- // If layout tag not found as root, assume everything minus script/style is content
54
- pageHtml = source.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
55
- pageHtml = pageHtml.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '').trim()
56
- }
57
-
58
- // 3. Parse props from the tag
59
- const mergedProps = { ...props }
60
- if (layoutPropsStr) {
61
- // Support legacy props={{...}}
62
- const legacyMatch = layoutPropsStr.match(/props=\{\{([^}]+)\}\}/)
63
- if (legacyMatch && legacyMatch[1]) {
64
- const propsBody = legacyMatch[1]
65
- const pairs = propsBody.split(/,(?![^[]*\])(?![^{]*\})/)
66
- for (const pair of pairs) {
67
- const [key, ...valParts] = pair.split(':')
68
- if (key && valParts.length > 0) {
69
- mergedProps[key.trim()] = valParts.join(':').trim()
70
- }
71
- }
72
- }
73
-
74
- // Support natural props: title={"Home"} or title="Home" or title={title}
75
- const attrRegex = /([a-zA-Z0-9-]+)=(?:\{([^}]+)\}|"([^"]*)"|'([^']*)')/g
76
- let attrMatch
77
- while ((attrMatch = attrRegex.exec(layoutPropsStr)) !== null) {
78
- const name = attrMatch[1]
79
- const value = attrMatch[2] || attrMatch[3] || attrMatch[4]
80
- if (name && name !== 'props') {
81
- mergedProps[name] = value
82
- }
83
- }
84
- }
85
-
86
- // 4. Merge Scripts with Prop Injection
87
- // Layout scripts come first, then page scripts. Props are injected at the very top.
88
- const propDeclarations = Object.entries(mergedProps)
89
- .map(([key, value]) => {
90
- // If value looks like a string literal, keep it as is, otherwise wrap if needed
91
- // Actually, if it came from {expression}, we should treat it as code.
92
- // If it came from "string", we treat it as a string.
93
- const isExpression = layoutPropsStr.includes(`${key}={${value}}`)
94
- if (isExpression) {
95
- return `const ${key} = ${value};`
96
- }
97
- return `const ${key} = ${typeof value === 'string' && !value.startsWith("'") && !value.startsWith('"') ? `'${value}'` : value};`
98
- })
99
- .join('\n')
100
-
101
- const mergedScripts = [
102
- propDeclarations,
103
- ...layout.scripts,
104
- ...pageScripts
105
- ].filter(Boolean).join('\n\n')
106
-
107
- // 5. Merge Styles
108
- const mergedStyles = [
109
- ...layout.styles,
110
- ...pageStyles
111
- ].filter(Boolean).join('\n\n')
112
-
113
- // 6. Inline HTML into layout slot
114
- let finalizedHtml = layout.html.replace(/<Slot\s*\/>/gi, pageHtml)
115
- finalizedHtml = finalizedHtml.replace(/<slot\s*>[\s\S]*?<\/slot>/gi, pageHtml)
116
-
117
- // 7. Reconstruct the full .zen source
118
- const propNames = Object.keys(mergedProps).join(',')
119
- const scriptTag = `<script setup${isTypeScript ? '="ts"' : ''}${propNames ? ` props="${propNames}"` : ''}>`
120
-
121
- return `
122
- ${scriptTag}
123
- ${mergedScripts}
124
- </script>
125
-
126
- ${finalizedHtml}
127
-
128
- <style>
129
- ${mergedStyles}
130
- </style>
131
- `.trim()
132
- }
@@ -1,292 +0,0 @@
1
- /**
2
- * Slot Resolution - Compound Component Model
3
- *
4
- * Resolves slots using compound component pattern (Card.Header, Card.Body)
5
- * NOT template tags. This matches React/Astro semantics.
6
- *
7
- * IMPORTANT: Slot content must preserve the parent reactive scope.
8
- * Components are purely structural transforms - they don't create new reactive boundaries.
9
- *
10
- * Example usage:
11
- * <Card>
12
- * <Card.Header><h3>Title</h3></Card.Header>
13
- * <p>Body content goes to default slot</p>
14
- * <Card.Footer><Button>OK</Button></Card.Footer>
15
- * </Card>
16
- */
17
-
18
- import type { TemplateNode, ComponentNode, ElementNode, LoopContext } from '../ir/types'
19
-
20
- export interface ResolvedSlots {
21
- default: TemplateNode[]
22
- named: Map<string, TemplateNode[]>
23
- // Preserve the parent's reactive scope for slot content
24
- parentLoopContext?: LoopContext
25
- }
26
-
27
- /**
28
- * Extract slots from component children using compound component pattern
29
- *
30
- * Children named `ParentComponent.SlotName` become named slots.
31
- * All other children go to the default slot.
32
- * Preserves the parent's reactive scope (loopContext) for all slot content.
33
- *
34
- * @param parentName - Name of the parent component (e.g., "Card")
35
- * @param children - Child nodes from component usage
36
- * @param parentLoopContext - The reactive scope from the parent (must be preserved)
37
- */
38
- export function extractSlotsFromChildren(
39
- parentName: string,
40
- children: TemplateNode[],
41
- parentLoopContext?: LoopContext
42
- ): ResolvedSlots {
43
- const defaultSlot: TemplateNode[] = []
44
- const namedSlots = new Map<string, TemplateNode[]>()
45
-
46
- for (const child of children) {
47
- // Check if this is a compound component (e.g., Card.Header)
48
- if (child.type === 'component') {
49
- const compoundMatch = parseCompoundName(child.name, parentName)
50
-
51
- if (compoundMatch) {
52
- // This is a named slot (e.g., Card.Header -> "header")
53
- const slotName = compoundMatch.toLowerCase()
54
-
55
- if (!namedSlots.has(slotName)) {
56
- namedSlots.set(slotName, [])
57
- }
58
-
59
- // The compound component's children become the slot content
60
- // Preserve parent's loopContext on each child
61
- const scopedChildren = child.children.map(c =>
62
- rebindNodeToScope(c, parentLoopContext)
63
- )
64
- namedSlots.get(slotName)!.push(...scopedChildren)
65
- } else {
66
- // Regular component, goes to default slot
67
- // Preserve parent's loopContext
68
- defaultSlot.push(rebindNodeToScope(child, parentLoopContext))
69
- }
70
- } else {
71
- // Elements, text, expressions go to default slot
72
- // Preserve parent's loopContext
73
- defaultSlot.push(rebindNodeToScope(child, parentLoopContext))
74
- }
75
- }
76
-
77
- return {
78
- default: defaultSlot,
79
- named: namedSlots,
80
- parentLoopContext
81
- }
82
- }
83
-
84
- /**
85
- * Rebind a node to the parent's reactive scope
86
- *
87
- * This ensures that expressions and event bindings in slot content
88
- * remain connected to the parent component's reactive graph.
89
- * Components must be purely structural - they don't create new reactive boundaries.
90
- */
91
- function rebindNodeToScope(node: TemplateNode, loopContext?: LoopContext): TemplateNode {
92
- // If no parent scope to preserve, return as-is
93
- if (!loopContext) {
94
- return node
95
- }
96
-
97
- // Merge the parent's loopContext with existing loopContext
98
- // Parent scope takes precedence to ensure reactivity flows through
99
- switch (node.type) {
100
- case 'expression':
101
- return {
102
- ...node,
103
- loopContext: mergeLoopContext(node.loopContext, loopContext)
104
- }
105
-
106
- case 'element':
107
- return {
108
- ...node,
109
- loopContext: mergeLoopContext(node.loopContext, loopContext),
110
- attributes: node.attributes.map(attr => ({
111
- ...attr,
112
- loopContext: attr.loopContext
113
- ? mergeLoopContext(attr.loopContext, loopContext)
114
- : loopContext
115
- })),
116
- children: node.children.map(c => rebindNodeToScope(c, loopContext))
117
- }
118
-
119
- case 'component':
120
- return {
121
- ...node,
122
- loopContext: mergeLoopContext(node.loopContext, loopContext),
123
- children: node.children.map(c => rebindNodeToScope(c, loopContext))
124
- }
125
-
126
- case 'text':
127
- // Text nodes don't have reactive bindings
128
- return node
129
-
130
- default:
131
- return node
132
- }
133
- }
134
-
135
- /**
136
- * Merge two loop contexts, combining their variables
137
- * Parent context variables take precedence (added last so they shadow)
138
- */
139
- function mergeLoopContext(existing?: LoopContext, parent?: LoopContext): LoopContext | undefined {
140
- if (!existing && !parent) return undefined
141
- if (!existing) return parent
142
- if (!parent) return existing
143
-
144
- // Combine variables, parent variables shadow existing
145
- const allVars = new Set([...existing.variables, ...parent.variables])
146
-
147
- return {
148
- variables: Array.from(allVars),
149
- mapSource: parent.mapSource || existing.mapSource
150
- }
151
- }
152
-
153
- /**
154
- * Parse compound component name
155
- *
156
- * Given "Card.Header" and parent "Card", returns "Header"
157
- * Given "Card.Footer" and parent "Card", returns "Footer"
158
- * Given "Button" and parent "Card", returns null (not a compound)
159
- *
160
- * @param componentName - Full component name (e.g., "Card.Header")
161
- * @param parentName - Parent component name (e.g., "Card")
162
- * @returns Slot name or null if not a compound of this parent
163
- */
164
- function parseCompoundName(componentName: string, parentName: string): string | null {
165
- const prefix = `${parentName}.`
166
-
167
- if (componentName.startsWith(prefix)) {
168
- return componentName.slice(prefix.length)
169
- }
170
-
171
- return null
172
- }
173
-
174
- /**
175
- * Resolve slots in component template nodes
176
- *
177
- * Replaces <slot /> and <slot name="X" /> with children from resolved slots.
178
- * All slot content is rebound to the parent's reactive scope.
179
- */
180
- export function resolveSlots(
181
- componentNodes: TemplateNode[],
182
- slots: ResolvedSlots
183
- ): TemplateNode[] {
184
- const resolved: TemplateNode[] = []
185
-
186
- for (const node of componentNodes) {
187
- const result = resolveNode(node, slots)
188
- if (Array.isArray(result)) {
189
- resolved.push(...result)
190
- } else {
191
- resolved.push(result)
192
- }
193
- }
194
-
195
- return resolved
196
- }
197
-
198
- /**
199
- * Resolve a single node, replacing slot tags with content
200
- * Ensures all slot content maintains the parent's reactive scope
201
- */
202
- function resolveNode(
203
- node: TemplateNode,
204
- slots: ResolvedSlots
205
- ): TemplateNode | TemplateNode[] {
206
- if (node.type === 'element' && node.tag === 'slot') {
207
- // This is a slot tag - replace it with children
208
- const nameAttr = node.attributes.find(attr => attr.name === 'name')
209
- const slotName = typeof nameAttr?.value === 'string' ? nameAttr.value : null
210
-
211
- if (slotName) {
212
- // Named slot
213
- const namedChildren = slots.named.get(slotName.toLowerCase()) || []
214
-
215
- // If no children provided and slot has fallback content, use fallback
216
- if (namedChildren.length === 0 && node.children.length > 0) {
217
- return node.children
218
- }
219
-
220
- // Return slot content (already scoped during extraction)
221
- return namedChildren.length > 0 ? namedChildren : []
222
- } else {
223
- // Default slot
224
- // If no children provided and slot has fallback content, use fallback
225
- if (slots.default.length === 0 && node.children.length > 0) {
226
- return node.children
227
- }
228
-
229
- // Return slot content (already scoped during extraction)
230
- return slots.default
231
- }
232
- }
233
-
234
- if (node.type === 'element') {
235
- // Recursively resolve slots in children
236
- const resolvedChildren: TemplateNode[] = []
237
- for (const child of node.children) {
238
- const result = resolveNode(child, slots)
239
- if (Array.isArray(result)) {
240
- resolvedChildren.push(...result)
241
- } else {
242
- resolvedChildren.push(result)
243
- }
244
- }
245
-
246
- return {
247
- ...node,
248
- children: resolvedChildren
249
- }
250
- }
251
-
252
- if (node.type === 'component') {
253
- // Recursively resolve slots in component children
254
- const resolvedChildren: TemplateNode[] = []
255
- for (const child of node.children) {
256
- const result = resolveNode(child, slots)
257
- if (Array.isArray(result)) {
258
- resolvedChildren.push(...result)
259
- } else {
260
- resolvedChildren.push(result)
261
- }
262
- }
263
-
264
- return {
265
- ...node,
266
- children: resolvedChildren
267
- }
268
- }
269
-
270
- // Text and expression nodes pass through unchanged
271
- return node
272
- }
273
-
274
- /**
275
- * Check if a node tree contains any slots
276
- */
277
- export function hasSlots(nodes: TemplateNode[]): boolean {
278
- function checkNode(node: TemplateNode): boolean {
279
- if (node.type === 'element') {
280
- if (node.tag === 'slot') {
281
- return true
282
- }
283
- return node.children.some(checkNode)
284
- }
285
- if (node.type === 'component') {
286
- return node.children.some(checkNode)
287
- }
288
- return false
289
- }
290
-
291
- return nodes.some(checkNode)
292
- }
@@ -1,126 +0,0 @@
1
- /**
2
- * Transform Template Nodes
3
- *
4
- * Transforms IR nodes into HTML strings and collects bindings
5
- */
6
-
7
- import type { TemplateNode, ElementNode, TextNode, ExpressionNode, ExpressionIR, LoopContext } from '../ir/types'
8
- import type { Binding } from '../output/types'
9
-
10
- let bindingIdCounter = 0
11
-
12
- function generateBindingId(): string {
13
- return `expr_${bindingIdCounter++}`
14
- }
15
-
16
- /**
17
- * Transform a template node to HTML and collect bindings
18
- * Phase 7: Supports loop context propagation for map expressions
19
- */
20
- export function transformNode(
21
- node: TemplateNode,
22
- expressions: ExpressionIR[],
23
- parentLoopContext?: LoopContext // Phase 7: Loop context from parent map expressions
24
- ): { html: string; bindings: Binding[] } {
25
- const bindings: Binding[] = []
26
-
27
- function transform(node: TemplateNode, loopContext?: LoopContext): string {
28
- switch (node.type) {
29
- case 'text':
30
- return escapeHtml((node as TextNode).value)
31
-
32
- case 'expression': {
33
- const exprNode = node as ExpressionNode
34
- // Find the expression in the expressions array
35
- const expr = expressions.find(e => e.id === exprNode.expression)
36
- if (!expr) {
37
- throw new Error(`Expression ${exprNode.expression} not found`)
38
- }
39
-
40
- const bindingId = expr.id
41
- // Phase 7: Use loop context from ExpressionNode if available, otherwise use passed context
42
- const activeLoopContext = exprNode.loopContext || loopContext
43
-
44
- bindings.push({
45
- id: bindingId,
46
- type: 'text',
47
- target: 'data-zen-text',
48
- expression: expr.code,
49
- location: expr.location,
50
- loopContext: activeLoopContext // Phase 7: Attach loop context to binding
51
- })
52
-
53
- return `<span data-zen-text="${bindingId}" style="display: contents;"></span>`
54
- }
55
-
56
- case 'element': {
57
- const elNode = node as ElementNode
58
- const tag = elNode.tag
59
-
60
- // Build attributes
61
- const attrs: string[] = []
62
- for (const attr of elNode.attributes) {
63
- if (typeof attr.value === 'string') {
64
- // Static attribute
65
- const value = escapeHtml(attr.value)
66
- attrs.push(`${attr.name}="${value}"`)
67
- } else {
68
- // Expression attribute
69
- const expr = attr.value as ExpressionIR
70
- const bindingId = expr.id
71
- // Phase 7: Use loop context from AttributeIR if available, otherwise use element's loop context
72
- const activeLoopContext = attr.loopContext || loopContext
73
-
74
- bindings.push({
75
- id: bindingId,
76
- type: 'attribute',
77
- target: attr.name, // e.g., "class", "style"
78
- expression: expr.code,
79
- location: expr.location,
80
- loopContext: activeLoopContext // Phase 7: Attach loop context to binding
81
- })
82
-
83
- // Use data-zen-attr-{name} for attribute expressions
84
- attrs.push(`data-zen-attr-${attr.name}="${bindingId}"`)
85
- }
86
- }
87
-
88
- const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : ''
89
-
90
- // Phase 7: Use loop context from ElementNode if available, otherwise use passed context
91
- const activeLoopContext = elNode.loopContext || loopContext
92
-
93
- // Transform children
94
- const childrenHtml = elNode.children.map(child => transform(child, activeLoopContext)).join('')
95
-
96
- // Self-closing tags
97
- const voidElements = new Set([
98
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
99
- 'link', 'meta', 'param', 'source', 'track', 'wbr'
100
- ])
101
-
102
- if (voidElements.has(tag.toLowerCase()) && childrenHtml === '') {
103
- return `<${tag}${attrStr} />`
104
- }
105
-
106
- return `<${tag}${attrStr}>${childrenHtml}</${tag}>`
107
- }
108
- }
109
- }
110
-
111
- const html = transform(node, parentLoopContext)
112
- return { html, bindings }
113
- }
114
-
115
- /**
116
- * Escape HTML special characters
117
- */
118
- function escapeHtml(text: string): string {
119
- return text
120
- .replace(/&/g, '&amp;')
121
- .replace(/</g, '&lt;')
122
- .replace(/>/g, '&gt;')
123
- .replace(/"/g, '&quot;')
124
- .replace(/'/g, '&#39;')
125
- }
126
-
@@ -1,38 +0,0 @@
1
- /**
2
- * Transform Template IR to Compiled Template
3
- *
4
- * Phase 2: Transform IR → Static HTML + Runtime Bindings
5
- */
6
-
7
- import type { ZenIR } from '../ir/types'
8
- import type { CompiledTemplate } from '../output/types'
9
- import { generateHTML } from './generateHTML'
10
- import { validateBindings, sortBindings } from './generateBindings'
11
-
12
- /**
13
- * Transform a ZenIR into CompiledTemplate
14
- */
15
- export function transformTemplate(ir: ZenIR): CompiledTemplate {
16
- // Generate HTML and collect bindings
17
- const { html, bindings } = generateHTML(ir.template.nodes, ir.template.expressions)
18
-
19
- // Validate bindings
20
- validateBindings(bindings)
21
-
22
- // Sort bindings by location for deterministic output
23
- const sortedBindings = sortBindings(bindings)
24
-
25
- // Extract scripts (raw content, pass through)
26
- const scripts = ir.script ? ir.script.raw : null
27
-
28
- // Extract styles (raw content, pass through)
29
- const styles = ir.styles.map(s => s.raw)
30
-
31
- return {
32
- html,
33
- bindings: sortedBindings,
34
- scripts,
35
- styles
36
- }
37
- }
38
-