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.
Files changed (231) hide show
  1. package/bin/cli.ts +281 -0
  2. package/build-static.ts +298 -0
  3. package/bunfig.toml +39 -0
  4. package/entries/client-router.tsx +111 -0
  5. package/entries/server-router.tsx +71 -0
  6. package/lib/client/ClientInitializer.test.ts +9 -0
  7. package/lib/client/ClientInitializer.test.ts.skip +92 -0
  8. package/lib/client/ClientInitializer.ts +60 -0
  9. package/lib/client/ErrorBoundary.test.tsx +595 -0
  10. package/lib/client/ErrorBoundary.tsx +230 -0
  11. package/lib/client/componentRegistry.test.ts +165 -0
  12. package/lib/client/componentRegistry.ts +18 -0
  13. package/lib/client/contexts/ThemeContext.tsx +73 -0
  14. package/lib/client/core/ComponentBuilder.test.ts +677 -0
  15. package/lib/client/core/ComponentBuilder.ts +660 -0
  16. package/lib/client/core/ComponentRenderer.test.tsx +176 -0
  17. package/lib/client/core/ComponentRenderer.tsx +83 -0
  18. package/lib/client/core/cmsTemplateProcessor.ts +129 -0
  19. package/lib/client/elementRegistry.ts +81 -0
  20. package/lib/client/hmr/HMRManager.tsx +179 -0
  21. package/lib/client/hmr/index.ts +5 -0
  22. package/lib/client/hmrWebSocket.test.ts +9 -0
  23. package/lib/client/hmrWebSocket.ts +250 -0
  24. package/lib/client/hooks/useColorVariables.test.ts +166 -0
  25. package/lib/client/hooks/useColorVariables.ts +249 -0
  26. package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
  27. package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
  28. package/lib/client/hydration/HydrationUtils.test.ts +154 -0
  29. package/lib/client/hydration/HydrationUtils.ts +35 -0
  30. package/lib/client/i18nConfigService.test.ts +74 -0
  31. package/lib/client/i18nConfigService.ts +78 -0
  32. package/lib/client/index.ts +56 -0
  33. package/lib/client/navigation.test.ts +441 -0
  34. package/lib/client/navigation.ts +23 -0
  35. package/lib/client/responsiveStyleResolver.test.ts +491 -0
  36. package/lib/client/responsiveStyleResolver.ts +184 -0
  37. package/lib/client/routing/RouteLoader.test.ts +635 -0
  38. package/lib/client/routing/RouteLoader.ts +347 -0
  39. package/lib/client/routing/Router.tsx +382 -0
  40. package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
  41. package/lib/client/scripts/ScriptExecutor.ts +171 -0
  42. package/lib/client/scripts/formHandler.ts +103 -0
  43. package/lib/client/styleProcessor.test.ts +126 -0
  44. package/lib/client/styleProcessor.ts +92 -0
  45. package/lib/client/styles/StyleInjector.test.ts +354 -0
  46. package/lib/client/styles/StyleInjector.ts +154 -0
  47. package/lib/client/templateEngine.test.ts +660 -0
  48. package/lib/client/templateEngine.ts +667 -0
  49. package/lib/client/theme.test.ts +173 -0
  50. package/lib/client/theme.ts +159 -0
  51. package/lib/client/utils/toast.ts +46 -0
  52. package/lib/server/createServer.ts +170 -0
  53. package/lib/server/cssGenerator.test.ts +172 -0
  54. package/lib/server/cssGenerator.ts +58 -0
  55. package/lib/server/fileWatcher.ts +134 -0
  56. package/lib/server/index.ts +55 -0
  57. package/lib/server/jsonLoader.test.ts +103 -0
  58. package/lib/server/jsonLoader.ts +350 -0
  59. package/lib/server/middleware/cors.test.ts +177 -0
  60. package/lib/server/middleware/cors.ts +69 -0
  61. package/lib/server/middleware/errorHandler.test.ts +208 -0
  62. package/lib/server/middleware/errorHandler.ts +63 -0
  63. package/lib/server/middleware/index.ts +9 -0
  64. package/lib/server/middleware/logger.test.ts +233 -0
  65. package/lib/server/middleware/logger.ts +99 -0
  66. package/lib/server/pageCache.test.ts +167 -0
  67. package/lib/server/pageCache.ts +97 -0
  68. package/lib/server/projectContext.ts +51 -0
  69. package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
  70. package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
  71. package/lib/server/providers/fileSystemPageProvider.ts +83 -0
  72. package/lib/server/routes/api/cms.test.ts +177 -0
  73. package/lib/server/routes/api/cms.ts +82 -0
  74. package/lib/server/routes/api/colors.ts +59 -0
  75. package/lib/server/routes/api/components.ts +70 -0
  76. package/lib/server/routes/api/config.test.ts +9 -0
  77. package/lib/server/routes/api/config.ts +28 -0
  78. package/lib/server/routes/api/core-routes.ts +182 -0
  79. package/lib/server/routes/api/functions.ts +170 -0
  80. package/lib/server/routes/api/index.ts +69 -0
  81. package/lib/server/routes/api/pages.ts +95 -0
  82. package/lib/server/routes/api/shared.test.ts +81 -0
  83. package/lib/server/routes/api/shared.ts +31 -0
  84. package/lib/server/routes/editor.test.ts +9 -0
  85. package/lib/server/routes/index.ts +104 -0
  86. package/lib/server/routes/pages.ts +161 -0
  87. package/lib/server/routes/static.ts +107 -0
  88. package/lib/server/services/ColorService.ts +193 -0
  89. package/lib/server/services/cmsService.test.ts +388 -0
  90. package/lib/server/services/cmsService.ts +296 -0
  91. package/lib/server/services/componentService.test.ts +276 -0
  92. package/lib/server/services/componentService.ts +346 -0
  93. package/lib/server/services/configService.ts +156 -0
  94. package/lib/server/services/fileWatcherService.ts +67 -0
  95. package/lib/server/services/index.ts +10 -0
  96. package/lib/server/services/pageService.test.ts +258 -0
  97. package/lib/server/services/pageService.ts +240 -0
  98. package/lib/server/ssrRenderer.test.ts +1005 -0
  99. package/lib/server/ssrRenderer.ts +878 -0
  100. package/lib/server/utilityClassGenerator.ts +11 -0
  101. package/lib/server/utils/index.ts +5 -0
  102. package/lib/server/utils/jsonLineMapper.test.ts +100 -0
  103. package/lib/server/utils/jsonLineMapper.ts +166 -0
  104. package/lib/server/validateStyleCoverage.test.ts +9 -0
  105. package/lib/server/validateStyleCoverage.ts +167 -0
  106. package/lib/server/websocketManager.test.ts +9 -0
  107. package/lib/server/websocketManager.ts +95 -0
  108. package/lib/shared/attributeNodeUtils.test.ts +152 -0
  109. package/lib/shared/attributeNodeUtils.ts +50 -0
  110. package/lib/shared/breakpoints.test.ts +166 -0
  111. package/lib/shared/breakpoints.ts +65 -0
  112. package/lib/shared/colorProperties.test.ts +111 -0
  113. package/lib/shared/colorProperties.ts +40 -0
  114. package/lib/shared/colorVariableUtils.test.ts +319 -0
  115. package/lib/shared/colorVariableUtils.ts +97 -0
  116. package/lib/shared/constants.test.ts +175 -0
  117. package/lib/shared/constants.ts +116 -0
  118. package/lib/shared/cssGeneration.ts +481 -0
  119. package/lib/shared/cssProperties.test.ts +252 -0
  120. package/lib/shared/cssProperties.ts +338 -0
  121. package/lib/shared/elementUtils.test.ts +245 -0
  122. package/lib/shared/elementUtils.ts +90 -0
  123. package/lib/shared/fontLoader.ts +97 -0
  124. package/lib/shared/i18n.test.ts +313 -0
  125. package/lib/shared/i18n.ts +286 -0
  126. package/lib/shared/index.ts +50 -0
  127. package/lib/shared/interfaces/contentProvider.test.ts +9 -0
  128. package/lib/shared/interfaces/contentProvider.ts +121 -0
  129. package/lib/shared/nodeUtils.test.ts +320 -0
  130. package/lib/shared/nodeUtils.ts +220 -0
  131. package/lib/shared/pathArrayUtils.test.ts +315 -0
  132. package/lib/shared/pathArrayUtils.ts +17 -0
  133. package/lib/shared/pathUtils.test.ts +260 -0
  134. package/lib/shared/pathUtils.ts +244 -0
  135. package/lib/shared/paths/Path.test.ts +74 -0
  136. package/lib/shared/paths/Path.ts +23 -0
  137. package/lib/shared/paths/PathConverter.test.ts +232 -0
  138. package/lib/shared/paths/PathConverter.ts +141 -0
  139. package/lib/shared/paths/PathUtils.ts +290 -0
  140. package/lib/shared/paths/PathValidator.test.ts +193 -0
  141. package/lib/shared/paths/PathValidator.ts +53 -0
  142. package/lib/shared/paths/index.ts +48 -0
  143. package/lib/shared/propResolver.test.ts +639 -0
  144. package/lib/shared/propResolver.ts +124 -0
  145. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
  146. package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
  147. package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
  148. package/lib/shared/registry/ClientRegistry.test.ts +26 -0
  149. package/lib/shared/registry/ClientRegistry.ts +15 -0
  150. package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
  151. package/lib/shared/registry/ComponentRegistry.ts +100 -0
  152. package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
  153. package/lib/shared/registry/NodeTypeManager.ts +94 -0
  154. package/lib/shared/registry/RegistryManager.test.ts +58 -0
  155. package/lib/shared/registry/RegistryManager.ts +60 -0
  156. package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
  157. package/lib/shared/registry/SSRRegistry.test.ts +26 -0
  158. package/lib/shared/registry/SSRRegistry.ts +15 -0
  159. package/lib/shared/registry/createNodeType.ts +175 -0
  160. package/lib/shared/registry/defineNodeType.ts +73 -0
  161. package/lib/shared/registry/fieldPresets.ts +109 -0
  162. package/lib/shared/registry/index.ts +50 -0
  163. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
  164. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
  165. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
  166. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
  167. package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
  168. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
  169. package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
  170. package/lib/shared/registry/nodeTypes/index.ts +75 -0
  171. package/lib/shared/responsiveScaling.test.ts +268 -0
  172. package/lib/shared/responsiveScaling.ts +194 -0
  173. package/lib/shared/responsiveStyleUtils.test.ts +300 -0
  174. package/lib/shared/responsiveStyleUtils.ts +139 -0
  175. package/lib/shared/slugTranslator.test.ts +325 -0
  176. package/lib/shared/slugTranslator.ts +177 -0
  177. package/lib/shared/styleNodeUtils.test.ts +132 -0
  178. package/lib/shared/styleNodeUtils.ts +102 -0
  179. package/lib/shared/styleUtils.test.ts +238 -0
  180. package/lib/shared/styleUtils.ts +63 -0
  181. package/lib/shared/themeDefaults.test.ts +113 -0
  182. package/lib/shared/themeDefaults.ts +103 -0
  183. package/lib/shared/tree/PathBuilder.ts +383 -0
  184. package/lib/shared/treePathUtils.test.ts +539 -0
  185. package/lib/shared/treePathUtils.ts +339 -0
  186. package/lib/shared/types/api.ts +58 -0
  187. package/lib/shared/types/cms.ts +95 -0
  188. package/lib/shared/types/colors.ts +45 -0
  189. package/lib/shared/types/components.ts +121 -0
  190. package/lib/shared/types/errors.test.ts +103 -0
  191. package/lib/shared/types/errors.ts +69 -0
  192. package/lib/shared/types/index.ts +96 -0
  193. package/lib/shared/types/nodes.ts +20 -0
  194. package/lib/shared/types/rendering.ts +61 -0
  195. package/lib/shared/types/styles.ts +38 -0
  196. package/lib/shared/types.ts +11 -0
  197. package/lib/shared/utilityClassConfig.ts +287 -0
  198. package/lib/shared/utilityClassMapper.test.ts +140 -0
  199. package/lib/shared/utilityClassMapper.ts +229 -0
  200. package/lib/shared/utils/fileUtils.test.ts +99 -0
  201. package/lib/shared/utils/fileUtils.ts +56 -0
  202. package/lib/shared/utils.test.ts +261 -0
  203. package/lib/shared/utils.ts +84 -0
  204. package/lib/shared/validation/index.ts +7 -0
  205. package/lib/shared/validation/propValidator.test.ts +178 -0
  206. package/lib/shared/validation/propValidator.ts +238 -0
  207. package/lib/shared/validation/schemas.test.ts +177 -0
  208. package/lib/shared/validation/schemas.ts +401 -0
  209. package/lib/shared/validation/validators.test.ts +109 -0
  210. package/lib/shared/validation/validators.ts +304 -0
  211. package/lib/test-utils/dom-setup.ts +55 -0
  212. package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
  213. package/lib/test-utils/factories/DomMockFactory.ts +487 -0
  214. package/lib/test-utils/factories/EventMockFactory.ts +244 -0
  215. package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
  216. package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
  217. package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
  218. package/lib/test-utils/factories/index.ts +11 -0
  219. package/lib/test-utils/fixtures.ts +134 -0
  220. package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
  221. package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
  222. package/lib/test-utils/helpers/index.ts +6 -0
  223. package/lib/test-utils/helpers.test.ts +73 -0
  224. package/lib/test-utils/helpers.ts +90 -0
  225. package/lib/test-utils/index.ts +17 -0
  226. package/lib/test-utils/mockFactories.ts +92 -0
  227. package/lib/test-utils/mocks.ts +341 -0
  228. package/package.json +38 -0
  229. package/templates/index-router.html +34 -0
  230. package/tsconfig.json +14 -0
  231. package/vite.config.ts +43 -0
@@ -0,0 +1,660 @@
1
+ /**
2
+ * Component Builder
3
+ * Builds React elements from component tree nodes with support for custom components,
4
+ * responsive styles, event handlers, and element registration.
5
+ */
6
+
7
+ import { createElement as h } from "react";
8
+ import type { ReactElement } from "react";
9
+
10
+ // Component registry and utilities
11
+ import { ComponentRegistry } from "../componentRegistry";
12
+ import { processStructure } from "../templateEngine";
13
+ import { NODE_TYPE } from "../../shared/constants";
14
+ import { ErrorBoundary } from "../ErrorBoundary";
15
+ import type { ComponentNode, StyleValue } from "../../shared/types";
16
+ import { isComponentNode, extractNodeProperties, isSlotMarker, isEmbedNode, isObjectLinkNode, isLocaleListNode } from "../../shared/nodeUtils";
17
+ import DOMPurify from "isomorphic-dompurify";
18
+ import { mergeNodeStyles } from "../../shared/styleNodeUtils";
19
+ import { extractAttributesFromNode } from "../../shared/attributeNodeUtils";
20
+ import { resolvePropsFromDefinition } from "../../shared/propResolver";
21
+ import { ElementRegistry } from "../elementRegistry";
22
+ import type { Path } from "../../shared/pathArrayUtils";
23
+ import type { I18nConfig } from "../../shared/types/components";
24
+ import { DEFAULT_I18N_CONFIG } from "../../shared/i18n";
25
+ import { getChildPath, pathToString } from "../../shared/pathArrayUtils";
26
+ import { navigateTo } from "../navigation";
27
+ import { responsiveStylesToClasses } from "../../shared/utilityClassMapper";
28
+ import { processCMSTemplate, processCMSPropsTemplate } from "./cmsTemplateProcessor";
29
+
30
+ type ComponentProps = Record<string, unknown>;
31
+
32
+ export interface ComponentBuilderConfig {
33
+ componentRegistry: ComponentRegistry;
34
+ elementRegistry: ElementRegistry;
35
+ }
36
+
37
+ export interface BuildComponentOptions {
38
+ node: ComponentNode | ComponentNode[] | string | number | null | undefined;
39
+ key?: number;
40
+ customProps?: ComponentProps;
41
+ elementPath?: Path;
42
+ parentComponentName?: string | null;
43
+ viewportWidth?: number;
44
+ componentContext?: string | null;
45
+ locale?: string;
46
+ i18nConfig?: I18nConfig;
47
+ cmsContext?: Record<string, unknown> | null;
48
+ /** CMS locale for i18n field resolution */
49
+ cmsLocale?: string | null;
50
+ }
51
+
52
+ /**
53
+ * ComponentBuilder class for building React elements from component tree nodes
54
+ */
55
+ export class ComponentBuilder {
56
+ private componentRegistry: ComponentRegistry;
57
+ private elementRegistry: ElementRegistry;
58
+
59
+ constructor(config: ComponentBuilderConfig) {
60
+ this.componentRegistry = config.componentRegistry;
61
+ this.elementRegistry = config.elementRegistry;
62
+ }
63
+
64
+ /**
65
+ * Helper function to create onClick handlers from JSON-defined onClick values
66
+ */
67
+ private createOnClickHandler(onClickValue: unknown): () => void {
68
+ return () => {
69
+ alert(String(onClickValue));
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Determines the parent component name for nested component instances.
75
+ * Priority: componentContext (if inside component definition) > parentComponentName (if inside another instance) > componentName (fallback for page-level components)
76
+ */
77
+ private getParentComponentNameForNestedComponent(
78
+ componentContext: string | null,
79
+ parentComponentName: string | null,
80
+ componentName: string | null
81
+ ): string | null {
82
+ return componentContext || parentComponentName || componentName || null;
83
+ }
84
+
85
+ /**
86
+ * Determines the effective parent component name when building children.
87
+ * Uses componentContext if available, otherwise falls back to parentComponentName.
88
+ */
89
+ private getEffectiveParentComponentName(
90
+ componentContext: string | null,
91
+ parentComponentName: string | null
92
+ ): string | null {
93
+ return componentContext || parentComponentName;
94
+ }
95
+
96
+ /**
97
+ * Find the path to the slot marker in a component structure
98
+ * This helps determine where children should be placed and what paths they should have
99
+ */
100
+ private findSlotMarkerPath(structure: ComponentNode | undefined, currentPath: Path = [0]): Path | null {
101
+ if (!structure) return null;
102
+
103
+ // Check if this node has children (SlotMarker doesn't have children)
104
+ if (isSlotMarker(structure)) return null;
105
+
106
+ const nodeChildren = (structure as any).children;
107
+ if (!nodeChildren || !Array.isArray(nodeChildren)) return null;
108
+
109
+ for (let i = 0; i < nodeChildren.length; i++) {
110
+ const child = nodeChildren[i];
111
+
112
+ // Check if this is a slot marker
113
+ if (isSlotMarker(child)) {
114
+ return getChildPath(currentPath, i);
115
+ }
116
+
117
+ // Recursively search in nested structures
118
+ if (typeof child === 'object' && child !== null && !Array.isArray(child) && 'type' in child) {
119
+ const nestedPath = this.findSlotMarkerPath(child as ComponentNode, getChildPath(currentPath, i));
120
+ if (nestedPath) return nestedPath;
121
+ }
122
+ }
123
+
124
+ return null;
125
+ }
126
+
127
+ /**
128
+ * Builds children elements recursively with proper component context tracking.
129
+ */
130
+ buildChildren(
131
+ children: (string | ComponentNode)[] | string | ComponentNode | undefined,
132
+ elementPath: Path,
133
+ parentComponentName: string | null,
134
+ viewportWidth: number,
135
+ componentContext: string | null,
136
+ locale?: string,
137
+ i18nConfig?: I18nConfig,
138
+ cmsContext?: Record<string, unknown> | null,
139
+ cmsLocale?: string | null
140
+ ): ReactElement | ReactElement[] | string | number | null {
141
+ if (Array.isArray(children)) {
142
+ return children
143
+ .map((child, index) =>
144
+ this.buildComponent({
145
+ node: child,
146
+ key: index,
147
+ customProps: {},
148
+ elementPath: getChildPath(elementPath, index),
149
+ parentComponentName,
150
+ viewportWidth,
151
+ componentContext,
152
+ locale,
153
+ i18nConfig,
154
+ cmsContext,
155
+ cmsLocale
156
+ })
157
+ )
158
+ .filter((item): item is ReactElement | string | number => item !== null) as ReactElement[];
159
+ }
160
+ return this.buildComponent({
161
+ node: children,
162
+ key: 0,
163
+ customProps: {},
164
+ elementPath: getChildPath(elementPath, 0),
165
+ parentComponentName,
166
+ viewportWidth,
167
+ componentContext,
168
+ locale,
169
+ i18nConfig,
170
+ cmsContext,
171
+ cmsLocale
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Component builder from JSON tree with support for custom components
177
+ * viewportWidth is passed down to resolve responsive styles based on actual viewport
178
+ */
179
+ buildComponent(options: BuildComponentOptions): ReactElement | ReactElement[] | string | number | null {
180
+ const {
181
+ node,
182
+ key = 0,
183
+ customProps = {},
184
+ elementPath = [0],
185
+ parentComponentName = null,
186
+ viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 1920,
187
+ componentContext = null,
188
+ locale,
189
+ i18nConfig,
190
+ cmsContext = null,
191
+ cmsLocale = null
192
+ } = options;
193
+
194
+ if (!node) return null;
195
+
196
+ // Handle text nodes - process CMS templates if context is available
197
+ if (typeof node === 'string') {
198
+ if (cmsContext && node.includes('{{cms.')) {
199
+ // Use cmsLocale for i18n resolution if available, otherwise use page locale
200
+ return processCMSTemplate(node, cmsContext, cmsLocale || locale);
201
+ }
202
+ return node;
203
+ }
204
+
205
+ if (typeof node === 'number') {
206
+ return node;
207
+ }
208
+
209
+ if (Array.isArray(node)) {
210
+ const result = node.map((child, index) => {
211
+ const childPath = getChildPath(elementPath, index);
212
+ return this.buildComponent({
213
+ node: child,
214
+ key: index,
215
+ customProps,
216
+ elementPath: childPath,
217
+ parentComponentName,
218
+ viewportWidth,
219
+ componentContext,
220
+ locale,
221
+ i18nConfig,
222
+ cmsContext,
223
+ cmsLocale
224
+ });
225
+ }).filter(
226
+ (item): item is ReactElement | string | number => item !== null
227
+ );
228
+ return result as ReactElement[];
229
+ }
230
+
231
+ // Handle new format: style at top level (not props.style)
232
+ const nodeType = typeof node === 'object' && node !== null && 'type' in node ? node.type : undefined;
233
+ const children = typeof node === 'object' && node !== null && 'children' in node ? (node.children || []) : [];
234
+
235
+ // Handle embed nodes early (before property extraction)
236
+ if (nodeType === NODE_TYPE.EMBED && isEmbedNode(node)) {
237
+ // Sanitize HTML with allowlist for SVG and common elements
238
+ // Script tags and event handlers are still removed for security
239
+ const sanitizedHtml = DOMPurify.sanitize(node.html, {
240
+ ALLOWED_TAGS: ['svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'text', 'tspan', 'image', 'defs', 'use', 'linearGradient', 'radialGradient', 'stop', 'clipPath', 'mask', 'pattern', 'marker', 'symbol', 'a', 'div', 'span', 'p', 'br', 'button', 'img', 'iframe', 'video', 'audio', 'source', 'canvas'],
241
+ ALLOWED_ATTR: ['class', 'id', 'style', 'width', 'height', 'viewBox', 'xmlns', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'd', 'cx', 'cy', 'r', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'points', 'href', 'src', 'alt', 'target', 'rel', 'data-*', 'aria-*', 'transform', 'opacity', 'fill-opacity', 'stroke-opacity', 'font-size', 'font-family', 'text-anchor', 'dominant-baseline', 'offset', 'stop-color', 'stop-opacity'],
242
+ KEEP_CONTENT: true
243
+ });
244
+ const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
245
+
246
+ // Extract attributes from node
247
+ const extractedAttributes = extractAttributesFromNode(node);
248
+
249
+ // Build embed node props with all features
250
+ const embedProps: Record<string, any> = {
251
+ key,
252
+ 'data-element-path': pathToString(elementPath),
253
+ 'data-embed-node': 'true',
254
+ ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false),
255
+ dangerouslySetInnerHTML: { __html: sanitizedHtml }
256
+ };
257
+
258
+ // Convert embed styles to utility classes
259
+ if (node.style) {
260
+ const utilityClasses = responsiveStylesToClasses(node.style);
261
+ if (utilityClasses.length > 0) {
262
+ const existingClassName = (extractedAttributes.className || '') as string;
263
+ const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
264
+ embedProps.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
265
+ }
266
+ }
267
+
268
+ // Add extracted attributes (like class, id, data-*, aria-*, etc.)
269
+ if (Object.keys(extractedAttributes).length > 0) {
270
+ Object.assign(embedProps, extractedAttributes);
271
+ }
272
+
273
+ // Add parent component context
274
+ if (effectiveParentComponentName) {
275
+ embedProps['data-parent-component'] = effectiveParentComponentName;
276
+ }
277
+ if (componentContext) {
278
+ embedProps['data-component-context'] = componentContext;
279
+ }
280
+
281
+ return h('div', embedProps);
282
+ }
283
+
284
+ // Handle object link nodes (render as div in editor)
285
+ if (nodeType === NODE_TYPE.OBJECT_LINK && isObjectLinkNode(node)) {
286
+ const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
287
+
288
+ // Extract attributes
289
+ const extractedAttributes = extractAttributesFromNode(node);
290
+
291
+ // Build object link props (renders as div in editor)
292
+ const objectLinkProps: Record<string, any> = {
293
+ key,
294
+ 'data-element-path': pathToString(elementPath),
295
+ 'data-object-link-node': 'true',
296
+ ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false)
297
+ };
298
+
299
+ // Convert object link styles to utility classes
300
+ if (node.style) {
301
+ const utilityClasses = responsiveStylesToClasses(node.style);
302
+ if (utilityClasses.length > 0) {
303
+ const existingClassName = (extractedAttributes.className || '') as string;
304
+ const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
305
+ objectLinkProps.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
306
+ }
307
+ }
308
+
309
+ // Add extracted attributes (like class, id, data-*, aria-*, etc.)
310
+ if (Object.keys(extractedAttributes).length > 0) {
311
+ Object.assign(objectLinkProps, extractedAttributes);
312
+ }
313
+
314
+ // Add parent component context
315
+ if (effectiveParentComponentName) {
316
+ objectLinkProps['data-parent-component'] = effectiveParentComponentName;
317
+ }
318
+ if (componentContext) {
319
+ objectLinkProps['data-component-context'] = componentContext;
320
+ }
321
+
322
+ // Build children recursively
323
+ const objectLinkChildren = this.buildChildren(children, elementPath, effectiveParentComponentName, viewportWidth, componentContext, locale, i18nConfig, cmsContext, cmsLocale);
324
+
325
+ return h('div', objectLinkProps, objectLinkChildren);
326
+ }
327
+
328
+ // Handle locale-list nodes (render placeholder in editor)
329
+ if (nodeType === NODE_TYPE.LOCALE_LIST && isLocaleListNode(node)) {
330
+ const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
331
+
332
+ // Extract attributes
333
+ const extractedAttributes = extractAttributesFromNode(node);
334
+
335
+ // Build locale list props
336
+ const localeListProps: Record<string, any> = {
337
+ key,
338
+ 'data-element-path': pathToString(elementPath),
339
+ 'data-locale-list': 'true',
340
+ ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false)
341
+ };
342
+
343
+ // Convert container styles to utility classes
344
+ if (node.style) {
345
+ const utilityClasses = responsiveStylesToClasses(node.style);
346
+ if (utilityClasses.length > 0) {
347
+ const existingClassName = (extractedAttributes.className || '') as string;
348
+ const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
349
+ localeListProps.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
350
+ }
351
+ }
352
+
353
+ // Add extracted attributes
354
+ if (Object.keys(extractedAttributes).length > 0) {
355
+ Object.assign(localeListProps, extractedAttributes);
356
+ }
357
+
358
+ // Add parent component context
359
+ if (effectiveParentComponentName) {
360
+ localeListProps['data-parent-component'] = effectiveParentComponentName;
361
+ }
362
+ if (componentContext) {
363
+ localeListProps['data-component-context'] = componentContext;
364
+ }
365
+
366
+ // Build locale links for editor preview using actual i18n config
367
+ const showCurrent = node.showCurrent !== false;
368
+ const showSeparator = node.showSeparator !== false;
369
+ const showFlag = node.showFlag !== false;
370
+ // Use actual locales from i18nConfig (LocaleConfig[])
371
+ const configLocales = i18nConfig?.locales || [];
372
+ const currentLocaleCode = locale || i18nConfig?.defaultLocale || 'en';
373
+
374
+ // Convert item styles, active item styles, separator styles, and flag styles to utility classes
375
+ const itemClasses = node.itemStyle ? responsiveStylesToClasses(node.itemStyle) : [];
376
+ const activeItemClasses = node.activeItemStyle ? responsiveStylesToClasses(node.activeItemStyle) : [];
377
+ const separatorClasses = node.separatorStyle ? responsiveStylesToClasses(node.separatorStyle) : [];
378
+ const flagClasses = node.flagStyle ? responsiveStylesToClasses(node.flagStyle) : [];
379
+
380
+ // Build locale links from config
381
+ const linkElements: ReactElement[] = [];
382
+ for (let i = 0; i < configLocales.length; i++) {
383
+ const localeConfig = configLocales[i];
384
+ const isCurrent = localeConfig.code === currentLocaleCode;
385
+ if (!showCurrent && isCurrent) continue;
386
+
387
+ // Add separator between links (empty span styled via separatorStyle)
388
+ if (showSeparator && linkElements.length > 0) {
389
+ linkElements.push(h('span', {
390
+ key: `sep-${i}`,
391
+ className: separatorClasses.length > 0 ? separatorClasses.join(' ') : undefined
392
+ }));
393
+ }
394
+
395
+ // Build link element content - flag icon (if enabled and exists) + nativeName in span
396
+ const linkContent: (ReactElement | string)[] = [];
397
+ if (showFlag && localeConfig.icon) {
398
+ linkContent.push(h('img', {
399
+ key: 'flag',
400
+ src: localeConfig.icon,
401
+ alt: `${localeConfig.name} flag`,
402
+ className: flagClasses.length > 0 ? flagClasses.join(' ') : undefined
403
+ }));
404
+ }
405
+ linkContent.push(h('div', { key: 'text' }, localeConfig.nativeName));
406
+
407
+ // Current item gets both itemClasses + activeItemClasses (additive/override)
408
+ const linkClasses = isCurrent ? [...itemClasses, ...activeItemClasses] : itemClasses;
409
+ linkElements.push(h('div', {
410
+ key: `locale-${localeConfig.code}`,
411
+ 'data-current': isCurrent ? 'true' : 'false',
412
+ 'data-locale': localeConfig.code,
413
+ className: linkClasses.length > 0 ? linkClasses.join(' ') : undefined,
414
+ style: { cursor: 'pointer' }
415
+ }, ...linkContent));
416
+ }
417
+
418
+ return h('div', localeListProps, linkElements);
419
+ }
420
+
421
+ // Extract node properties based on type
422
+ const { tag, componentName, props: nodeProps } = extractNodeProperties(node);
423
+
424
+ // Filter internal props from nodeProps before building props object
425
+ // This prevents node.type (e.g., "node") from being passed to DOM while allowing
426
+ // attributes.type (e.g., "text" for inputs) to be preserved when merged later
427
+ const imageOnlyProps = ['src', 'alt', 'loading', 'width', 'height'];
428
+ const internalProps = ['type', 'tag', 'component', 'props', 'children', 'html', 'style', ...imageOnlyProps];
429
+ let props: Record<string, unknown> = {};
430
+ for (const [key, value] of Object.entries(nodeProps)) {
431
+ if (internalProps.includes(key)) {
432
+ // Keep image properties if this is an img tag
433
+ if (tag === 'img' && imageOnlyProps.includes(key)) {
434
+ props[key] = value;
435
+ }
436
+ // else skip - it's an internal prop
437
+ } else {
438
+ props[key] = value;
439
+ }
440
+ }
441
+
442
+ // Extract styles from node and convert to utility classes
443
+ const nodeStyle = node.style || (isComponentNode(node) ? node.props?.style : undefined);
444
+
445
+ if (nodeStyle) {
446
+ // Convert style objects to utility classes
447
+ const utilityClasses = responsiveStylesToClasses(nodeStyle);
448
+ if (utilityClasses.length > 0) {
449
+ const existingClassName = (props.className || '') as string;
450
+ const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
451
+ props.className = [...classArray, ...utilityClasses].filter(Boolean).join(' ');
452
+ }
453
+ }
454
+
455
+ // Extract attributes from node and merge into props
456
+ // Attributes override props with the same key
457
+ // Note: Internal props were already filtered above, so attributes like type="text" are preserved
458
+ const extractedAttributes = extractAttributesFromNode(node);
459
+ if (Object.keys(extractedAttributes).length > 0) {
460
+ props = { ...props, ...extractedAttributes };
461
+ }
462
+
463
+ // Check if this is a custom component reference
464
+ if (nodeType === NODE_TYPE.COMPONENT && componentName && this.componentRegistry.has(componentName)) {
465
+ const componentDef = this.componentRegistry.get(componentName);
466
+
467
+ if (!componentDef) return null;
468
+
469
+ try {
470
+ // Extract structured component definition
471
+ const structuredComponentDef = componentDef.component;
472
+ if (!structuredComponentDef) {
473
+ return h(ErrorBoundary, {
474
+ key,
475
+ componentName: componentName,
476
+ level: 'component',
477
+ }, null);
478
+ }
479
+
480
+ // Resolve props with defaults from interface definition (with i18n support)
481
+ const resolvedProps = resolvePropsFromDefinition(
482
+ structuredComponentDef,
483
+ props,
484
+ children,
485
+ locale,
486
+ i18nConfig
487
+ );
488
+
489
+ // Process the structure with resolved props
490
+ // processStructure now handles structure.style and merges into props.style
491
+ // Pass viewportWidth to respect responsive breakpoints in preview
492
+ // Pass instance children to replace { type: "children" } markers
493
+ const processedStructure = processStructure(
494
+ structuredComponentDef.structure,
495
+ { props: resolvedProps, componentDef: structuredComponentDef },
496
+ viewportWidth,
497
+ children
498
+ );
499
+
500
+ // Type guard: ensure processedStructure is a ComponentNode
501
+ if (!processedStructure || typeof processedStructure === 'string' || typeof processedStructure === 'number' || Array.isArray(processedStructure)) {
502
+ return null;
503
+ }
504
+
505
+ // Merge instance style overrides (from component instance props) with structure styles
506
+ if (props.style && typeof props.style === 'object') {
507
+ mergeNodeStyles(processedStructure, props.style as StyleValue, viewportWidth);
508
+ }
509
+
510
+ // Add event handlers to props (only for component instances)
511
+ if (props.onClick && isComponentNode(processedStructure)) {
512
+ if (!processedStructure.props) {
513
+ processedStructure.props = {};
514
+ }
515
+ processedStructure.props.onClick = this.createOnClickHandler(props.onClick);
516
+ }
517
+
518
+ // Build the component instance's structure with proper component context tracking
519
+ // Elements inside this component will have parentComponentName set via closure variables
520
+ // Component root will have props stored in registry (no DOM attributes needed)
521
+ const nestedParentComponentName = this.getParentComponentNameForNestedComponent(
522
+ componentContext,
523
+ parentComponentName,
524
+ componentName
525
+ );
526
+
527
+ const componentElement = this.buildComponent({
528
+ node: processedStructure,
529
+ key,
530
+ customProps: {
531
+ // Store resolved props in a special key to pass to registry (not added to DOM)
532
+ '__componentProps': resolvedProps
533
+ },
534
+ elementPath,
535
+ parentComponentName: nestedParentComponentName,
536
+ viewportWidth,
537
+ componentContext: componentName,
538
+ locale,
539
+ i18nConfig,
540
+ cmsContext,
541
+ cmsLocale
542
+ });
543
+ return h(ErrorBoundary, {
544
+ key,
545
+ componentName: componentName,
546
+ level: 'component',
547
+ }, componentElement);
548
+
549
+ } catch (error) {
550
+ return h(ErrorBoundary, {
551
+ key,
552
+ componentName: tag,
553
+ level: 'component',
554
+ }, null);
555
+ }
556
+ }
557
+
558
+ // Handle Link component for navigation (only for HTML nodes)
559
+ if (tag === 'Link') {
560
+ const { to, ...restProps } = props;
561
+ const href = typeof to === 'string' ? to : '#';
562
+
563
+ // Use effective parent component name for consistency with regular HTML elements
564
+ const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
565
+
566
+ // Navigation click handler
567
+ const navigationOnClick = (e: any) => {
568
+ e.preventDefault();
569
+ if (typeof to === 'string' && to) {
570
+ navigateTo(to);
571
+ }
572
+ };
573
+
574
+ return h('a', {
575
+ ...restProps,
576
+ key,
577
+ href,
578
+ // Add data attributes for selection overlay
579
+ 'data-element-path': pathToString(elementPath),
580
+ // Add parent component context
581
+ ...(effectiveParentComponentName && { 'data-parent-component': effectiveParentComponentName }),
582
+ // Add current component context (when inside component definition)
583
+ ...(componentContext && { 'data-component-context': componentContext }),
584
+ ref: (el: HTMLElement | null) => this.elementRegistry.register(elementPath, el, effectiveParentComponentName, false),
585
+ onClick: navigationOnClick,
586
+ style: {
587
+ textDecoration: 'none',
588
+ color: '#0070f3',
589
+ cursor: 'pointer',
590
+ ...(props.style && typeof props.style === 'object' ? props.style : {}),
591
+ }
592
+ }, this.buildChildren(children, elementPath, effectiveParentComponentName, viewportWidth, componentContext, locale, i18nConfig, cmsContext, cmsLocale));
593
+ }
594
+
595
+ // Prepare element props
596
+ // Remove __componentProps from customProps before spreading (it's only for registry, not DOM)
597
+ const { __componentProps, ...customPropsForDOM } = customProps || {};
598
+ const isComponentRoot = !!(customProps && '__componentProps' in customProps);
599
+ const processedProps: Record<string, unknown> = {
600
+ ...props,
601
+ ...customPropsForDOM,
602
+ key
603
+ // Note: Data attributes moved to ref callback to avoid rerenders on path changes
604
+ };
605
+
606
+ // Register element in registry and set data attributes via DOM (not React props)
607
+ // Setting attributes in ref callback avoids React rerenders when paths change during drag/reorder
608
+ processedProps.ref = (el: HTMLElement | null) => {
609
+ if (el) {
610
+ // Set data attributes via DOM (not React props) to avoid rerenders on path changes
611
+ el.setAttribute('data-element-path', pathToString(elementPath));
612
+ if (isComponentRoot) el.setAttribute('data-component-root', 'true');
613
+ if (parentComponentName) el.setAttribute('data-parent-component', parentComponentName);
614
+ if (componentContext) el.setAttribute('data-component-context', componentContext);
615
+ }
616
+
617
+ // Extract props if this is a component root
618
+ const componentProps = isComponentRoot && customProps.__componentProps
619
+ ? customProps.__componentProps as Record<string, unknown>
620
+ : undefined;
621
+ // Use componentContext as component name for component roots
622
+ const componentNameForRegistry = isComponentRoot ? componentContext : parentComponentName;
623
+
624
+ // Build full metadata for registry
625
+ const metadata = {
626
+ parentComponentName: parentComponentName ?? null,
627
+ componentContext: componentContext ?? null
628
+ };
629
+
630
+ this.elementRegistry.register(elementPath, el, componentNameForRegistry, isComponentRoot, componentProps, metadata);
631
+ };
632
+
633
+ // Add onClick handler - either custom or selection handler
634
+ const originalOnClick = props.onClick ? this.createOnClickHandler(props.onClick) : undefined;
635
+
636
+ // Calculate effective parent component name before using it in onClick handler
637
+ const effectiveParentComponentName = this.getEffectiveParentComponentName(componentContext, parentComponentName);
638
+
639
+ // Add onClick handler from props if exists
640
+ if (originalOnClick) {
641
+ processedProps.onClick = originalOnClick;
642
+ }
643
+
644
+ // For HTML elements, use tag; components and embed nodes should not reach here (they're handled above)
645
+ const finalTag = tag || 'div';
646
+
647
+ // HTML void elements cannot have children
648
+ const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
649
+ if (VOID_ELEMENTS.includes(finalTag.toLowerCase())) {
650
+ // Ensure no children prop is passed to void elements
651
+ const { children: _ignoredChildren, ...voidElementProps } = processedProps;
652
+ return h(finalTag, voidElementProps);
653
+ }
654
+
655
+ // Build children recursively with proper component context
656
+ const childElements = this.buildChildren(children, elementPath, effectiveParentComponentName, viewportWidth, componentContext, locale, i18nConfig, cmsContext, cmsLocale);
657
+ return h(finalTag, processedProps, childElements);
658
+ }
659
+ }
660
+