meno-core 1.0.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/bin/cli.ts +281 -0
- package/build-static.ts +298 -0
- package/bunfig.toml +39 -0
- package/entries/client-router.tsx +111 -0
- package/entries/server-router.tsx +71 -0
- package/lib/client/ClientInitializer.test.ts +9 -0
- package/lib/client/ClientInitializer.test.ts.skip +92 -0
- package/lib/client/ClientInitializer.ts +60 -0
- package/lib/client/ErrorBoundary.test.tsx +595 -0
- package/lib/client/ErrorBoundary.tsx +230 -0
- package/lib/client/componentRegistry.test.ts +165 -0
- package/lib/client/componentRegistry.ts +18 -0
- package/lib/client/contexts/ThemeContext.tsx +73 -0
- package/lib/client/core/ComponentBuilder.test.ts +677 -0
- package/lib/client/core/ComponentBuilder.ts +660 -0
- package/lib/client/core/ComponentRenderer.test.tsx +176 -0
- package/lib/client/core/ComponentRenderer.tsx +83 -0
- package/lib/client/core/cmsTemplateProcessor.ts +129 -0
- package/lib/client/elementRegistry.ts +81 -0
- package/lib/client/hmr/HMRManager.tsx +179 -0
- package/lib/client/hmr/index.ts +5 -0
- package/lib/client/hmrWebSocket.test.ts +9 -0
- package/lib/client/hmrWebSocket.ts +250 -0
- package/lib/client/hooks/useColorVariables.test.ts +166 -0
- package/lib/client/hooks/useColorVariables.ts +249 -0
- package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
- package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
- package/lib/client/hydration/HydrationUtils.test.ts +154 -0
- package/lib/client/hydration/HydrationUtils.ts +35 -0
- package/lib/client/i18nConfigService.test.ts +74 -0
- package/lib/client/i18nConfigService.ts +78 -0
- package/lib/client/index.ts +56 -0
- package/lib/client/navigation.test.ts +441 -0
- package/lib/client/navigation.ts +23 -0
- package/lib/client/responsiveStyleResolver.test.ts +491 -0
- package/lib/client/responsiveStyleResolver.ts +184 -0
- package/lib/client/routing/RouteLoader.test.ts +635 -0
- package/lib/client/routing/RouteLoader.ts +347 -0
- package/lib/client/routing/Router.tsx +382 -0
- package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
- package/lib/client/scripts/ScriptExecutor.ts +171 -0
- package/lib/client/scripts/formHandler.ts +103 -0
- package/lib/client/styleProcessor.test.ts +126 -0
- package/lib/client/styleProcessor.ts +92 -0
- package/lib/client/styles/StyleInjector.test.ts +354 -0
- package/lib/client/styles/StyleInjector.ts +154 -0
- package/lib/client/templateEngine.test.ts +660 -0
- package/lib/client/templateEngine.ts +667 -0
- package/lib/client/theme.test.ts +173 -0
- package/lib/client/theme.ts +159 -0
- package/lib/client/utils/toast.ts +46 -0
- package/lib/server/createServer.ts +170 -0
- package/lib/server/cssGenerator.test.ts +172 -0
- package/lib/server/cssGenerator.ts +58 -0
- package/lib/server/fileWatcher.ts +134 -0
- package/lib/server/index.ts +55 -0
- package/lib/server/jsonLoader.test.ts +103 -0
- package/lib/server/jsonLoader.ts +350 -0
- package/lib/server/middleware/cors.test.ts +177 -0
- package/lib/server/middleware/cors.ts +69 -0
- package/lib/server/middleware/errorHandler.test.ts +208 -0
- package/lib/server/middleware/errorHandler.ts +63 -0
- package/lib/server/middleware/index.ts +9 -0
- package/lib/server/middleware/logger.test.ts +233 -0
- package/lib/server/middleware/logger.ts +99 -0
- package/lib/server/pageCache.test.ts +167 -0
- package/lib/server/pageCache.ts +97 -0
- package/lib/server/projectContext.ts +51 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
- package/lib/server/providers/fileSystemPageProvider.ts +83 -0
- package/lib/server/routes/api/cms.test.ts +177 -0
- package/lib/server/routes/api/cms.ts +82 -0
- package/lib/server/routes/api/colors.ts +59 -0
- package/lib/server/routes/api/components.ts +70 -0
- package/lib/server/routes/api/config.test.ts +9 -0
- package/lib/server/routes/api/config.ts +28 -0
- package/lib/server/routes/api/core-routes.ts +182 -0
- package/lib/server/routes/api/functions.ts +170 -0
- package/lib/server/routes/api/index.ts +69 -0
- package/lib/server/routes/api/pages.ts +95 -0
- package/lib/server/routes/api/shared.test.ts +81 -0
- package/lib/server/routes/api/shared.ts +31 -0
- package/lib/server/routes/editor.test.ts +9 -0
- package/lib/server/routes/index.ts +104 -0
- package/lib/server/routes/pages.ts +161 -0
- package/lib/server/routes/static.ts +107 -0
- package/lib/server/services/ColorService.ts +193 -0
- package/lib/server/services/cmsService.test.ts +388 -0
- package/lib/server/services/cmsService.ts +296 -0
- package/lib/server/services/componentService.test.ts +276 -0
- package/lib/server/services/componentService.ts +346 -0
- package/lib/server/services/configService.ts +156 -0
- package/lib/server/services/fileWatcherService.ts +67 -0
- package/lib/server/services/index.ts +10 -0
- package/lib/server/services/pageService.test.ts +258 -0
- package/lib/server/services/pageService.ts +240 -0
- package/lib/server/ssrRenderer.test.ts +1005 -0
- package/lib/server/ssrRenderer.ts +878 -0
- package/lib/server/utilityClassGenerator.ts +11 -0
- package/lib/server/utils/index.ts +5 -0
- package/lib/server/utils/jsonLineMapper.test.ts +100 -0
- package/lib/server/utils/jsonLineMapper.ts +166 -0
- package/lib/server/validateStyleCoverage.test.ts +9 -0
- package/lib/server/validateStyleCoverage.ts +167 -0
- package/lib/server/websocketManager.test.ts +9 -0
- package/lib/server/websocketManager.ts +95 -0
- package/lib/shared/attributeNodeUtils.test.ts +152 -0
- package/lib/shared/attributeNodeUtils.ts +50 -0
- package/lib/shared/breakpoints.test.ts +166 -0
- package/lib/shared/breakpoints.ts +65 -0
- package/lib/shared/colorProperties.test.ts +111 -0
- package/lib/shared/colorProperties.ts +40 -0
- package/lib/shared/colorVariableUtils.test.ts +319 -0
- package/lib/shared/colorVariableUtils.ts +97 -0
- package/lib/shared/constants.test.ts +175 -0
- package/lib/shared/constants.ts +116 -0
- package/lib/shared/cssGeneration.ts +481 -0
- package/lib/shared/cssProperties.test.ts +252 -0
- package/lib/shared/cssProperties.ts +338 -0
- package/lib/shared/elementUtils.test.ts +245 -0
- package/lib/shared/elementUtils.ts +90 -0
- package/lib/shared/fontLoader.ts +97 -0
- package/lib/shared/i18n.test.ts +313 -0
- package/lib/shared/i18n.ts +286 -0
- package/lib/shared/index.ts +50 -0
- package/lib/shared/interfaces/contentProvider.test.ts +9 -0
- package/lib/shared/interfaces/contentProvider.ts +121 -0
- package/lib/shared/nodeUtils.test.ts +320 -0
- package/lib/shared/nodeUtils.ts +220 -0
- package/lib/shared/pathArrayUtils.test.ts +315 -0
- package/lib/shared/pathArrayUtils.ts +17 -0
- package/lib/shared/pathUtils.test.ts +260 -0
- package/lib/shared/pathUtils.ts +244 -0
- package/lib/shared/paths/Path.test.ts +74 -0
- package/lib/shared/paths/Path.ts +23 -0
- package/lib/shared/paths/PathConverter.test.ts +232 -0
- package/lib/shared/paths/PathConverter.ts +141 -0
- package/lib/shared/paths/PathUtils.ts +290 -0
- package/lib/shared/paths/PathValidator.test.ts +193 -0
- package/lib/shared/paths/PathValidator.ts +53 -0
- package/lib/shared/paths/index.ts +48 -0
- package/lib/shared/propResolver.test.ts +639 -0
- package/lib/shared/propResolver.ts +124 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
- package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
- package/lib/shared/registry/ClientRegistry.test.ts +26 -0
- package/lib/shared/registry/ClientRegistry.ts +15 -0
- package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
- package/lib/shared/registry/ComponentRegistry.ts +100 -0
- package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
- package/lib/shared/registry/NodeTypeManager.ts +94 -0
- package/lib/shared/registry/RegistryManager.test.ts +58 -0
- package/lib/shared/registry/RegistryManager.ts +60 -0
- package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
- package/lib/shared/registry/SSRRegistry.test.ts +26 -0
- package/lib/shared/registry/SSRRegistry.ts +15 -0
- package/lib/shared/registry/createNodeType.ts +175 -0
- package/lib/shared/registry/defineNodeType.ts +73 -0
- package/lib/shared/registry/fieldPresets.ts +109 -0
- package/lib/shared/registry/index.ts +50 -0
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
- package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
- package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
- package/lib/shared/registry/nodeTypes/index.ts +75 -0
- package/lib/shared/responsiveScaling.test.ts +268 -0
- package/lib/shared/responsiveScaling.ts +194 -0
- package/lib/shared/responsiveStyleUtils.test.ts +300 -0
- package/lib/shared/responsiveStyleUtils.ts +139 -0
- package/lib/shared/slugTranslator.test.ts +325 -0
- package/lib/shared/slugTranslator.ts +177 -0
- package/lib/shared/styleNodeUtils.test.ts +132 -0
- package/lib/shared/styleNodeUtils.ts +102 -0
- package/lib/shared/styleUtils.test.ts +238 -0
- package/lib/shared/styleUtils.ts +63 -0
- package/lib/shared/themeDefaults.test.ts +113 -0
- package/lib/shared/themeDefaults.ts +103 -0
- package/lib/shared/tree/PathBuilder.ts +383 -0
- package/lib/shared/treePathUtils.test.ts +539 -0
- package/lib/shared/treePathUtils.ts +339 -0
- package/lib/shared/types/api.ts +58 -0
- package/lib/shared/types/cms.ts +95 -0
- package/lib/shared/types/colors.ts +45 -0
- package/lib/shared/types/components.ts +121 -0
- package/lib/shared/types/errors.test.ts +103 -0
- package/lib/shared/types/errors.ts +69 -0
- package/lib/shared/types/index.ts +96 -0
- package/lib/shared/types/nodes.ts +20 -0
- package/lib/shared/types/rendering.ts +61 -0
- package/lib/shared/types/styles.ts +38 -0
- package/lib/shared/types.ts +11 -0
- package/lib/shared/utilityClassConfig.ts +287 -0
- package/lib/shared/utilityClassMapper.test.ts +140 -0
- package/lib/shared/utilityClassMapper.ts +229 -0
- package/lib/shared/utils/fileUtils.test.ts +99 -0
- package/lib/shared/utils/fileUtils.ts +56 -0
- package/lib/shared/utils.test.ts +261 -0
- package/lib/shared/utils.ts +84 -0
- package/lib/shared/validation/index.ts +7 -0
- package/lib/shared/validation/propValidator.test.ts +178 -0
- package/lib/shared/validation/propValidator.ts +238 -0
- package/lib/shared/validation/schemas.test.ts +177 -0
- package/lib/shared/validation/schemas.ts +401 -0
- package/lib/shared/validation/validators.test.ts +109 -0
- package/lib/shared/validation/validators.ts +304 -0
- package/lib/test-utils/dom-setup.ts +55 -0
- package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
- package/lib/test-utils/factories/DomMockFactory.ts +487 -0
- package/lib/test-utils/factories/EventMockFactory.ts +244 -0
- package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
- package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
- package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
- package/lib/test-utils/factories/index.ts +11 -0
- package/lib/test-utils/fixtures.ts +134 -0
- package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
- package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
- package/lib/test-utils/helpers/index.ts +6 -0
- package/lib/test-utils/helpers.test.ts +73 -0
- package/lib/test-utils/helpers.ts +90 -0
- package/lib/test-utils/index.ts +17 -0
- package/lib/test-utils/mockFactories.ts +92 -0
- package/lib/test-utils/mocks.ts +341 -0
- package/package.json +38 -0
- package/templates/index-router.html +34 -0
- package/tsconfig.json +14 -0
- package/vite.config.ts +43 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Engine
|
|
3
|
+
* Handles template evaluation with {{expression}} syntax and style mappings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { StyleMapping, ComponentDefinition, StructuredComponentDefinition, ComponentNode, HtmlNode, ComponentInstanceNode, EmbedNode, StyleObject, ResponsiveStyleObject, StyleValue, TemplateContext } from '../shared/types';
|
|
7
|
+
import { isResponsiveStyle } from '../shared/styleUtils';
|
|
8
|
+
import { normalizeStyle as normalizeStyleShared, mergeResponsiveStyles } from '../shared/responsiveStyleUtils';
|
|
9
|
+
import { DEFAULT_BREAKPOINTS } from '../shared/breakpoints';
|
|
10
|
+
import { NODE_TYPE } from '../shared/constants';
|
|
11
|
+
import { isValidNodeType, isComponentNode, isHtmlNode, isSlotMarker, isEmbedNode, isLocaleListNode, isObjectLinkNode } from '../shared/nodeUtils';
|
|
12
|
+
import { applyStylesToNode } from '../shared/styleNodeUtils';
|
|
13
|
+
|
|
14
|
+
// Re-export for backward compatibility
|
|
15
|
+
export { isResponsiveStyle };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize a style value - if it's responsive, merge it; if it's flat, return as-is
|
|
19
|
+
* Uses 'all' strategy for editor compatibility (merges all breakpoints)
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeStyle(style: StyleValue | null | undefined): StyleObject | null {
|
|
22
|
+
return normalizeStyleShared(style, 'all');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Helper to add a processed item to an array, handling both single items and arrays
|
|
27
|
+
* Converts numbers to strings to match ComponentNode['children'] type
|
|
28
|
+
*/
|
|
29
|
+
function addProcessedItemToArray(
|
|
30
|
+
item: ComponentNode | ComponentNode[] | string | number | null,
|
|
31
|
+
array: Array<ComponentNode | string>
|
|
32
|
+
): void {
|
|
33
|
+
if (item === null) return;
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(item)) {
|
|
36
|
+
array.push(...item);
|
|
37
|
+
} else if (typeof item === 'number') {
|
|
38
|
+
// Convert numbers to strings to match ComponentNode['children'] type
|
|
39
|
+
array.push(String(item));
|
|
40
|
+
} else {
|
|
41
|
+
array.push(item as ComponentNode | string);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Link mapping interface for object-link nodes
|
|
47
|
+
*/
|
|
48
|
+
export interface LinkMapping {
|
|
49
|
+
_mapping: true;
|
|
50
|
+
prop: string;
|
|
51
|
+
values?: Record<string, { href: string; target?: string }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Type guard for link mappings
|
|
56
|
+
*/
|
|
57
|
+
export function isLinkMapping(value: unknown): value is LinkMapping {
|
|
58
|
+
if (!value || typeof value !== 'object') return false;
|
|
59
|
+
const obj = value as Record<string, unknown>;
|
|
60
|
+
// values is optional - when empty/missing, acts as passthrough for link-type props
|
|
61
|
+
return obj._mapping === true && typeof obj.prop === 'string' && (obj.values === undefined || typeof obj.values === 'object');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolves a link mapping object to its actual value based on current props
|
|
66
|
+
*
|
|
67
|
+
* Supports two modes:
|
|
68
|
+
* 1. Value mapping: Maps a prop value to a predefined link object
|
|
69
|
+
* { _mapping: true, prop: "variant", values: { primary: { href: "/products" } } }
|
|
70
|
+
*
|
|
71
|
+
* 2. Passthrough: When values is empty/missing, uses the prop directly if it's a link object
|
|
72
|
+
* { _mapping: true, prop: "link" } - uses props.link directly
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // Value mapping
|
|
76
|
+
* resolveLinkMapping(
|
|
77
|
+
* { _mapping: true, prop: "variant", values: { primary: { href: "/products" } } },
|
|
78
|
+
* { variant: "primary" }
|
|
79
|
+
* ) // { href: "/products" }
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // Passthrough mode
|
|
83
|
+
* resolveLinkMapping(
|
|
84
|
+
* { _mapping: true, prop: "link" },
|
|
85
|
+
* { link: { href: "/about", target: "_blank" } }
|
|
86
|
+
* ) // { href: "/about", target: "_blank" }
|
|
87
|
+
*/
|
|
88
|
+
export function resolveLinkMapping(
|
|
89
|
+
mappingObj: unknown,
|
|
90
|
+
props: Record<string, unknown> | undefined
|
|
91
|
+
): { href: string; target?: string } | undefined {
|
|
92
|
+
if (!isLinkMapping(mappingObj)) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Guard against undefined props
|
|
97
|
+
if (!props) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { prop, values } = mappingObj;
|
|
102
|
+
const propValue = props[prop];
|
|
103
|
+
if (propValue === undefined || propValue === null) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Passthrough mode: if prop value is already a link object, use it directly
|
|
108
|
+
if (typeof propValue === 'object' && propValue !== null && 'href' in propValue) {
|
|
109
|
+
return propValue as { href: string; target?: string };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Value mapping mode: look up the prop value in the values map
|
|
113
|
+
if (values) {
|
|
114
|
+
const mappedValue = values[String(propValue)];
|
|
115
|
+
return mappedValue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolves a style mapping object to its actual value based on current props
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* resolveStyleMapping(
|
|
126
|
+
* { _mapping: true, prop: "variant", values: { primary: "#0070f3", secondary: "#f3f4f6" } },
|
|
127
|
+
* { variant: "primary" }
|
|
128
|
+
* ) // "#0070f3"
|
|
129
|
+
*/
|
|
130
|
+
export function resolveStyleMapping(
|
|
131
|
+
mappingObj: unknown,
|
|
132
|
+
props: Record<string, unknown> | undefined
|
|
133
|
+
): string | number | undefined {
|
|
134
|
+
if (!mappingObj || typeof mappingObj !== 'object') {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const mapping = mappingObj as StyleMapping;
|
|
139
|
+
if (!mapping || !mapping._mapping) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { prop, values } = mapping;
|
|
144
|
+
if (!prop || !values || typeof values !== 'object') {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Guard against undefined props
|
|
149
|
+
if (!props) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const propValue = props[prop];
|
|
154
|
+
if (propValue === undefined || propValue === null) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const mappedValue = values[String(propValue)];
|
|
159
|
+
return mappedValue !== undefined ? mappedValue : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Evaluates a template string with the given context
|
|
164
|
+
* Template format: {{expression}}
|
|
165
|
+
* Handles undefined variables gracefully by returning undefined instead of throwing
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* evaluateTemplate("{{name}}", { name: "John" }) // "John"
|
|
169
|
+
* evaluateTemplate("{{count + 1}}", { count: 5 }) // 6
|
|
170
|
+
* evaluateTemplate("{{undefinedVar}}", {}) // undefined (no error thrown)
|
|
171
|
+
*/
|
|
172
|
+
export function evaluateTemplate(
|
|
173
|
+
template: string,
|
|
174
|
+
context: Record<string, unknown>
|
|
175
|
+
): unknown {
|
|
176
|
+
if (typeof template !== 'string') return template;
|
|
177
|
+
|
|
178
|
+
const templateMatch = template.match(/^\{\{(.+)\}\}$/);
|
|
179
|
+
if (!templateMatch) return template;
|
|
180
|
+
|
|
181
|
+
const expression = templateMatch[1].trim();
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// Create function with context variables as parameters
|
|
185
|
+
// Wrap in try-catch to handle undefined variables gracefully
|
|
186
|
+
const func = new Function(
|
|
187
|
+
...Object.keys(context),
|
|
188
|
+
`
|
|
189
|
+
try {
|
|
190
|
+
return ${expression};
|
|
191
|
+
} catch (e) {
|
|
192
|
+
if (e instanceof ReferenceError) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
throw e;
|
|
196
|
+
}
|
|
197
|
+
`
|
|
198
|
+
);
|
|
199
|
+
const result = func(...Object.values(context));
|
|
200
|
+
// If result is undefined (ReferenceError or actual undefined), return template string for backward compatibility
|
|
201
|
+
return result === undefined ? template : result;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('Template evaluation error:', error, 'Expression:', expression, 'Context:', context);
|
|
204
|
+
// Return original template string on error for backward compatibility
|
|
205
|
+
return template;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Checks if a code string contains any template patterns {{...}}
|
|
211
|
+
*/
|
|
212
|
+
export function hasTemplates(code: string): boolean {
|
|
213
|
+
if (typeof code !== 'string') return false;
|
|
214
|
+
return /\{\{[^}]+\}\}/.test(code);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Processes template strings in JavaScript or CSS code
|
|
219
|
+
* Replaces {{propName}} or {{expression}} with evaluated values
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* processCodeTemplates("console.log('{{name}}');", { name: "John" })
|
|
223
|
+
* // "console.log('John');"
|
|
224
|
+
*
|
|
225
|
+
* processCodeTemplates(".card { color: {{color}}; }", { color: "#ff0000" })
|
|
226
|
+
* // ".card { color: #ff0000; }"
|
|
227
|
+
*/
|
|
228
|
+
export function processCodeTemplates(
|
|
229
|
+
code: string,
|
|
230
|
+
context: Record<string, unknown> | undefined
|
|
231
|
+
): string {
|
|
232
|
+
if (typeof code !== 'string') return code;
|
|
233
|
+
|
|
234
|
+
// Guard against undefined context
|
|
235
|
+
if (!context) {
|
|
236
|
+
return code;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Match all {{...}} patterns in the code
|
|
240
|
+
return code.replace(/\{\{([^}]+)\}\}/g, (match, expression) => {
|
|
241
|
+
const trimmedExpr = expression.trim();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// Filter context keys to only valid JavaScript identifiers
|
|
245
|
+
const validIdentifierPattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
246
|
+
const validKeys = Object.keys(context).filter(key => validIdentifierPattern.test(key));
|
|
247
|
+
const validValues = validKeys.map(key => context[key]);
|
|
248
|
+
|
|
249
|
+
// Create a function that evaluates the expression with valid context
|
|
250
|
+
const func = new Function(...validKeys, `return ${trimmedExpr}`);
|
|
251
|
+
const result = func(...validValues);
|
|
252
|
+
|
|
253
|
+
// Convert result to string, handling various types
|
|
254
|
+
if (result === null || result === undefined) {
|
|
255
|
+
return '';
|
|
256
|
+
}
|
|
257
|
+
if (typeof result === 'string') {
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
if (typeof result === 'number' || typeof result === 'boolean') {
|
|
261
|
+
return String(result);
|
|
262
|
+
}
|
|
263
|
+
// For objects/arrays, stringify them
|
|
264
|
+
return JSON.stringify(result);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
// ReferenceError means a variable (like cms) isn't available yet
|
|
267
|
+
// Return empty string for graceful degradation during loading
|
|
268
|
+
if (error instanceof ReferenceError) {
|
|
269
|
+
return '';
|
|
270
|
+
}
|
|
271
|
+
console.error('Code template evaluation error:', error, 'Expression:', trimmedExpr, 'Context:', context);
|
|
272
|
+
// Return original template if evaluation fails
|
|
273
|
+
return match;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Processes a component structure recursively, evaluating templates
|
|
280
|
+
* @param context - Template context containing props, componentDef, and extensible namespaces (e.g., cms, page)
|
|
281
|
+
* @param viewportWidth - Optional viewport width for responsive style resolution. If provided, uses 'viewport' strategy; otherwise uses 'all' strategy.
|
|
282
|
+
* @param instanceChildren - Optional children from component instance to replace { type: "children" } markers
|
|
283
|
+
*/
|
|
284
|
+
export function processStructure(
|
|
285
|
+
structure: ComponentNode | ComponentNode[] | string | number | null | undefined,
|
|
286
|
+
context: TemplateContext,
|
|
287
|
+
viewportWidth?: number,
|
|
288
|
+
instanceChildren?: ComponentNode['children'],
|
|
289
|
+
preserveResponsiveStyles: boolean = false
|
|
290
|
+
): ComponentNode | ComponentNode[] | string | number | null {
|
|
291
|
+
try {
|
|
292
|
+
// Handle null/undefined
|
|
293
|
+
if (structure === null || structure === undefined) return null;
|
|
294
|
+
|
|
295
|
+
// Preserve boolean values (don't convert false to null)
|
|
296
|
+
if (typeof structure === 'boolean') {
|
|
297
|
+
return structure as any;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof structure === 'string') {
|
|
301
|
+
// Build evaluation context from TemplateContext
|
|
302
|
+
// Start with props, then merge componentDef for backward compatibility
|
|
303
|
+
const evalContext: Record<string, unknown> = { ...context.props };
|
|
304
|
+
|
|
305
|
+
// Add componentDef properties to context (for backward compatibility)
|
|
306
|
+
if (context.componentDef && typeof context.componentDef === 'object') {
|
|
307
|
+
Object.assign(evalContext, context.componentDef as Record<string, unknown>);
|
|
308
|
+
}
|
|
309
|
+
// Future: Add context.cms, context.page, etc.
|
|
310
|
+
|
|
311
|
+
// Check if entire string is a complete template {{expr}}
|
|
312
|
+
// Use evaluateTemplate to preserve type (objects, arrays, numbers)
|
|
313
|
+
if (/^\{\{.+\}\}$/.test(structure)) {
|
|
314
|
+
const result = evaluateTemplate(structure, evalContext);
|
|
315
|
+
if (typeof result === 'string' || typeof result === 'number') {
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
if (result === undefined || result === null) {
|
|
319
|
+
return '';
|
|
320
|
+
}
|
|
321
|
+
return String(result);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check if string contains partial templates like "heading-{{size}}"
|
|
325
|
+
// Use processCodeTemplates for string interpolation
|
|
326
|
+
if (hasTemplates(structure)) {
|
|
327
|
+
return processCodeTemplates(structure, evalContext);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// No templates - return as-is
|
|
331
|
+
return structure;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (Array.isArray(structure)) {
|
|
335
|
+
const processed: Array<ComponentNode | string> = [];
|
|
336
|
+
for (const item of structure) {
|
|
337
|
+
// Check if this is a slot marker
|
|
338
|
+
if (isSlotMarker(item)) {
|
|
339
|
+
// Replace marker with instance children (if provided)
|
|
340
|
+
if (instanceChildren) {
|
|
341
|
+
const childrenArray = Array.isArray(instanceChildren) ? instanceChildren : [instanceChildren];
|
|
342
|
+
for (const child of childrenArray) {
|
|
343
|
+
const processedChild = processStructure(child, context, viewportWidth, instanceChildren, preserveResponsiveStyles);
|
|
344
|
+
addProcessedItemToArray(processedChild, processed);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// If no instance children, marker renders nothing (skip it)
|
|
348
|
+
} else {
|
|
349
|
+
// Regular item - process normally
|
|
350
|
+
const processedItem = processStructure(item, context, viewportWidth, instanceChildren, preserveResponsiveStyles);
|
|
351
|
+
addProcessedItemToArray(processedItem, processed);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return processed;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (typeof structure === 'object' && !Array.isArray(structure) && structure !== null) {
|
|
358
|
+
// Check if this is a slot marker (shouldn't happen here since we handle it in array processing, but guard anyway)
|
|
359
|
+
if (isSlotMarker(structure)) {
|
|
360
|
+
// This shouldn't happen in object processing, but if it does, return instance children
|
|
361
|
+
if (instanceChildren) {
|
|
362
|
+
const processed: Array<ComponentNode | string> = [];
|
|
363
|
+
const childrenArray = Array.isArray(instanceChildren) ? instanceChildren : [instanceChildren];
|
|
364
|
+
for (const child of childrenArray) {
|
|
365
|
+
const processedChild = processStructure(child, context, viewportWidth, instanceChildren, preserveResponsiveStyles);
|
|
366
|
+
addProcessedItemToArray(processedChild, processed);
|
|
367
|
+
}
|
|
368
|
+
return processed.length === 1 ? processed[0] : processed;
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Preserve type from input structure, default to "node" if not present or invalid
|
|
374
|
+
const inputNode = structure as ComponentNode;
|
|
375
|
+
const preservedType = inputNode.type && isValidNodeType(inputNode.type)
|
|
376
|
+
? inputNode.type
|
|
377
|
+
: NODE_TYPE.NODE;
|
|
378
|
+
|
|
379
|
+
// Create base processed object based on type
|
|
380
|
+
let processed: ComponentNode;
|
|
381
|
+
if (preservedType === NODE_TYPE.COMPONENT) {
|
|
382
|
+
processed = {
|
|
383
|
+
type: NODE_TYPE.COMPONENT,
|
|
384
|
+
component: '',
|
|
385
|
+
props: {},
|
|
386
|
+
children: [] as Array<ComponentNode | string>
|
|
387
|
+
} as ComponentInstanceNode;
|
|
388
|
+
} else if (preservedType === NODE_TYPE.EMBED) {
|
|
389
|
+
// Handle embed nodes - they have html content, not children
|
|
390
|
+
processed = {
|
|
391
|
+
type: NODE_TYPE.EMBED,
|
|
392
|
+
html: '',
|
|
393
|
+
} as any;
|
|
394
|
+
} else if (preservedType === NODE_TYPE.OBJECT_LINK) {
|
|
395
|
+
// Handle object-link nodes - they have href property
|
|
396
|
+
processed = {
|
|
397
|
+
type: NODE_TYPE.OBJECT_LINK,
|
|
398
|
+
href: '',
|
|
399
|
+
children: [] as Array<ComponentNode | string>
|
|
400
|
+
} as any;
|
|
401
|
+
} else if (preservedType === NODE_TYPE.LOCALE_LIST) {
|
|
402
|
+
// Handle locale-list nodes - they have style, itemStyle, activeItemStyle properties
|
|
403
|
+
processed = {
|
|
404
|
+
type: NODE_TYPE.LOCALE_LIST,
|
|
405
|
+
} as any;
|
|
406
|
+
} else {
|
|
407
|
+
processed = {
|
|
408
|
+
type: NODE_TYPE.NODE,
|
|
409
|
+
tag: 'div',
|
|
410
|
+
children: [] as Array<ComponentNode | string>
|
|
411
|
+
} as HtmlNode;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// First pass: process all keys and resolve style mappings
|
|
415
|
+
let resolvedStyle: Record<string, string | number> | null = null;
|
|
416
|
+
|
|
417
|
+
for (const [key, value] of Object.entries(structure)) {
|
|
418
|
+
try {
|
|
419
|
+
if (key === 'children') {
|
|
420
|
+
const processedChildren = processStructure(value, context, viewportWidth, instanceChildren, preserveResponsiveStyles);
|
|
421
|
+
if (Array.isArray(processedChildren)) {
|
|
422
|
+
processed.children = processedChildren;
|
|
423
|
+
} else if (processedChildren !== null && processedChildren !== undefined) {
|
|
424
|
+
processed.children = [processedChildren as ComponentNode | string];
|
|
425
|
+
}
|
|
426
|
+
} else if (key === 'tag') {
|
|
427
|
+
if (isHtmlNode(processed)) {
|
|
428
|
+
if (typeof value === 'string') {
|
|
429
|
+
// Process template in tag (e.g., "h{{size}}" -> "h1")
|
|
430
|
+
const evalContext: Record<string, unknown> = { ...context.props };
|
|
431
|
+
if (context.componentDef && typeof context.componentDef === 'object') {
|
|
432
|
+
Object.assign(evalContext, context.componentDef as Record<string, unknown>);
|
|
433
|
+
}
|
|
434
|
+
// Use processCodeTemplates to handle partial templates like "h{{size}}"
|
|
435
|
+
processed.tag = processCodeTemplates(value, evalContext);
|
|
436
|
+
} else {
|
|
437
|
+
processed.tag = String(value);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} else if (key === 'component') {
|
|
441
|
+
if (isComponentNode(processed)) {
|
|
442
|
+
if (typeof value === 'string') {
|
|
443
|
+
processed.component = value;
|
|
444
|
+
} else {
|
|
445
|
+
processed.component = String(value);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} else if (key === 'html') {
|
|
449
|
+
// Handle html property for embed nodes
|
|
450
|
+
if (preservedType === NODE_TYPE.EMBED) {
|
|
451
|
+
if (typeof value === 'string') {
|
|
452
|
+
(processed as any).html = value;
|
|
453
|
+
} else {
|
|
454
|
+
(processed as any).html = String(value);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} else if (key === 'type') {
|
|
458
|
+
// Preserve type field if valid - but don't change the structure type
|
|
459
|
+
if (typeof value === 'string' && isValidNodeType(value)) {
|
|
460
|
+
// Only update if type actually changed
|
|
461
|
+
if (value === NODE_TYPE.COMPONENT && !isComponentNode(processed)) {
|
|
462
|
+
// Convert to component node
|
|
463
|
+
const newProcessed: ComponentInstanceNode = {
|
|
464
|
+
type: NODE_TYPE.COMPONENT,
|
|
465
|
+
component: '',
|
|
466
|
+
props: (processed as any).props || {},
|
|
467
|
+
children: processed.children,
|
|
468
|
+
style: processed.style
|
|
469
|
+
};
|
|
470
|
+
processed = newProcessed;
|
|
471
|
+
} else if (value === NODE_TYPE.NODE && !isHtmlNode(processed)) {
|
|
472
|
+
// Convert to HTML node
|
|
473
|
+
const newProcessed: HtmlNode = {
|
|
474
|
+
type: NODE_TYPE.NODE,
|
|
475
|
+
tag: (processed as any).component || 'div',
|
|
476
|
+
children: processed.children,
|
|
477
|
+
style: processed.style
|
|
478
|
+
};
|
|
479
|
+
processed = newProcessed;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} else if (key === 'style' && typeof value === 'object' && value !== null) {
|
|
483
|
+
// Process style at top level - DO NOT recursively call processStructure
|
|
484
|
+
// as it would mangle style mapping objects { _mapping: true, prop: "size", values: {...} }
|
|
485
|
+
// into ComponentNode structures. Style objects need special handling.
|
|
486
|
+
const processedStyle = value as Record<string, unknown>;
|
|
487
|
+
if (processedStyle && typeof processedStyle === 'object' && !Array.isArray(processedStyle)) {
|
|
488
|
+
// Check if it's a responsive style object
|
|
489
|
+
if (isResponsiveStyle(processedStyle)) {
|
|
490
|
+
if (preserveResponsiveStyles) {
|
|
491
|
+
// Preserve responsive styles as-is (for SSR)
|
|
492
|
+
// Just resolve templates and mappings within each breakpoint
|
|
493
|
+
const resolvedResponsive: ResponsiveStyleObject = {};
|
|
494
|
+
for (const [bkeyName, bkeyValue] of Object.entries(processedStyle)) {
|
|
495
|
+
if (typeof bkeyValue === 'object' && bkeyValue !== null) {
|
|
496
|
+
resolvedResponsive[bkeyName] = {};
|
|
497
|
+
for (const [styleKey, styleValue] of Object.entries(bkeyValue)) {
|
|
498
|
+
const resolved = resolveStyleMapping(styleValue, context.props);
|
|
499
|
+
if (resolved !== undefined) {
|
|
500
|
+
resolvedResponsive[bkeyName]![styleKey] = resolved;
|
|
501
|
+
} else if (typeof styleValue === 'string') {
|
|
502
|
+
if (hasTemplates(styleValue)) {
|
|
503
|
+
const evaluated = processCodeTemplates(styleValue, context.props);
|
|
504
|
+
resolvedResponsive[bkeyName]![styleKey] = evaluated;
|
|
505
|
+
} else {
|
|
506
|
+
resolvedResponsive[bkeyName]![styleKey] = styleValue;
|
|
507
|
+
}
|
|
508
|
+
} else if (typeof styleValue === 'number') {
|
|
509
|
+
resolvedResponsive[bkeyName]![styleKey] = styleValue;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
resolvedStyle = resolvedResponsive as StyleObject;
|
|
515
|
+
} else {
|
|
516
|
+
// Preserve responsive styles for editor (same as SSR)
|
|
517
|
+
// responsiveStylesToClasses will generate prefixed classes (t-, mob-)
|
|
518
|
+
// CSS media queries will handle displaying the correct styles based on viewport
|
|
519
|
+
const resolvedResponsive: ResponsiveStyleObject = {};
|
|
520
|
+
for (const [bkeyName, bkeyValue] of Object.entries(processedStyle)) {
|
|
521
|
+
if (typeof bkeyValue === 'object' && bkeyValue !== null) {
|
|
522
|
+
resolvedResponsive[bkeyName] = {};
|
|
523
|
+
for (const [styleKey, styleValue] of Object.entries(bkeyValue)) {
|
|
524
|
+
const resolved = resolveStyleMapping(styleValue, context.props);
|
|
525
|
+
if (resolved !== undefined) {
|
|
526
|
+
resolvedResponsive[bkeyName]![styleKey] = resolved;
|
|
527
|
+
} else if (typeof styleValue === 'string') {
|
|
528
|
+
if (hasTemplates(styleValue)) {
|
|
529
|
+
const evaluated = processCodeTemplates(styleValue, context.props);
|
|
530
|
+
resolvedResponsive[bkeyName]![styleKey] = evaluated;
|
|
531
|
+
} else {
|
|
532
|
+
resolvedResponsive[bkeyName]![styleKey] = styleValue;
|
|
533
|
+
}
|
|
534
|
+
} else if (typeof styleValue === 'number') {
|
|
535
|
+
resolvedResponsive[bkeyName]![styleKey] = styleValue;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
resolvedStyle = resolvedResponsive as StyleObject;
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
// Legacy flat style object - resolve mappings and evaluate templates
|
|
544
|
+
resolvedStyle = {};
|
|
545
|
+
for (const [styleKey, styleValue] of Object.entries(processedStyle)) {
|
|
546
|
+
// First resolve style mappings
|
|
547
|
+
const resolved = resolveStyleMapping(styleValue, context.props);
|
|
548
|
+
if (resolved !== undefined) {
|
|
549
|
+
resolvedStyle[styleKey] = resolved;
|
|
550
|
+
} else if (typeof styleValue === 'string') {
|
|
551
|
+
// Evaluate template strings in style values (supports partial templates like "{{size}}px")
|
|
552
|
+
// Use processCodeTemplates to handle templates with text before/after
|
|
553
|
+
if (hasTemplates(styleValue)) {
|
|
554
|
+
const evaluated = processCodeTemplates(styleValue, context.props);
|
|
555
|
+
resolvedStyle[styleKey] = evaluated;
|
|
556
|
+
} else {
|
|
557
|
+
// No templates, keep original value
|
|
558
|
+
resolvedStyle[styleKey] = styleValue;
|
|
559
|
+
}
|
|
560
|
+
} else if (typeof styleValue === 'number') {
|
|
561
|
+
resolvedStyle[styleKey] = styleValue;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
} else if (key === 'props' && typeof value === 'object' && value !== null) {
|
|
567
|
+
// Process non-style props (for instance props only)
|
|
568
|
+
const processedProps = processStructure(value, context, viewportWidth, instanceChildren, preserveResponsiveStyles);
|
|
569
|
+
// Merge non-style props (style is handled separately at top level)
|
|
570
|
+
if (typeof processedProps === 'object' && !Array.isArray(processedProps) && processedProps !== null && processed.props) {
|
|
571
|
+
const propsObj = processedProps as unknown as Record<string, unknown>;
|
|
572
|
+
Object.assign(processed.props, propsObj);
|
|
573
|
+
// Remove style from props since we handle it at top level
|
|
574
|
+
if (processed.props) {
|
|
575
|
+
delete processed.props.style;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
} else if (key === 'hover') {
|
|
579
|
+
// Skip hover for now - it's not a standard React prop
|
|
580
|
+
// Could be implemented with onMouseEnter/onMouseLeave
|
|
581
|
+
} else if (key === 'href' && preservedType === NODE_TYPE.OBJECT_LINK) {
|
|
582
|
+
// Special handling for href in object-link nodes - resolve link mappings
|
|
583
|
+
if (isLinkMapping(value)) {
|
|
584
|
+
const resolved = resolveLinkMapping(value, context.props);
|
|
585
|
+
if (resolved) {
|
|
586
|
+
(processed as any).href = resolved.href;
|
|
587
|
+
if (resolved.target) {
|
|
588
|
+
(processed as any).attributes = {
|
|
589
|
+
...((processed as any).attributes || {}),
|
|
590
|
+
target: resolved.target
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
(processed as any).href = '#';
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
// Regular href value - process as template
|
|
598
|
+
const processedValue = processStructure(value, context, viewportWidth, instanceChildren, preserveResponsiveStyles);
|
|
599
|
+
if (processedValue !== null && processedValue !== undefined) {
|
|
600
|
+
(processed as any).href = processedValue;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} else if (key === 'attributes' && typeof value === 'object' && value !== null) {
|
|
604
|
+
// Special handling for attributes - process templates but don't treat as node structure
|
|
605
|
+
// This preserves type="checkbox" etc. which would otherwise be caught by node type handling
|
|
606
|
+
const processedAttributes: Record<string, unknown> = {};
|
|
607
|
+
const evalContext: Record<string, unknown> = { ...context.props };
|
|
608
|
+
if (context.componentDef && typeof context.componentDef === 'object') {
|
|
609
|
+
Object.assign(evalContext, context.componentDef as Record<string, unknown>);
|
|
610
|
+
}
|
|
611
|
+
for (const [attrKey, attrValue] of Object.entries(value)) {
|
|
612
|
+
if (typeof attrValue === 'string' && hasTemplates(attrValue)) {
|
|
613
|
+
// Check if entire string is a complete template {{expr}} - preserve type (boolean, number)
|
|
614
|
+
if (/^\{\{.+\}\}$/.test(attrValue)) {
|
|
615
|
+
const result = evaluateTemplate(attrValue, evalContext);
|
|
616
|
+
// Keep the original type (boolean, number, string)
|
|
617
|
+
processedAttributes[attrKey] = result === attrValue ? attrValue : result;
|
|
618
|
+
} else {
|
|
619
|
+
// Partial template like "prefix-{{value}}" - always string
|
|
620
|
+
processedAttributes[attrKey] = processCodeTemplates(attrValue, evalContext);
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
processedAttributes[attrKey] = attrValue;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
(processed as unknown as Record<string, unknown>).attributes = processedAttributes;
|
|
627
|
+
} else if (key !== 'type' && key !== 'children' && key !== 'style' && key !== 'props') {
|
|
628
|
+
const processedValue = processStructure(value, context, viewportWidth, instanceChildren, preserveResponsiveStyles);
|
|
629
|
+
// Only assign if it's a valid value
|
|
630
|
+
if (processedValue !== null && processedValue !== undefined) {
|
|
631
|
+
// Store extra properties on the node object
|
|
632
|
+
(processed as unknown as Record<string, unknown>)[key] = processedValue;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.error(`Error processing key "${key}":`, error);
|
|
637
|
+
// Continue processing other keys
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Apply resolved styles to the processed node based on its type
|
|
642
|
+
// Both SSR and Editor now preserve responsive styles for proper utility class generation
|
|
643
|
+
if (resolvedStyle && Object.keys(resolvedStyle).length > 0) {
|
|
644
|
+
if (isResponsiveStyle(resolvedStyle)) {
|
|
645
|
+
// Apply responsive styles directly to node.style or props.style
|
|
646
|
+
if (isComponentNode(processed)) {
|
|
647
|
+
processed.props = processed.props || {};
|
|
648
|
+
processed.props.style = resolvedStyle as ResponsiveStyleObject;
|
|
649
|
+
} else if (isHtmlNode(processed) || isEmbedNode(processed) || isLocaleListNode(processed) || isObjectLinkNode(processed)) {
|
|
650
|
+
processed.style = resolvedStyle as ResponsiveStyleObject;
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
// Legacy flat style object
|
|
654
|
+
applyStylesToNode(processed, resolvedStyle);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return processed;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return structure;
|
|
662
|
+
} catch (error) {
|
|
663
|
+
console.error('Error in processStructure:', error);
|
|
664
|
+
throw error; // Re-throw so it can be caught by error boundary
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|