@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,1208 @@
|
|
|
1
|
+
// compiler/component-process.ts
|
|
2
|
+
// Phase 3: Main component processing pipeline
|
|
3
|
+
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import * as parse5 from "parse5";
|
|
7
|
+
import type { ZenFile, ScriptBlock, StyleBlock } from "./types";
|
|
8
|
+
import { parseZen } from "./parse";
|
|
9
|
+
import { discoverComponents, discoverLayouts, type ComponentMetadata } from "./component";
|
|
10
|
+
import { extractStateDeclarations } from "./parse";
|
|
11
|
+
|
|
12
|
+
// Native HTML elements that should never be matched as components
|
|
13
|
+
// This prevents <button> from matching Button.zen, etc.
|
|
14
|
+
const NATIVE_HTML_ELEMENTS = new Set([
|
|
15
|
+
'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio',
|
|
16
|
+
'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button',
|
|
17
|
+
'canvas', 'caption', 'cite', 'code', 'col', 'colgroup',
|
|
18
|
+
'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt',
|
|
19
|
+
'em', 'embed',
|
|
20
|
+
'fieldset', 'figcaption', 'figure', 'footer', 'form',
|
|
21
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html',
|
|
22
|
+
'i', 'iframe', 'img', 'input', 'ins',
|
|
23
|
+
'kbd',
|
|
24
|
+
'label', 'legend', 'li', 'link',
|
|
25
|
+
'main', 'map', 'mark', 'menu', 'meta', 'meter',
|
|
26
|
+
'nav', 'noscript',
|
|
27
|
+
'object', 'ol', 'optgroup', 'option', 'output',
|
|
28
|
+
'p', 'param', 'picture', 'pre', 'progress',
|
|
29
|
+
'q',
|
|
30
|
+
'rp', 'rt', 'ruby',
|
|
31
|
+
's', 'samp', 'script', 'section', 'select', 'slot', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'svg',
|
|
32
|
+
'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track',
|
|
33
|
+
'u', 'ul',
|
|
34
|
+
'var', 'video',
|
|
35
|
+
'wbr'
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
interface ProcessedComponent {
|
|
39
|
+
metadata: ComponentMetadata;
|
|
40
|
+
instanceId: string;
|
|
41
|
+
props: Map<string, string>; // prop name -> value from parent
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Process a ZenFile to inline all component usages and handle layouts
|
|
46
|
+
* Returns a new ZenFile with components inlined
|
|
47
|
+
*/
|
|
48
|
+
export function processComponents(entryFile: ZenFile, entryPath: string): ZenFile {
|
|
49
|
+
const entryDir = path.dirname(entryPath);
|
|
50
|
+
|
|
51
|
+
// For file-based routing: if entry is in pages/, look for components/layouts in parent directory
|
|
52
|
+
// Otherwise, look in the same directory as the entry file
|
|
53
|
+
let baseDir = entryDir;
|
|
54
|
+
if (entryDir.endsWith("pages") || entryDir.endsWith(path.join("app", "pages"))) {
|
|
55
|
+
baseDir = path.dirname(entryDir);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const componentsDir = path.join(baseDir, "components");
|
|
59
|
+
const layoutsDir = path.join(baseDir, "layouts");
|
|
60
|
+
|
|
61
|
+
// Also look for components in router/navigation (for ZenLink)
|
|
62
|
+
// Use routerNavigationDir as the base so component names are derived correctly
|
|
63
|
+
// (e.g., ZenLink.zen -> ZenLink, not router/navigation/ZenLink -> RouterNavigationZenlink)
|
|
64
|
+
const routerNavigationDir = path.join(baseDir, "..", "router", "navigation");
|
|
65
|
+
|
|
66
|
+
// Discover components from both app/components and router/navigation
|
|
67
|
+
const appComponents = discoverComponents(componentsDir, baseDir);
|
|
68
|
+
const routerComponents = discoverComponents(routerNavigationDir, routerNavigationDir);
|
|
69
|
+
|
|
70
|
+
// Merge components (router components take precedence if name conflicts)
|
|
71
|
+
const components = new Map(appComponents);
|
|
72
|
+
for (const [name, metadata] of routerComponents.entries()) {
|
|
73
|
+
components.set(name, metadata);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Discover layouts
|
|
77
|
+
const layouts = discoverLayouts(layoutsDir, baseDir);
|
|
78
|
+
|
|
79
|
+
// Keep components and layouts separate
|
|
80
|
+
// Components will be processed in the component loop
|
|
81
|
+
// Layouts are processed separately first
|
|
82
|
+
const allComponents = new Map<string, ComponentMetadata>();
|
|
83
|
+
const componentNameMap = new Map<string, string>(); // lowercase -> original case
|
|
84
|
+
|
|
85
|
+
// Only add actual components (not layouts) to the component processing map
|
|
86
|
+
for (const [name, metadata] of components.entries()) {
|
|
87
|
+
allComponents.set(name, metadata);
|
|
88
|
+
componentNameMap.set(name.toLowerCase(), name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Layouts are stored separately and processed before components
|
|
92
|
+
const layoutNames = new Set(layouts.keys());
|
|
93
|
+
const layoutNamesLowercase = new Set(Array.from(layoutNames).map(n => n.toLowerCase()));
|
|
94
|
+
|
|
95
|
+
if (allComponents.size === 0) {
|
|
96
|
+
// No components, return original file
|
|
97
|
+
return entryFile;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if entry file uses a layout
|
|
101
|
+
const layoutUsage = findLayoutUsage(entryFile.html, layouts);
|
|
102
|
+
|
|
103
|
+
// Process layout first (if used)
|
|
104
|
+
let processedHtml = entryFile.html;
|
|
105
|
+
let processedScripts = [...entryFile.scripts];
|
|
106
|
+
let processedStyles = [...entryFile.styles];
|
|
107
|
+
|
|
108
|
+
if (layoutUsage) {
|
|
109
|
+
const layout = layouts.get(layoutUsage.layoutName);
|
|
110
|
+
if (layout) {
|
|
111
|
+
// Extract page content (everything except html/head/body tags)
|
|
112
|
+
const pageContent = extractPageContent(entryFile.html);
|
|
113
|
+
|
|
114
|
+
// For layouts, we need special handling - layouts define the HTML structure
|
|
115
|
+
const layoutResult = inlineLayout(layout, pageContent, `layout-${layoutUsage.layoutName}`, layoutUsage.props);
|
|
116
|
+
|
|
117
|
+
processedHtml = layoutResult.html;
|
|
118
|
+
// Layout scripts must come BEFORE page scripts so layout state is declared first
|
|
119
|
+
processedScripts = [...layoutResult.scripts, ...processedScripts];
|
|
120
|
+
processedStyles = [...layoutResult.styles, ...processedStyles];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Process all component usages (process from innermost to outermost)
|
|
125
|
+
// Exclude layouts from component processing (they're already processed)
|
|
126
|
+
let instanceCounter = 0;
|
|
127
|
+
const componentNamesLowercase = new Set(Array.from(allComponents.keys()).map(n => n.toLowerCase()));
|
|
128
|
+
const functionProps = new Set<string>(); // Track function names passed as props
|
|
129
|
+
const maxIterations = 100; // Prevent infinite loops
|
|
130
|
+
let iterations = 0;
|
|
131
|
+
|
|
132
|
+
// Filter out layouts from component usage check
|
|
133
|
+
// Also skip elements that have data-zen-component attribute (already processed)
|
|
134
|
+
function hasComponentUsageFiltered(html: string, componentNamesLowercase: Set<string>, componentNameMap: Map<string, string>, excludeLayouts: Set<string>): boolean {
|
|
135
|
+
const document = parse5.parse(html);
|
|
136
|
+
let found = false;
|
|
137
|
+
|
|
138
|
+
function walk(node: any): void {
|
|
139
|
+
if (found) return; // Early exit if already found
|
|
140
|
+
|
|
141
|
+
if (node.tagName) {
|
|
142
|
+
const tagLower = node.tagName.toLowerCase();
|
|
143
|
+
// Skip layouts and already-replaced nodes (have data-zen-replaced)
|
|
144
|
+
const hasReplaced = node.attrs && node.attrs.some((attr: any) => attr.name === 'data-zen-replaced' && attr.value === 'true');
|
|
145
|
+
const hasZenComponent = node.attrs && node.attrs.some((attr: any) => attr.name === 'data-zen-component');
|
|
146
|
+
|
|
147
|
+
// Only count as found if: not a layout, not already replaced, not a native HTML element, and matches a component name
|
|
148
|
+
if (!hasReplaced && !hasZenComponent && !excludeLayouts.has(tagLower) && !NATIVE_HTML_ELEMENTS.has(tagLower) && componentNamesLowercase.has(tagLower)) {
|
|
149
|
+
found = true;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (node.childNodes && !found) {
|
|
154
|
+
node.childNodes.forEach(walk);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
walk(document);
|
|
159
|
+
return found;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Helper function to find first component usage (excluding layouts)
|
|
163
|
+
function findFirstComponentUsageFiltered(html: string, componentNamesLowercase: Set<string>, componentNameMap: Map<string, string>, excludeLayouts: Set<string>): { tagName: string; node: any; children: any[]; props: Map<string, string> } | null {
|
|
164
|
+
const document = parse5.parse(html);
|
|
165
|
+
let found: { tagName: string; node: any; children: any[]; props: Map<string, string> } | null = null;
|
|
166
|
+
|
|
167
|
+
function walk(node: any): void {
|
|
168
|
+
if (found) return; // Early exit if already found
|
|
169
|
+
|
|
170
|
+
if (node.tagName) {
|
|
171
|
+
const tagLower = node.tagName.toLowerCase();
|
|
172
|
+
// Skip layouts and already-replaced nodes (have data-zen-replaced)
|
|
173
|
+
const hasReplaced = node.attrs && node.attrs.some((attr: any) => attr.name === 'data-zen-replaced' && attr.value === 'true');
|
|
174
|
+
const hasZenComponent = node.attrs && node.attrs.some((attr: any) => attr.name === 'data-zen-component');
|
|
175
|
+
|
|
176
|
+
// Only process if: not a layout, not already replaced, not a native HTML element, and matches a component name
|
|
177
|
+
if (!hasReplaced && !hasZenComponent && !excludeLayouts.has(tagLower) && !NATIVE_HTML_ELEMENTS.has(tagLower)) {
|
|
178
|
+
const originalName = componentNameMap.get(tagLower);
|
|
179
|
+
if (originalName) {
|
|
180
|
+
// Extract props from attributes
|
|
181
|
+
const props = new Map<string, string>();
|
|
182
|
+
if (node.attrs) {
|
|
183
|
+
for (const attr of node.attrs) {
|
|
184
|
+
if (attr.name !== "slot") {
|
|
185
|
+
props.set(attr.name, attr.value || "");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Get children (excluding slot attribute nodes)
|
|
191
|
+
const children = (node.childNodes || []).filter((child: any) => {
|
|
192
|
+
// Keep element nodes and text nodes, but filter out slot attributes
|
|
193
|
+
return child.nodeName !== "#comment";
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
found = { tagName: originalName, node, children, props };
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (node.childNodes && !found) {
|
|
202
|
+
node.childNodes.forEach(walk);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
walk(document);
|
|
207
|
+
return found;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
while (iterations < maxIterations) {
|
|
211
|
+
// Re-check for components at the start of each iteration
|
|
212
|
+
const hasComponents = hasComponentUsageFiltered(processedHtml, componentNamesLowercase, componentNameMap, layoutNamesLowercase);
|
|
213
|
+
if (!hasComponents) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
iterations++;
|
|
218
|
+
const usage = findFirstComponentUsageFiltered(processedHtml, componentNamesLowercase, componentNameMap, layoutNamesLowercase);
|
|
219
|
+
if (!usage) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const component = allComponents.get(usage.tagName);
|
|
224
|
+
if (!component) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Extract props from parent script context if available
|
|
229
|
+
// Look for variables/functions in parent scripts that match prop names
|
|
230
|
+
const enhancedProps = new Map(usage.props);
|
|
231
|
+
// Create case-insensitive lookup for enhancedProps (parse5 lowercases attribute names)
|
|
232
|
+
const enhancedPropsLowercase = new Map<string, string>();
|
|
233
|
+
for (const [key, value] of enhancedProps.entries()) {
|
|
234
|
+
enhancedPropsLowercase.set(key.toLowerCase(), value);
|
|
235
|
+
}
|
|
236
|
+
// Track function props from usage.props
|
|
237
|
+
for (const [propName, propValue] of usage.props.entries()) {
|
|
238
|
+
// Check if propValue is a function name (not quoted, valid identifier)
|
|
239
|
+
// Handle both {variable} and variable syntax
|
|
240
|
+
let trimmedValue = propValue.trim();
|
|
241
|
+
// Strip braces if present: {increment} -> increment
|
|
242
|
+
if (trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) {
|
|
243
|
+
trimmedValue = trimmedValue.slice(1, -1).trim();
|
|
244
|
+
}
|
|
245
|
+
const isFunctionOrVar = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmedValue) &&
|
|
246
|
+
!trimmedValue.startsWith('"') &&
|
|
247
|
+
!trimmedValue.startsWith("'");
|
|
248
|
+
if (isFunctionOrVar) {
|
|
249
|
+
// This is a function prop - track it for mutation validator
|
|
250
|
+
functionProps.add(trimmedValue);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (const [propName] of component.props.entries()) {
|
|
254
|
+
// Check case-insensitively (parse5 lowercases attribute names)
|
|
255
|
+
const hasProp = enhancedProps.has(propName) || enhancedPropsLowercase.has(propName.toLowerCase());
|
|
256
|
+
if (!hasProp) {
|
|
257
|
+
// Check if this prop name exists as a variable/function in parent scripts
|
|
258
|
+
// This allows passing functions and variables from parent script context
|
|
259
|
+
for (const script of processedScripts) {
|
|
260
|
+
const scriptContent = script.content;
|
|
261
|
+
// Check if propName is a function or variable in the script
|
|
262
|
+
// Simple heuristic: look for "function propName" or "const propName" or "let propName" or "var propName"
|
|
263
|
+
const functionMatch = new RegExp(`function\\s+${propName}\\s*\\(`).test(scriptContent);
|
|
264
|
+
const varMatch = new RegExp(`(?:const|let|var)\\s+${propName}\\s*[=;]`).test(scriptContent);
|
|
265
|
+
if (functionMatch || varMatch) {
|
|
266
|
+
// Found in parent script - use it as-is (don't quote it)
|
|
267
|
+
enhancedProps.set(propName, propName);
|
|
268
|
+
functionProps.add(propName); // Track as function prop
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const instanceId = `comp-${instanceCounter++}`;
|
|
276
|
+
const result = inlineComponentIntoHtml(
|
|
277
|
+
processedHtml,
|
|
278
|
+
component,
|
|
279
|
+
usage.children,
|
|
280
|
+
instanceId,
|
|
281
|
+
enhancedProps
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Verify the replacement actually happened
|
|
285
|
+
if (!result || result.html === processedHtml) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
processedHtml = result.html;
|
|
290
|
+
processedScripts = [...processedScripts, ...result.scripts];
|
|
291
|
+
processedStyles = [...processedStyles, ...result.styles];
|
|
292
|
+
|
|
293
|
+
// Safety check: if we've processed more than expected, something is wrong
|
|
294
|
+
if (instanceCounter > 100) {
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
html: processedHtml,
|
|
301
|
+
scripts: processedScripts,
|
|
302
|
+
styles: processedStyles,
|
|
303
|
+
functionProps: functionProps
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Inline a layout - layouts define the HTML structure (html/head/body)
|
|
309
|
+
* Replaces <Slot /> with page content
|
|
310
|
+
*/
|
|
311
|
+
function inlineLayout(
|
|
312
|
+
layout: ComponentMetadata,
|
|
313
|
+
pageContent: any[],
|
|
314
|
+
instanceId: string,
|
|
315
|
+
layoutUsageProps: Map<string, string> = new Map()
|
|
316
|
+
): { html: string; scripts: ScriptBlock[]; styles: StyleBlock[] } {
|
|
317
|
+
// Layout HTML already has html/head/body structure
|
|
318
|
+
const layoutDoc = parse5.parse(layout.html);
|
|
319
|
+
|
|
320
|
+
// Find the body element to replace slots
|
|
321
|
+
let bodyElement: any = null;
|
|
322
|
+
function findBody(node: any): void {
|
|
323
|
+
if (node.tagName === "body") {
|
|
324
|
+
bodyElement = node;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (node.childNodes) {
|
|
328
|
+
for (const child of node.childNodes) {
|
|
329
|
+
findBody(child);
|
|
330
|
+
if (bodyElement) return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
findBody(layoutDoc);
|
|
335
|
+
|
|
336
|
+
if (!bodyElement) {
|
|
337
|
+
// No body found, return layout HTML as-is
|
|
338
|
+
return { html: layout.html, scripts: [], styles: [] };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Replace slots in body with page content
|
|
342
|
+
function replaceSlots(node: any): void {
|
|
343
|
+
if (!node.childNodes) return;
|
|
344
|
+
|
|
345
|
+
const newChildren: any[] = [];
|
|
346
|
+
|
|
347
|
+
for (const child of node.childNodes) {
|
|
348
|
+
if (child.tagName === "Slot" || child.tagName === "slot") {
|
|
349
|
+
const slotNameAttr = child.attrs?.find((a: any) => a.name === "name");
|
|
350
|
+
const slotName = slotNameAttr?.value || "default";
|
|
351
|
+
|
|
352
|
+
// Find content for this slot
|
|
353
|
+
const content = pageContent.filter(node => {
|
|
354
|
+
const slotAttr = node.attrs?.find((a: any) => a.name === "slot");
|
|
355
|
+
const nodeSlotName = slotAttr?.value || "default";
|
|
356
|
+
return nodeSlotName === slotName;
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Add slot content
|
|
360
|
+
for (const slotNode of content) {
|
|
361
|
+
newChildren.push(cloneNode(slotNode));
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
replaceSlots(child);
|
|
365
|
+
newChildren.push(child);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
node.childNodes = newChildren;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
replaceSlots(bodyElement);
|
|
373
|
+
|
|
374
|
+
// Serialize the layout document back to HTML
|
|
375
|
+
const html = parse5.serialize(layoutDoc);
|
|
376
|
+
|
|
377
|
+
// Process layout scripts with props
|
|
378
|
+
// NOTE: Layouts are document-level, so we DON'T transform state names (keep them as-is)
|
|
379
|
+
// This allows { title } in HTML to match state title in scripts
|
|
380
|
+
const processedScripts: ScriptBlock[] = [];
|
|
381
|
+
let scriptIndex = 0;
|
|
382
|
+
|
|
383
|
+
// Create props object for layout
|
|
384
|
+
const propsObjEntries: string[] = [];
|
|
385
|
+
// Extract props from layout usage
|
|
386
|
+
for (const [propName, defaultValue] of layout.props.entries()) {
|
|
387
|
+
const propValue = layoutUsageProps.get(propName);
|
|
388
|
+
if (propValue !== undefined) {
|
|
389
|
+
// Use provided value - quote if it's a string
|
|
390
|
+
propsObjEntries.push(` ${propName}: "${propValue}"`);
|
|
391
|
+
} else if (defaultValue === "?") {
|
|
392
|
+
// Optional prop from type Props - use undefined
|
|
393
|
+
propsObjEntries.push(` ${propName}: undefined`);
|
|
394
|
+
} else if (defaultValue) {
|
|
395
|
+
// Use default value (already an expression)
|
|
396
|
+
propsObjEntries.push(` ${propName}: ${defaultValue}`);
|
|
397
|
+
} else {
|
|
398
|
+
// No default, use undefined
|
|
399
|
+
propsObjEntries.push(` ${propName}: undefined`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const propsObjCode = propsObjEntries.length > 0
|
|
403
|
+
? `const props = {\n${propsObjEntries.join(",\n")}\n};`
|
|
404
|
+
: `const props = {};`;
|
|
405
|
+
|
|
406
|
+
for (const script of layout.scripts) {
|
|
407
|
+
let processedScript = script;
|
|
408
|
+
|
|
409
|
+
// Remove type Props definitions (TypeScript types are compile-time only, not needed in JavaScript)
|
|
410
|
+
processedScript = processedScript.replace(/type\s+Props\s*=\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}\s*/gs, '');
|
|
411
|
+
|
|
412
|
+
// Replace props references in state declarations with actual prop values
|
|
413
|
+
// e.g., "state title = props.title || 'My App'" -> "state title = 'Home Page' || 'My App'"
|
|
414
|
+
for (const [propName, propValue] of layoutUsageProps.entries()) {
|
|
415
|
+
// Replace props.propName with the actual value (quote if it's a string)
|
|
416
|
+
const quotedValue = propValue ? `"${propValue}"` : 'undefined';
|
|
417
|
+
const propsRefRegex = new RegExp(`props\\.${propName}\\b`, 'g');
|
|
418
|
+
processedScript = processedScript.replace(propsRefRegex, quotedValue);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Layouts use state names as-is (no instance scoping)
|
|
422
|
+
// Wrap in IIFE to scope props (even though we've replaced props references above)
|
|
423
|
+
const wrappedScript = `(function() {\n // Props for layout\n ${propsObjCode}\n\n${processedScript}\n})();`;
|
|
424
|
+
|
|
425
|
+
processedScripts.push({ content: wrappedScript, index: scriptIndex++ });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Process layout styles with scoping
|
|
429
|
+
const processedStyles: StyleBlock[] = [];
|
|
430
|
+
let styleIndex = 0;
|
|
431
|
+
for (const style of layout.styles) {
|
|
432
|
+
// Layout styles don't need scoping - they're at the document level
|
|
433
|
+
processedStyles.push({ content: style, index: styleIndex++ });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
html,
|
|
438
|
+
scripts: processedScripts,
|
|
439
|
+
styles: processedStyles
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Check if a layout is used in HTML (looks for layout component as root element)
|
|
445
|
+
*/
|
|
446
|
+
function findLayoutUsage(html: string, layouts: Map<string, ComponentMetadata>): { layoutName: string; node: any; props: Map<string, string> } | null {
|
|
447
|
+
const document = parse5.parse(html);
|
|
448
|
+
|
|
449
|
+
// Create lowercase map for case-insensitive matching
|
|
450
|
+
const layoutLowercaseMap = new Map<string, string>();
|
|
451
|
+
for (const name of layouts.keys()) {
|
|
452
|
+
layoutLowercaseMap.set(name.toLowerCase(), name);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function findLayout(node: any): { layoutName: string; node: any; props: Map<string, string> } | null {
|
|
456
|
+
if (node.tagName) {
|
|
457
|
+
const originalName = layoutLowercaseMap.get(node.tagName.toLowerCase());
|
|
458
|
+
if (originalName) {
|
|
459
|
+
// Extract props from attributes
|
|
460
|
+
const props = new Map<string, string>();
|
|
461
|
+
if (node.attrs) {
|
|
462
|
+
for (const attr of node.attrs) {
|
|
463
|
+
props.set(attr.name, attr.value || "");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return { layoutName: originalName, node, props };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (node.childNodes) {
|
|
470
|
+
for (const child of node.childNodes) {
|
|
471
|
+
const result = findLayout(child);
|
|
472
|
+
if (result) return result;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return findLayout(document);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Extract page content from HTML (everything inside body, or root content if no body/html)
|
|
483
|
+
*/
|
|
484
|
+
function extractPageContent(html: string): any[] {
|
|
485
|
+
const document = parse5.parse(html);
|
|
486
|
+
|
|
487
|
+
function findContent(node: any): any[] {
|
|
488
|
+
// If we find body, return its children
|
|
489
|
+
if (node.tagName === "body") {
|
|
490
|
+
return node.childNodes || [];
|
|
491
|
+
}
|
|
492
|
+
// If we find html, look for body inside it
|
|
493
|
+
if (node.tagName === "html") {
|
|
494
|
+
if (node.childNodes) {
|
|
495
|
+
for (const child of node.childNodes) {
|
|
496
|
+
if (child.tagName === "body") {
|
|
497
|
+
return child.childNodes || [];
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// No body tag, return html's children (skip head)
|
|
502
|
+
return (node.childNodes || []).filter((n: any) => n.tagName !== "head");
|
|
503
|
+
}
|
|
504
|
+
// If this is the document root, look for the first element (skip #documentType, etc)
|
|
505
|
+
if (node.nodeName === "#document") {
|
|
506
|
+
if (node.childNodes) {
|
|
507
|
+
for (const child of node.childNodes) {
|
|
508
|
+
if (child.tagName === "html") {
|
|
509
|
+
return findContent(child);
|
|
510
|
+
}
|
|
511
|
+
// No html tag - page content is directly at root level
|
|
512
|
+
if (child.tagName && child.tagName !== "#documentType") {
|
|
513
|
+
return [child];
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// No html tag found - return all non-document-type nodes
|
|
517
|
+
return (node.childNodes || []).filter((n: any) =>
|
|
518
|
+
n.tagName && n.tagName !== "#documentType"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return findContent(document);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Check if HTML contains any component usages
|
|
530
|
+
*/
|
|
531
|
+
function hasComponentUsage(
|
|
532
|
+
html: string,
|
|
533
|
+
componentNamesLowercase: Set<string>,
|
|
534
|
+
componentNameMap: Map<string, string>
|
|
535
|
+
): boolean {
|
|
536
|
+
const document = parse5.parse(html);
|
|
537
|
+
let found = false;
|
|
538
|
+
|
|
539
|
+
function walk(node: any): void {
|
|
540
|
+
const tagLower = node.tagName ? node.tagName.toLowerCase() : '';
|
|
541
|
+
if (node.tagName && !NATIVE_HTML_ELEMENTS.has(tagLower) && componentNamesLowercase.has(tagLower)) {
|
|
542
|
+
found = true;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (node.childNodes && !found) {
|
|
546
|
+
node.childNodes.forEach(walk);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
walk(document);
|
|
551
|
+
return found;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Find the first component usage in HTML
|
|
556
|
+
*/
|
|
557
|
+
function findFirstComponentUsage(
|
|
558
|
+
html: string,
|
|
559
|
+
componentNamesLowercase: Set<string>,
|
|
560
|
+
componentNameMap: Map<string, string>
|
|
561
|
+
): { tagName: string; node: any; children: any[]; props: Map<string, string> } | null {
|
|
562
|
+
const document = parse5.parse(html);
|
|
563
|
+
let found: { tagName: string; node: any; children: any[]; props: Map<string, string> } | null = null;
|
|
564
|
+
|
|
565
|
+
function walk(node: any): void {
|
|
566
|
+
if (found) return;
|
|
567
|
+
|
|
568
|
+
const tagLower = node.tagName ? node.tagName.toLowerCase() : '';
|
|
569
|
+
if (node.tagName && !NATIVE_HTML_ELEMENTS.has(tagLower) && componentNamesLowercase.has(tagLower)) {
|
|
570
|
+
// Get the original component name (with correct case)
|
|
571
|
+
const originalName = componentNameMap.get(tagLower) || node.tagName;
|
|
572
|
+
|
|
573
|
+
const props = new Map<string, string>();
|
|
574
|
+
if (node.attrs) {
|
|
575
|
+
for (const attr of node.attrs) {
|
|
576
|
+
props.set(attr.name, attr.value || "");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
found = {
|
|
581
|
+
tagName: originalName, // Use original case name
|
|
582
|
+
node,
|
|
583
|
+
children: node.childNodes || [],
|
|
584
|
+
props
|
|
585
|
+
};
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (node.childNodes) {
|
|
590
|
+
node.childNodes.forEach(walk);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
walk(document);
|
|
595
|
+
return found;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Inline a component into HTML, replacing the component tag with component HTML
|
|
600
|
+
* and handling slots, props, styles, and scripts
|
|
601
|
+
*/
|
|
602
|
+
function inlineComponentIntoHtml(
|
|
603
|
+
html: string,
|
|
604
|
+
component: ComponentMetadata,
|
|
605
|
+
slotContent: any[],
|
|
606
|
+
instanceId: string,
|
|
607
|
+
props: Map<string, string> = new Map()
|
|
608
|
+
): { html: string; scripts: ScriptBlock[]; styles: StyleBlock[] } {
|
|
609
|
+
const document = parse5.parse(html);
|
|
610
|
+
|
|
611
|
+
// Strip script and style tags from component HTML before parsing (they're handled separately)
|
|
612
|
+
let componentHtml = component.html;
|
|
613
|
+
componentHtml = componentHtml.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
614
|
+
componentHtml = componentHtml.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
615
|
+
componentHtml = componentHtml.trim();
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
// Use parseFragment instead of parse to avoid html/head/body wrapper
|
|
619
|
+
const componentFragment = parse5.parseFragment(componentHtml);
|
|
620
|
+
|
|
621
|
+
// Find component root - should be the first element in the fragment
|
|
622
|
+
let componentRoot: any = null;
|
|
623
|
+
|
|
624
|
+
function findFirstElement(node: any): void {
|
|
625
|
+
if (!node) return;
|
|
626
|
+
|
|
627
|
+
// Skip script/style/document nodes
|
|
628
|
+
if (node.tagName) {
|
|
629
|
+
const tagLower = node.tagName.toLowerCase();
|
|
630
|
+
if (tagLower === "script" || tagLower === "style") {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
// Found a valid component root element
|
|
634
|
+
componentRoot = node;
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// For non-element nodes, recurse into children
|
|
639
|
+
if (node.childNodes && Array.isArray(node.childNodes)) {
|
|
640
|
+
for (const child of node.childNodes) {
|
|
641
|
+
findFirstElement(child);
|
|
642
|
+
if (componentRoot) return;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
findFirstElement(componentFragment);
|
|
648
|
+
|
|
649
|
+
if (!componentRoot) {
|
|
650
|
+
return { html, scripts: [], styles: [] };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Transform state references in component HTML before cloning
|
|
654
|
+
const stateDecls = new Set<string>();
|
|
655
|
+
for (const script of component.scripts) {
|
|
656
|
+
const decls = extractStateDeclarations(script);
|
|
657
|
+
for (const [stateName] of decls.entries()) {
|
|
658
|
+
stateDecls.add(stateName);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Note: We don't create clonedRoot here anymore - we create a fresh clone
|
|
663
|
+
// for each replacement in replaceComponentTag to avoid reusing nodes with parentNode set
|
|
664
|
+
|
|
665
|
+
// Track prop-based handlers that need wrapper functions (propName -> eventType)
|
|
666
|
+
// This will be populated during component inlining and used to generate wrappers
|
|
667
|
+
const propHandlers = new Map<string, string>(); // propName -> eventType (e.g., "onClick" -> "click")
|
|
668
|
+
|
|
669
|
+
// Replace slots with slot content (this function will be used in replaceComponentTag)
|
|
670
|
+
function replaceSlots(node: any): void {
|
|
671
|
+
if (!node.childNodes || node.childNodes.length === 0) return;
|
|
672
|
+
|
|
673
|
+
const newChildren: any[] = [];
|
|
674
|
+
|
|
675
|
+
for (const child of node.childNodes) {
|
|
676
|
+
if (child.tagName === "Slot" || child.tagName === "slot") {
|
|
677
|
+
const slotNameAttr = child.attrs?.find((a: any) => a.name === "name");
|
|
678
|
+
const slotName = slotNameAttr?.value || "default";
|
|
679
|
+
|
|
680
|
+
// Find content for this slot
|
|
681
|
+
const content = slotContent.filter(node => {
|
|
682
|
+
const slotAttr = node.attrs?.find((a: any) => a.name === "slot");
|
|
683
|
+
const nodeSlotName = slotAttr?.value || "default";
|
|
684
|
+
return nodeSlotName === slotName;
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Add slot content
|
|
688
|
+
for (const slotNode of content) {
|
|
689
|
+
newChildren.push(cloneNode(slotNode));
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
replaceSlots(child);
|
|
693
|
+
newChildren.push(child);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
node.childNodes = newChildren;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Replace component tag with cloned root in document
|
|
701
|
+
// Create a fresh clone for EACH replacement to avoid reusing nodes with parentNode set
|
|
702
|
+
// Note: parse5 lowercases tag names, so compare case-insensitively
|
|
703
|
+
let replaced = false;
|
|
704
|
+
function replaceComponentTag(node: any): boolean {
|
|
705
|
+
if (node.childNodes) {
|
|
706
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
707
|
+
const child = node.childNodes[i];
|
|
708
|
+
// Check if this is a component tag that needs replacement
|
|
709
|
+
// Must match component name (case-insensitive) and NOT have data-zen-replaced
|
|
710
|
+
const childTagLower = child.tagName ? child.tagName.toLowerCase() : '';
|
|
711
|
+
const componentNameLower = component.name.toLowerCase();
|
|
712
|
+
const isComponentTag = childTagLower === componentNameLower;
|
|
713
|
+
const alreadyReplaced = child.attrs && child.attrs.some((attr: any) => attr.name === 'data-zen-replaced' && attr.value === 'true');
|
|
714
|
+
|
|
715
|
+
if (isComponentTag && !alreadyReplaced) {
|
|
716
|
+
// Found the component tag - create a FRESH clone for this replacement
|
|
717
|
+
const freshClone = cloneNode(componentRoot);
|
|
718
|
+
|
|
719
|
+
// Replace hyphens in instanceId with underscores for valid JavaScript identifiers
|
|
720
|
+
const safeInstanceId = instanceId.replace(/-/g, '_');
|
|
721
|
+
|
|
722
|
+
// Transform state references in the fresh clone
|
|
723
|
+
function transformStateInHtml(node: any): void {
|
|
724
|
+
// Transform text bindings: { stateName } to { __zen_instanceId_stateName }
|
|
725
|
+
// Also inline prop values: { propName } -> actual prop value
|
|
726
|
+
if (node.nodeName === "#text" && node.value) {
|
|
727
|
+
// First, inline prop values (props are constants, not reactive)
|
|
728
|
+
for (const [propName, propValue] of props.entries()) {
|
|
729
|
+
// Check if propValue is a simple identifier (function/state reference) - don't inline those
|
|
730
|
+
let trimmedValue = propValue.trim();
|
|
731
|
+
if (trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) {
|
|
732
|
+
trimmedValue = trimmedValue.slice(1, -1).trim();
|
|
733
|
+
}
|
|
734
|
+
const isFunctionOrVar = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmedValue) &&
|
|
735
|
+
!trimmedValue.startsWith('"') &&
|
|
736
|
+
!trimmedValue.startsWith("'");
|
|
737
|
+
|
|
738
|
+
// Only inline if it's not a function/state reference (i.e., it's a literal value)
|
|
739
|
+
if (!isFunctionOrVar) {
|
|
740
|
+
// Inline the prop value directly
|
|
741
|
+
const regex = new RegExp(`\\{\\s*${propName}\\s*\\}`, 'g');
|
|
742
|
+
// Get the actual prop value - remove quotes for text content
|
|
743
|
+
let inlineValue = propValue.trim();
|
|
744
|
+
if ((inlineValue.startsWith('"') && inlineValue.endsWith('"')) ||
|
|
745
|
+
(inlineValue.startsWith("'") && inlineValue.endsWith("'"))) {
|
|
746
|
+
inlineValue = inlineValue.slice(1, -1); // Remove quotes
|
|
747
|
+
}
|
|
748
|
+
node.value = node.value.replace(regex, inlineValue);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Then transform state references to instance-scoped names
|
|
753
|
+
for (const stateName of stateDecls) {
|
|
754
|
+
const instanceStateName = `__zen_${safeInstanceId}_${stateName}`;
|
|
755
|
+
const regex = new RegExp(`\\{\\s*${stateName}\\s*\\}`, 'g');
|
|
756
|
+
node.value = node.value.replace(regex, `{ ${instanceStateName} }`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Transform attribute bindings (:class, :value) to use instance-scoped state names
|
|
761
|
+
// Also inline prop values in attribute values and handle :attr bindings
|
|
762
|
+
if (node.attrs && Array.isArray(node.attrs)) {
|
|
763
|
+
for (const attr of node.attrs) {
|
|
764
|
+
let attrValue = attr.value || '';
|
|
765
|
+
|
|
766
|
+
// Handle :attr bindings (like :href, :src) - inline prop values or transform state references
|
|
767
|
+
if (attr.name && attr.name.startsWith(':') && attr.name !== ':class' && attr.name !== ':value') {
|
|
768
|
+
// This is a non-reactive attribute binding (e.g., :href)
|
|
769
|
+
// Inline prop values first
|
|
770
|
+
for (const [propName, propValue] of props.entries()) {
|
|
771
|
+
let trimmedValue = propValue.trim();
|
|
772
|
+
if (trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) {
|
|
773
|
+
trimmedValue = trimmedValue.slice(1, -1).trim();
|
|
774
|
+
}
|
|
775
|
+
const isFunctionOrVar = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmedValue) &&
|
|
776
|
+
!trimmedValue.startsWith('"') &&
|
|
777
|
+
!trimmedValue.startsWith("'");
|
|
778
|
+
|
|
779
|
+
if (!isFunctionOrVar) {
|
|
780
|
+
// Inline prop value for attribute
|
|
781
|
+
const propRegex = new RegExp(`\\b${propName}\\b`, 'g');
|
|
782
|
+
let inlineValue = propValue.trim();
|
|
783
|
+
// Remove quotes for attribute values (parse5 will handle quoting)
|
|
784
|
+
if ((inlineValue.startsWith('"') && inlineValue.endsWith('"')) ||
|
|
785
|
+
(inlineValue.startsWith("'") && inlineValue.endsWith("'"))) {
|
|
786
|
+
inlineValue = inlineValue.slice(1, -1); // Remove quotes
|
|
787
|
+
}
|
|
788
|
+
attrValue = attrValue.replace(propRegex, inlineValue);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Transform state references
|
|
793
|
+
for (const stateName of stateDecls) {
|
|
794
|
+
const instanceStateName = `__zen_${safeInstanceId}_${stateName}`;
|
|
795
|
+
const stateRefRegex = new RegExp(`\\b${stateName}\\b`, 'g');
|
|
796
|
+
attrValue = attrValue.replace(stateRefRegex, instanceStateName);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Replace :attr with regular attr (remove colon)
|
|
800
|
+
attr.name = attr.name.slice(1);
|
|
801
|
+
attr.value = attrValue;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Handle reactive bindings (:class, :value)
|
|
805
|
+
const isBindingAttr = attr.name === ':class' || attr.name === ':value' ||
|
|
806
|
+
attr.name === 'data-zen-class' || attr.name === 'data-zen-value';
|
|
807
|
+
if (isBindingAttr) {
|
|
808
|
+
for (const stateName of stateDecls) {
|
|
809
|
+
const instanceStateName = `__zen_${safeInstanceId}_${stateName}`;
|
|
810
|
+
// Replace state references in attribute values using word boundaries
|
|
811
|
+
// This handles expressions like "{ 'btn': true, 'btn-clicked': clicks > 0 }"
|
|
812
|
+
const stateRefRegex = new RegExp(`\\b${stateName}\\b`, 'g');
|
|
813
|
+
attrValue = attrValue.replace(stateRefRegex, instanceStateName);
|
|
814
|
+
}
|
|
815
|
+
attr.value = attrValue;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Handle regular attributes with {propName} syntax
|
|
819
|
+
if (attrValue && attrValue.includes('{')) {
|
|
820
|
+
// Inline prop values in attribute values
|
|
821
|
+
for (const [propName, propValue] of props.entries()) {
|
|
822
|
+
let trimmedValue = propValue.trim();
|
|
823
|
+
if (trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) {
|
|
824
|
+
trimmedValue = trimmedValue.slice(1, -1).trim();
|
|
825
|
+
}
|
|
826
|
+
const isFunctionOrVar = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmedValue) &&
|
|
827
|
+
!trimmedValue.startsWith('"') &&
|
|
828
|
+
!trimmedValue.startsWith("'");
|
|
829
|
+
|
|
830
|
+
if (!isFunctionOrVar) {
|
|
831
|
+
// Inline prop value
|
|
832
|
+
const propRegex = new RegExp(`\\{\\s*${propName}\\s*\\}`, 'g');
|
|
833
|
+
let inlineValue = propValue.trim();
|
|
834
|
+
if (inlineValue.startsWith('"') || inlineValue.startsWith("'")) {
|
|
835
|
+
inlineValue = inlineValue.slice(1, -1); // Remove quotes for attribute value
|
|
836
|
+
}
|
|
837
|
+
attrValue = attrValue.replace(propRegex, inlineValue);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
attr.value = attrValue;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (node.childNodes) {
|
|
846
|
+
node.childNodes.forEach(transformStateInHtml);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
transformStateInHtml(freshClone);
|
|
850
|
+
|
|
851
|
+
// Add instance attributes
|
|
852
|
+
if (!freshClone.attrs) freshClone.attrs = [];
|
|
853
|
+
freshClone.attrs.push({ name: "data-zen-instance", value: instanceId });
|
|
854
|
+
freshClone.attrs.push({ name: "data-zen-component", value: component.name });
|
|
855
|
+
freshClone.attrs.push({ name: "data-zen-replaced", value: "true" });
|
|
856
|
+
|
|
857
|
+
// Transform event handler attributes to use instance-scoped function names
|
|
858
|
+
// This ensures each component instance has unique handler names
|
|
859
|
+
// Handle both onclick (before split.ts) and data-zen-click (after split.ts)
|
|
860
|
+
for (const attr of freshClone.attrs) {
|
|
861
|
+
const attrName = attr.name ? attr.name.toLowerCase() : '';
|
|
862
|
+
// Check for onclick, onchange, etc. (before split.ts transforms them)
|
|
863
|
+
if (attrName.startsWith('on') && attrName.length > 2) {
|
|
864
|
+
const handlerName = attr.value;
|
|
865
|
+
if (handlerName && /^\w+$/.test(handlerName)) {
|
|
866
|
+
// Check if this handler name is a prop (not a function)
|
|
867
|
+
// Convert camelCase prop name (onClick) to event type (click)
|
|
868
|
+
const eventType = attrName.slice(2); // Remove "on" prefix
|
|
869
|
+
if (component.props.has(handlerName)) {
|
|
870
|
+
// It's a prop - bind directly (no wrapper function)
|
|
871
|
+
// Store the mapping for later use (to register on window)
|
|
872
|
+
propHandlers.set(handlerName, eventType);
|
|
873
|
+
// Use instance-scoped name that will reference the prop directly
|
|
874
|
+
const handlerRefName = `__zen_${safeInstanceId}_${handlerName}`;
|
|
875
|
+
attr.value = handlerRefName;
|
|
876
|
+
} else {
|
|
877
|
+
// It's a regular function name, make it instance-scoped
|
|
878
|
+
attr.value = `__zen_${safeInstanceId}_${handlerName}`;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// Check for data-zen-click, data-zen-change, etc. (after split.ts transforms them)
|
|
883
|
+
else if (attr.name && attr.name.startsWith('data-zen-') &&
|
|
884
|
+
attr.name !== 'data-zen-instance' &&
|
|
885
|
+
attr.name !== 'data-zen-component' &&
|
|
886
|
+
attr.name !== 'data-zen-replaced' &&
|
|
887
|
+
attr.name !== 'data-zen-class' &&
|
|
888
|
+
attr.name !== 'data-zen-value' &&
|
|
889
|
+
attr.name !== 'data-zen-bind' &&
|
|
890
|
+
attr.name !== 'data-zen-bind-id') {
|
|
891
|
+
// This is likely an event handler (data-zen-click, etc.)
|
|
892
|
+
// Transform the handler name to instance-scoped version
|
|
893
|
+
const handlerName = attr.value;
|
|
894
|
+
if (handlerName && /^\w+$/.test(handlerName)) {
|
|
895
|
+
// It's a simple function name, make it instance-scoped
|
|
896
|
+
attr.value = `__zen_${safeInstanceId}_${handlerName}`;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Replace slots if needed
|
|
902
|
+
if (component.hasSlots) {
|
|
903
|
+
replaceSlots(freshClone);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Replace the component tag with the fresh clone
|
|
907
|
+
node.childNodes[i] = freshClone;
|
|
908
|
+
freshClone.parentNode = node;
|
|
909
|
+
replaced = true;
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
if (replaceComponentTag(child)) {
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
replaceComponentTag(document);
|
|
921
|
+
|
|
922
|
+
if (!replaced) {
|
|
923
|
+
return { html, scripts: [], styles: [] };
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Process component scripts with props and instance-scoped state
|
|
927
|
+
const processedScripts: ScriptBlock[] = [];
|
|
928
|
+
let scriptIndex = 0;
|
|
929
|
+
|
|
930
|
+
// First, extract state declarations to know what to transform
|
|
931
|
+
const allStateDecls = new Set<string>();
|
|
932
|
+
for (const script of component.scripts) {
|
|
933
|
+
const decls = extractStateDeclarations(script);
|
|
934
|
+
for (const [stateName] of decls.entries()) {
|
|
935
|
+
allStateDecls.add(stateName);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Props as direct bindings (NOT a props object)
|
|
940
|
+
// Inline props directly into component scope as const bindings
|
|
941
|
+
const propBindings: string[] = []; // Array of "const propName = ..." statements
|
|
942
|
+
const stateProps = new Set<string>(); // Track which props are state variables (bind to parent state)
|
|
943
|
+
const functionProps = new Set<string>(); // Track which props are function references (bind to parent function)
|
|
944
|
+
|
|
945
|
+
// Create case-insensitive lookup map for props (parse5 lowercases attribute names)
|
|
946
|
+
// Also create a reverse map: lowercase -> original key for case-insensitive lookup
|
|
947
|
+
const propsLowercase = new Map<string, string>(); // lowercase key -> original value
|
|
948
|
+
const propsKeyMap = new Map<string, string>(); // lowercase key -> original key
|
|
949
|
+
for (const [key, value] of props.entries()) {
|
|
950
|
+
const keyLower = key.toLowerCase();
|
|
951
|
+
propsLowercase.set(keyLower, value);
|
|
952
|
+
propsKeyMap.set(keyLower, key);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
for (const [propName, defaultValue] of component.props.entries()) {
|
|
956
|
+
// Look up prop value case-insensitively (parse5 lowercases attribute names)
|
|
957
|
+
// Try exact match first, then case-insensitive match
|
|
958
|
+
let propValue = props.get(propName);
|
|
959
|
+
if (propValue === undefined || propValue === "") {
|
|
960
|
+
propValue = propsLowercase.get(propName.toLowerCase());
|
|
961
|
+
}
|
|
962
|
+
if (propValue !== undefined && propValue !== "") {
|
|
963
|
+
// Handle both {variable} and variable syntax
|
|
964
|
+
let trimmedValue = propValue.trim();
|
|
965
|
+
// Strip braces if present: {increment} -> increment, {clicks} -> clicks
|
|
966
|
+
if (trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) {
|
|
967
|
+
trimmedValue = trimmedValue.slice(1, -1).trim();
|
|
968
|
+
}
|
|
969
|
+
// Check if propValue is a function reference or variable (not quoted, valid identifier)
|
|
970
|
+
const isFunctionOrVar = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmedValue) &&
|
|
971
|
+
!trimmedValue.startsWith('"') &&
|
|
972
|
+
!trimmedValue.startsWith("'");
|
|
973
|
+
|
|
974
|
+
if (isFunctionOrVar) {
|
|
975
|
+
// It's a function reference or state variable - bind to parent via window
|
|
976
|
+
// We'll determine if it's a function or state when we generate the binding code
|
|
977
|
+
propBindings.push(`const ${propName} = window.${trimmedValue};`);
|
|
978
|
+
// Note: trimmedValue (e.g., "increment") needs to be added to top-level functionProps
|
|
979
|
+
// This is handled earlier in the component processing loop (line ~209)
|
|
980
|
+
// The local functionProps set here is not used for mutation validation
|
|
981
|
+
} else if (/^["']/.test(trimmedValue)) {
|
|
982
|
+
// Already quoted string - use as-is
|
|
983
|
+
propBindings.push(`const ${propName} = ${trimmedValue};`);
|
|
984
|
+
} else {
|
|
985
|
+
// String literal - quote it
|
|
986
|
+
propBindings.push(`const ${propName} = "${trimmedValue}";`);
|
|
987
|
+
}
|
|
988
|
+
} else if (defaultValue === "?") {
|
|
989
|
+
// Optional prop from type Props - use undefined
|
|
990
|
+
propBindings.push(`const ${propName} = undefined;`);
|
|
991
|
+
} else if (defaultValue) {
|
|
992
|
+
// Use default value (already an expression)
|
|
993
|
+
propBindings.push(`const ${propName} = ${defaultValue};`);
|
|
994
|
+
} else {
|
|
995
|
+
// No default, use undefined
|
|
996
|
+
propBindings.push(`const ${propName} = undefined;`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const propsBindingsCode = propBindings.length > 0
|
|
1001
|
+
? propBindings.join("\n ") + "\n"
|
|
1002
|
+
: "";
|
|
1003
|
+
|
|
1004
|
+
// Replace hyphens in instanceId with underscores for valid JavaScript identifiers
|
|
1005
|
+
const safeInstanceId = instanceId.replace(/-/g, '_');
|
|
1006
|
+
|
|
1007
|
+
for (const script of component.scripts) {
|
|
1008
|
+
let processedScript = script;
|
|
1009
|
+
|
|
1010
|
+
// Remove type Props definitions (TypeScript types are compile-time only, not needed in JavaScript)
|
|
1011
|
+
// This keeps the compiled output clean and prevents confusion
|
|
1012
|
+
// Match: type Props = { ... } with multiline support and nested braces
|
|
1013
|
+
processedScript = processedScript.replace(/type\s+Props\s*=\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}\s*/gs, '');
|
|
1014
|
+
|
|
1015
|
+
// Transform state declarations to be instance-scoped
|
|
1016
|
+
for (const stateName of allStateDecls) {
|
|
1017
|
+
const instanceStateName = `__zen_${safeInstanceId}_${stateName}`;
|
|
1018
|
+
// Replace state declarations first
|
|
1019
|
+
processedScript = processedScript.replace(
|
|
1020
|
+
new RegExp(`state\\s+${stateName}\\s*=`, 'g'),
|
|
1021
|
+
`state ${instanceStateName} =`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Now replace state references (but not in state declarations, which we already transformed)
|
|
1026
|
+
// Replace ALL occurrences of state name with instance-scoped name
|
|
1027
|
+
// Important: Use window.property to ensure setter is triggered for mutations
|
|
1028
|
+
for (const stateName of allStateDecls) {
|
|
1029
|
+
const instanceStateName = `__zen_${safeInstanceId}_${stateName}`;
|
|
1030
|
+
// Replace state references using word boundaries
|
|
1031
|
+
// For assignments (including +=, -=, etc.), use window property to trigger setters
|
|
1032
|
+
// This handles: clicks, clicks += 1, clicks > 0, console.log("clicked", clicks)
|
|
1033
|
+
// But NOT: state clicks = 0 (already transformed above)
|
|
1034
|
+
|
|
1035
|
+
// Replace assignment operations first (including +=, -=, *=, /=, etc.)
|
|
1036
|
+
const assignmentOps = ['\\+=', '-=', '\\*=', '/=', '%=', '\\*\\*='];
|
|
1037
|
+
for (const op of assignmentOps) {
|
|
1038
|
+
const assignmentRegex = new RegExp(`\\b${stateName}\\s*${op}`, 'g');
|
|
1039
|
+
processedScript = processedScript.replace(assignmentRegex, `window.${instanceStateName} ${op.replace('\\', '')}`);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Then replace regular assignments (stateName = ...)
|
|
1043
|
+
const simpleAssignRegex = new RegExp(`\\b${stateName}\\s*=\\s*(?!\\s*[=!<>])`, 'g');
|
|
1044
|
+
processedScript = processedScript.replace(simpleAssignRegex, `window.${instanceStateName} =`);
|
|
1045
|
+
|
|
1046
|
+
// Finally, replace all other references (reads) with instance-scoped name
|
|
1047
|
+
// For reads, we can use the variable directly since the getter will be called
|
|
1048
|
+
const stateRefRegex = new RegExp(`\\b${stateName}\\b`, 'g');
|
|
1049
|
+
processedScript = processedScript.replace(stateRefRegex, instanceStateName);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Extract function declarations and register them on window for event delegation
|
|
1053
|
+
// Match: function functionName(...) { ... }
|
|
1054
|
+
const functionRegex = /function\s+(\w+)\s*\([^)]*\)\s*\{/g;
|
|
1055
|
+
const functionNames: string[] = [];
|
|
1056
|
+
let match;
|
|
1057
|
+
while ((match = functionRegex.exec(processedScript)) !== null) {
|
|
1058
|
+
if (match[1]) {
|
|
1059
|
+
functionNames.push(match[1]);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Replace props.propName references with direct propName (props are now direct bindings)
|
|
1064
|
+
// This handles cases where component code might still reference props.propName
|
|
1065
|
+
for (const propName of component.props.keys()) {
|
|
1066
|
+
const propsRefRegex = new RegExp(`props\\.${propName}\\b`, 'g');
|
|
1067
|
+
processedScript = processedScript.replace(propsRefRegex, propName);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Wrap script in IIFE to scope props and register functions on window
|
|
1071
|
+
// Props are inlined as direct bindings (NOT a props object)
|
|
1072
|
+
let wrappedScript = `(function() {\n // Props as direct bindings (inlined from parent)\n ${propsBindingsCode}`;
|
|
1073
|
+
|
|
1074
|
+
// Add the processed script content
|
|
1075
|
+
wrappedScript += processedScript;
|
|
1076
|
+
|
|
1077
|
+
// Register functions on window for event delegation with instance-scoped names
|
|
1078
|
+
// This ensures each component instance has unique function names
|
|
1079
|
+
if (functionNames.length > 0) {
|
|
1080
|
+
wrappedScript += `\n\n // Register functions on window for event delegation (instance-scoped)\n`;
|
|
1081
|
+
for (const funcName of functionNames) {
|
|
1082
|
+
const instanceFuncName = `__zen_${safeInstanceId}_${funcName}`;
|
|
1083
|
+
wrappedScript += ` window.${instanceFuncName} = ${funcName};\n`;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Register prop-based event handlers directly (no wrapper functions)
|
|
1088
|
+
// onclick="onClick" where onClick is a prop -> directly reference the bound function
|
|
1089
|
+
if (propHandlers.size > 0) {
|
|
1090
|
+
wrappedScript += `\n\n // Register prop-based event handlers directly (no wrappers)\n`;
|
|
1091
|
+
for (const [propName, eventType] of propHandlers.entries()) {
|
|
1092
|
+
// Create an instance-scoped reference to the prop function
|
|
1093
|
+
const handlerName = `__zen_${safeInstanceId}_${propName}`;
|
|
1094
|
+
wrappedScript += ` // Bind ${propName} prop to handler name for event delegation\n`;
|
|
1095
|
+
wrappedScript += ` const ${handlerName} = ${propName};\n`;
|
|
1096
|
+
wrappedScript += ` if (typeof ${handlerName} === 'function') {\n`;
|
|
1097
|
+
wrappedScript += ` window.${handlerName} = ${handlerName};\n`;
|
|
1098
|
+
wrappedScript += ` }\n\n`;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
wrappedScript += `})();`;
|
|
1103
|
+
|
|
1104
|
+
processedScripts.push({ content: wrappedScript, index: scriptIndex++ });
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Process component styles with scoping
|
|
1108
|
+
const processedStyles: StyleBlock[] = [];
|
|
1109
|
+
let styleIndex = 0;
|
|
1110
|
+
for (const style of component.styles) {
|
|
1111
|
+
// Scope styles to component instance
|
|
1112
|
+
const scopedStyle = scopeStyle(style, component.name);
|
|
1113
|
+
processedStyles.push({ content: scopedStyle, index: styleIndex++ });
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const serializedHtml = parse5.serialize(document);
|
|
1117
|
+
|
|
1118
|
+
return {
|
|
1119
|
+
html: serializedHtml,
|
|
1120
|
+
scripts: processedScripts,
|
|
1121
|
+
styles: processedStyles
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Clone a parse5 node
|
|
1127
|
+
*/
|
|
1128
|
+
function cloneNode(node: any): any {
|
|
1129
|
+
// For text nodes, just copy the value
|
|
1130
|
+
if (node.nodeName === "#text") {
|
|
1131
|
+
return {
|
|
1132
|
+
nodeName: node.nodeName,
|
|
1133
|
+
value: node.value,
|
|
1134
|
+
parentNode: undefined
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// For comment nodes, copy the data
|
|
1139
|
+
if (node.nodeName === "#comment") {
|
|
1140
|
+
return {
|
|
1141
|
+
nodeName: node.nodeName,
|
|
1142
|
+
data: node.data,
|
|
1143
|
+
parentNode: undefined
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// For document fragments
|
|
1148
|
+
if (node.nodeName === "#document-fragment") {
|
|
1149
|
+
const cloned: any = {
|
|
1150
|
+
nodeName: node.nodeName,
|
|
1151
|
+
childNodes: [],
|
|
1152
|
+
parentNode: undefined
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
if (node.childNodes && node.childNodes.length > 0) {
|
|
1156
|
+
cloned.childNodes = node.childNodes.map((child: any) => {
|
|
1157
|
+
const clonedChild = cloneNode(child);
|
|
1158
|
+
clonedChild.parentNode = cloned;
|
|
1159
|
+
return clonedChild;
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return cloned;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// For element nodes, clone with proper structure
|
|
1167
|
+
const cloned: any = {
|
|
1168
|
+
nodeName: node.nodeName,
|
|
1169
|
+
tagName: node.tagName,
|
|
1170
|
+
attrs: node.attrs ? node.attrs.map((a: any) => ({ ...a })) : [],
|
|
1171
|
+
childNodes: [],
|
|
1172
|
+
parentNode: undefined
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// Copy namespace URI if present (for SVG elements, etc.)
|
|
1176
|
+
if (node.namespaceURI) {
|
|
1177
|
+
cloned.namespaceURI = node.namespaceURI;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (node.childNodes && node.childNodes.length > 0) {
|
|
1181
|
+
cloned.childNodes = node.childNodes.map((child: any) => {
|
|
1182
|
+
const clonedChild = cloneNode(child);
|
|
1183
|
+
clonedChild.parentNode = cloned;
|
|
1184
|
+
return clonedChild;
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return cloned;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Scope CSS styles to component instances
|
|
1193
|
+
*/
|
|
1194
|
+
function scopeStyle(style: string, componentName: string): string {
|
|
1195
|
+
const scopeSelector = `[data-zen-component="${componentName}"]`;
|
|
1196
|
+
|
|
1197
|
+
// Simple CSS scoping - prepend scope selector to each rule
|
|
1198
|
+
// This is a simplified approach
|
|
1199
|
+
return style.replace(/([^{]+)\{/g, (match, selector) => {
|
|
1200
|
+
const trimmed = selector.trim();
|
|
1201
|
+
if (trimmed.startsWith("@")) {
|
|
1202
|
+
// Media queries, keyframes, etc.
|
|
1203
|
+
return match;
|
|
1204
|
+
}
|
|
1205
|
+
return `${scopeSelector} ${trimmed} {`;
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|