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,141 @@
1
+ /**
2
+ * Path Conversion Utilities
3
+ * Handles conversion between array paths and string paths
4
+ */
5
+
6
+ import type { Path } from './Path';
7
+ import { ROOT_STRING, ROOT_0_STRING, CHILDREN_SEPARATOR, ROOT_PREFIX, CHILDREN_TOKEN, PATH_SEPARATOR } from './Path';
8
+ import { validatePath, PathConversionError } from './PathValidator';
9
+
10
+ /**
11
+ * Normalize path input to Path array
12
+ * Accepts both Path arrays and string paths, converts to Path array
13
+ * @param input - Path array or string path
14
+ * @returns Normalized Path array
15
+ */
16
+ export function normalizePathInput(input: Path | string): Path {
17
+ if (typeof input === 'string') {
18
+ return stringToPath(input);
19
+ }
20
+ if (!Array.isArray(input)) {
21
+ throw new PathConversionError('Path input must be an array or string', input);
22
+ }
23
+ return input;
24
+ }
25
+
26
+ /**
27
+ * Convert array path to string for Map keys and DOM attributes
28
+ * [0,0,1] -> "0,0,1"
29
+ */
30
+ export function pathToString(path: Path): string {
31
+ return path.join(',');
32
+ }
33
+
34
+ /**
35
+ * Convert string path to array path
36
+ * "0,0,1" -> [0,0,1] (comma-separated format from pathToString)
37
+ * "0" -> [0]
38
+ * "root" -> [0]
39
+ */
40
+ export function stringToPath(str: string): Path {
41
+ if (!str || str === ROOT_STRING || str === ROOT_0_STRING) {
42
+ return [0];
43
+ }
44
+
45
+ // Parse comma-separated format: "0,0,1" -> [0, 0, 1]
46
+ return str.split(',').map(s => {
47
+ const num = parseInt(s, 10);
48
+ if (isNaN(num)) {
49
+ throw new PathConversionError(`Invalid numeric index: ${s}`, str);
50
+ }
51
+ return num;
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Convert array path to legacy string format (for backward compatibility)
57
+ * Converts Path arrays to legacy string format used by TreeNode.path and iframe messages
58
+ *
59
+ * @param path - Path array to convert (e.g., [0,0,1])
60
+ * @returns Legacy string path (e.g., "root_0_children_0_children_1")
61
+ */
62
+ export function pathToLegacyString(path: Path): string {
63
+ if (path.length === 0 || (path.length === 1 && path[0] === 0)) {
64
+ return ROOT_0_STRING;
65
+ }
66
+
67
+ // Build: root_0_children_0_children_1
68
+ let result = ROOT_0_STRING;
69
+ for (let i = 1; i < path.length; i++) {
70
+ result += `${CHILDREN_SEPARATOR}${path[i]}`;
71
+ }
72
+
73
+ return result;
74
+ }
75
+
76
+ /**
77
+ * Convert DOM path string to tree path array
78
+ * DOM paths: "root", "root_children_0"
79
+ * Tree paths: [0], [0,0]
80
+ */
81
+ export function domPathStringToTreePath(domPath: string): Path {
82
+ if (domPath === ROOT_STRING || domPath === '') {
83
+ return [0];
84
+ }
85
+
86
+ // Remove "root_" prefix and parse
87
+ const cleanPath = domPath.startsWith(ROOT_PREFIX)
88
+ ? domPath.substring(ROOT_PREFIX.length)
89
+ : domPath;
90
+
91
+ // Parse "children_0_children_1" -> [0, 1]
92
+ const parts = cleanPath.split(PATH_SEPARATOR);
93
+ const path: Path = [0]; // Tree paths always start with [0]
94
+
95
+ for (let i = 0; i < parts.length; i++) {
96
+ if (parts[i] === CHILDREN_TOKEN && i + 1 < parts.length) {
97
+ const indexStr = parts[i + 1];
98
+ const index = parseInt(indexStr, 10);
99
+ if (!isNaN(index) && indexStr === String(index)) {
100
+ // Valid numeric index
101
+ path.push(index);
102
+ i++;
103
+ }
104
+ // Invalid index segments are silently skipped
105
+ }
106
+ }
107
+
108
+ return path;
109
+ }
110
+
111
+ /**
112
+ * Convert tree path array to DOM path string
113
+ * Converts Path arrays to DOM path strings used for element registration and highlighting
114
+ *
115
+ * @param path - Path array or string (e.g., [0,0,1] or "root_0_children_0_children_1")
116
+ * @returns DOM path string (e.g., "root_children_0_children_1")
117
+ * @throws {InvalidPathError} If path contains invalid values
118
+ */
119
+ export function treePathToDomPathString(path: Path | string): string {
120
+ // Handle string input (backward compatibility)
121
+ if (typeof path === 'string') {
122
+ const pathArray = stringToPath(path);
123
+ return treePathToDomPathString(pathArray);
124
+ }
125
+
126
+ // Validate path before processing
127
+ validatePath(path);
128
+
129
+ if (path.length === 0 || (path.length === 1 && path[0] === 0)) {
130
+ return ROOT_STRING;
131
+ }
132
+
133
+ // Build: root_children_0_children_1
134
+ let result = ROOT_STRING;
135
+ for (let i = 1; i < path.length; i++) {
136
+ result += `${CHILDREN_SEPARATOR}${path[i]}`;
137
+ }
138
+
139
+ return result;
140
+ }
141
+
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Path Utility Functions
3
+ * Common operations on paths
4
+ */
5
+
6
+ import type { Path } from './Path';
7
+ import type { ComponentNode } from '../types';
8
+ import { isRootPath } from './PathValidator';
9
+ import { normalizePathInput } from './PathConverter';
10
+ import { isSlotMarker } from '../nodeUtils';
11
+
12
+ /**
13
+ * Calculate path offset caused by slot expansion
14
+ *
15
+ * When a component structure contains {type: "children"} markers,
16
+ * they are replaced with actual instance children during rendering.
17
+ * This changes indices for all subsequent elements.
18
+ *
19
+ * Example:
20
+ * Component structure: [children_marker, Card, Button]
21
+ * If children_marker expands to 3 children: [child1, child2, child3, Card, Button]
22
+ * Original indices: [0, 1, 2]
23
+ * Actual indices: [0,1,2, 3, 4]
24
+ * Offset for index 1: +2 (expanded from 1 to 3 children)
25
+ * Offset for index 2: +2
26
+ *
27
+ * @param componentPath - Path in component context (e.g., [0, 0, 2])
28
+ * @param componentStructure - Component definition structure
29
+ * @param instanceChildren - Actual children from component instance
30
+ * @returns Offset to add to path indices due to slot expansion
31
+ */
32
+ function calculateChildrenExpansionOffset(
33
+ componentPath: Path,
34
+ componentStructure: ComponentNode | undefined,
35
+ instanceChildren: ComponentNode['children'] | undefined
36
+ ): number {
37
+ if (!componentStructure || !Array.isArray(instanceChildren)) {
38
+ return 0;
39
+ }
40
+
41
+ // Count actual children (accounting for marker expansion)
42
+ const childrenCount = Array.isArray(instanceChildren) ? instanceChildren.length : (instanceChildren ? 1 : 0);
43
+
44
+ let offset = 0;
45
+ let currentNode = componentStructure;
46
+
47
+ // Walk the component structure following the path
48
+ for (let i = 1; i < componentPath.length; i++) {
49
+ const index = componentPath[i];
50
+
51
+ // Get children of current node
52
+ if (!currentNode || !Array.isArray((currentNode as any).children)) {
53
+ break;
54
+ }
55
+
56
+ const children = (currentNode as any).children;
57
+
58
+ // Check if first level has a slot marker that affects the index
59
+ if (i === 1 && Array.isArray(children) && children.length > 0 && isSlotMarker(children[0])) {
60
+ // Children marker is at index 0, so indices 0 to (childrenCount - 1) map to children
61
+ const pageFirstIndex = index;
62
+
63
+ // If page index is within the children range, it's a child (no offset)
64
+ if (pageFirstIndex < childrenCount) {
65
+ // Keep original offset (0)
66
+ } else {
67
+ // Page index is after slot expansion
68
+ // Subtract the offset (childrenCount - 1) to get component index
69
+ offset = childrenCount - 1;
70
+ }
71
+ }
72
+
73
+ // Move to next node in the path
74
+ if (index < children.length) {
75
+ currentNode = (children[index] as any) as ComponentNode;
76
+ } else {
77
+ break;
78
+ }
79
+ }
80
+
81
+ return offset;
82
+ }
83
+
84
+ /**
85
+ * Get parent path
86
+ * [0,0,1] -> [0,0]
87
+ * [0] -> [0] (root has no parent, return itself)
88
+ */
89
+ export function getParentPath(path: Path): Path {
90
+ if (path.length <= 1) {
91
+ return [0]; // Root
92
+ }
93
+ return path.slice(0, -1);
94
+ }
95
+
96
+ /**
97
+ * Get child path by appending index
98
+ * [0,0] + 1 -> [0,0,1]
99
+ */
100
+ export function getChildPath(parent: Path, index: number): Path {
101
+ return [...parent, index];
102
+ }
103
+
104
+ /**
105
+ * Check if path1 is ancestor of path2
106
+ * [0,0] is ancestor of [0,0,1] -> true
107
+ */
108
+ export function isAncestorPath(ancestor: Path, descendant: Path): boolean {
109
+ if (ancestor.length >= descendant.length) {
110
+ return false;
111
+ }
112
+ return ancestor.every((val, idx) => val === descendant[idx]);
113
+ }
114
+
115
+ /**
116
+ * Get depth of path
117
+ * [0] -> 0, [0,1] -> 1, [0,1,2] -> 2
118
+ */
119
+ export function getPathDepth(path: Path): number {
120
+ return Math.max(0, path.length - 1);
121
+ }
122
+
123
+ /**
124
+ * Build all parent paths for tree expansion
125
+ * [0,0,1] -> [[0], [0,0]]
126
+ */
127
+ export function buildParentPaths(path: Path): Path[] {
128
+ if (isRootPath(path)) {
129
+ return [];
130
+ }
131
+
132
+ const parents: Path[] = [];
133
+ for (let i = 1; i < path.length; i++) {
134
+ parents.push(path.slice(0, i));
135
+ }
136
+
137
+ return parents;
138
+ }
139
+
140
+ /**
141
+ * Compare two paths for equality
142
+ * Accepts both Path arrays and strings, normalizes before comparison
143
+ *
144
+ * @param path1 - First path (Path array or string)
145
+ * @param path2 - Second path (Path array or string)
146
+ * @returns True if paths are equal
147
+ */
148
+ export function pathsEqual(path1: Path | string, path2: Path | string): boolean {
149
+ const normalizedPath1 = normalizePathInput(path1);
150
+ const normalizedPath2 = normalizePathInput(path2);
151
+
152
+ if (normalizedPath1.length !== normalizedPath2.length) {
153
+ return false;
154
+ }
155
+ return normalizedPath1.every((val, idx) => val === normalizedPath2[idx]);
156
+ }
157
+
158
+ /**
159
+ * Convert page-context path to component-context path
160
+ *
161
+ * When editing a component, paths from the canvas are in page context
162
+ * (full path including component instance location). This function converts
163
+ * them to component-context paths (relative to component definition root).
164
+ *
165
+ * Accounts for {type: "children"} expansion when provided with component
166
+ * structure and instance children.
167
+ *
168
+ * @param pagePath - Path in page context (e.g., [0, 0, 1, 0, 0])
169
+ * @param componentInstancePath - Path to component instance in page (e.g., [0, 0, 1])
170
+ * @param componentStructure - Component definition structure (for children offset calculation)
171
+ * @param instanceChildren - Actual children from component instance (for children offset calculation)
172
+ * @returns Path in component context (e.g., [0, 0, 0]) or null if not within component
173
+ *
174
+ * @example
175
+ * convertPagePathToComponentPath([0, 0, 1, 0, 0], [0, 0, 1])
176
+ * // Returns: [0, 0, 0]
177
+ *
178
+ * convertPagePathToComponentPath([0, 0, 2], [0, 0, 1])
179
+ * // Returns: null (not within component instance)
180
+ *
181
+ * convertPagePathToComponentPath([0, 0, 1, 4], [0, 0, 1], structure, [child1, child2, child3])
182
+ * // If structure has slot marker at index 0:
183
+ * // Returns: [0, 1] (not [0, 4] - offset back by -3)
184
+ */
185
+ export function convertPagePathToComponentPath(
186
+ pagePath: Path,
187
+ componentInstancePath: Path,
188
+ componentStructure?: ComponentNode,
189
+ instanceChildren?: ComponentNode['children']
190
+ ): Path | null {
191
+ // Check if pagePath starts with componentInstancePath
192
+ // Path must be within or equal to component instance
193
+ const isWithinComponent = isAncestorPath(componentInstancePath, pagePath) ||
194
+ pathsEqual(pagePath, componentInstancePath);
195
+
196
+ if (!isWithinComponent) {
197
+ return null; // Path is not within the component instance
198
+ }
199
+
200
+ // Extract suffix (indices after component instance path)
201
+ const suffix = pagePath.slice(componentInstancePath.length);
202
+
203
+ // If we need to account for slot expansion, build the component path to calculate offset
204
+ let adjustedSuffix = suffix;
205
+ if (suffix.length > 0 && componentStructure && Array.isArray(instanceChildren)) {
206
+ const childrenCount = instanceChildren.length;
207
+
208
+ // Check if first level has a slot marker that affects the index
209
+ if (
210
+ Array.isArray(componentStructure.children) &&
211
+ componentStructure.children.length > 0 &&
212
+ isSlotMarker(componentStructure.children[0])
213
+ ) {
214
+ // Children marker is at index 0, so indices 0 to (childrenCount - 1) map to children
215
+ const pageFirstIndex = suffix[0];
216
+
217
+ // If page index is within the children range, it's a child (no component path)
218
+ if (pageFirstIndex < childrenCount) {
219
+ // This element is from instance children, not part of component structure
220
+ // We can still return the path but know it's an instance child
221
+ // Keep the original suffix
222
+ adjustedSuffix = suffix;
223
+ } else {
224
+ // Page index is after slot expansion
225
+ // Subtract the offset (childrenCount - 1) to get component index
226
+ const offset = childrenCount - 1;
227
+ adjustedSuffix = [suffix[0] - offset, ...suffix.slice(1)];
228
+ }
229
+ }
230
+ }
231
+
232
+ // Component paths always start with [0] (component root)
233
+ return [0, ...adjustedSuffix];
234
+ }
235
+
236
+ /**
237
+ * Convert component-context path to page-context path
238
+ *
239
+ * When editing a component, tree selections use component-context paths.
240
+ * To highlight in the canvas, we need to convert them to page-context paths
241
+ * (full path including component instance location).
242
+ *
243
+ * Accounts for {type: "children"} expansion when provided with component
244
+ * structure and instance children.
245
+ *
246
+ * @param componentPath - Path in component context (e.g., [0, 0, 0])
247
+ * @param componentInstancePath - Path to component instance in page (e.g., [0, 0, 1])
248
+ * @param componentStructure - Component definition structure (for children offset calculation)
249
+ * @param instanceChildren - Actual children from component instance (for children offset calculation)
250
+ * @returns Path in page context (e.g., [0, 0, 1, 0, 0])
251
+ *
252
+ * @example
253
+ * convertComponentPathToPagePath([0, 0, 0], [0, 0, 1])
254
+ * // Returns: [0, 0, 1, 0, 0]
255
+ *
256
+ * convertComponentPathToPagePath([0], [0, 0, 1])
257
+ * // Returns: [0, 0, 1] (component root maps to instance)
258
+ *
259
+ * convertComponentPathToPagePath([0, 1], [0, 0, 1], structure, [child1, child2, child3])
260
+ * // If structure has slot marker at index 0:
261
+ * // Returns: [0, 0, 1, 4] (not [0, 0, 1, 1] - offset by +3)
262
+ */
263
+ export function convertComponentPathToPagePath(
264
+ componentPath: Path,
265
+ componentInstancePath: Path,
266
+ componentStructure?: ComponentNode,
267
+ instanceChildren?: ComponentNode['children']
268
+ ): Path {
269
+ // Component paths start with [0] (component root)
270
+ if (componentPath.length === 1 && componentPath[0] === 0) {
271
+ // Component root maps to the component instance path
272
+ return componentInstancePath;
273
+ }
274
+
275
+ // Extract suffix (everything after component root [0])
276
+ const suffix = componentPath.slice(1);
277
+
278
+ // Calculate offset caused by slot expansion
279
+ const offset = calculateChildrenExpansionOffset(componentPath, componentStructure, instanceChildren);
280
+
281
+ // Apply offset to first level index if slot expansion occurred
282
+ let adjustedSuffix = suffix;
283
+ if (offset > 0 && suffix.length > 0) {
284
+ adjustedSuffix = [suffix[0] + offset, ...suffix.slice(1)];
285
+ }
286
+
287
+ // Append adjusted suffix to component instance path
288
+ return [...componentInstancePath, ...adjustedSuffix];
289
+ }
290
+
@@ -0,0 +1,193 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import {
3
+ validatePath,
4
+ isRootPath,
5
+ InvalidPathError,
6
+ PathConversionError
7
+ } from './PathValidator';
8
+
9
+ describe('PathValidator', () => {
10
+ describe('validatePath', () => {
11
+ test('validates valid path', () => {
12
+ expect(() => validatePath([0, 1, 2])).not.toThrow();
13
+ });
14
+
15
+ test('validates empty path', () => {
16
+ expect(() => validatePath([])).not.toThrow();
17
+ });
18
+
19
+ test('validates single element path', () => {
20
+ expect(() => validatePath([0])).not.toThrow();
21
+ });
22
+
23
+ test('validates path with zero', () => {
24
+ expect(() => validatePath([0, 0, 0])).not.toThrow();
25
+ });
26
+
27
+ test('validates path with large numbers', () => {
28
+ expect(() => validatePath([100, 200, 300])).not.toThrow();
29
+ });
30
+
31
+ test('throws on non-array path', () => {
32
+ expect(() => validatePath('not an array' as any)).toThrow(InvalidPathError);
33
+ });
34
+
35
+ test('throws on path with string value', () => {
36
+ expect(() => validatePath([0, '1' as any, 2])).toThrow(InvalidPathError);
37
+ });
38
+
39
+ test('throws on path with NaN', () => {
40
+ expect(() => validatePath([0, NaN, 2])).toThrow(InvalidPathError);
41
+ });
42
+
43
+ test('throws on path with Infinity', () => {
44
+ expect(() => validatePath([0, Infinity, 2])).toThrow(InvalidPathError);
45
+ });
46
+
47
+ test('throws on path with negative Infinity', () => {
48
+ expect(() => validatePath([0, -Infinity, 2])).toThrow(InvalidPathError);
49
+ });
50
+
51
+ test('throws on path with null', () => {
52
+ expect(() => validatePath([0, null as any, 2])).toThrow(InvalidPathError);
53
+ });
54
+
55
+ test('throws on path with undefined', () => {
56
+ expect(() => validatePath([0, undefined as any, 2])).toThrow(InvalidPathError);
57
+ });
58
+
59
+ test('throws on path with object', () => {
60
+ expect(() => validatePath([0, {} as any, 2])).toThrow(InvalidPathError);
61
+ });
62
+
63
+ test('InvalidPathError includes path in error', () => {
64
+ const invalidPath = [0, 'invalid' as any, 2];
65
+ try {
66
+ validatePath(invalidPath);
67
+ expect(true).toBe(false); // Should not reach here
68
+ } catch (error) {
69
+ expect(error).toBeInstanceOf(InvalidPathError);
70
+ if (error instanceof InvalidPathError) {
71
+ expect(error.path).toBe(invalidPath);
72
+ expect(error.index).toBe(1);
73
+ }
74
+ }
75
+ });
76
+
77
+ test('InvalidPathError includes index', () => {
78
+ try {
79
+ validatePath([0, 1, 'bad' as any, 3]);
80
+ } catch (error) {
81
+ if (error instanceof InvalidPathError) {
82
+ expect(error.index).toBe(2);
83
+ }
84
+ }
85
+ });
86
+
87
+ test('allows negative numbers', () => {
88
+ expect(() => validatePath([0, -1, -5])).not.toThrow();
89
+ });
90
+
91
+ test('allows decimal numbers', () => {
92
+ expect(() => validatePath([0, 1.5, 2.7])).not.toThrow();
93
+ });
94
+ });
95
+
96
+ describe('isRootPath', () => {
97
+ test('returns true for empty path', () => {
98
+ expect(isRootPath([])).toBe(true);
99
+ });
100
+
101
+ test('returns true for [0] path', () => {
102
+ expect(isRootPath([0])).toBe(true);
103
+ });
104
+
105
+ test('returns false for [0, 1] path', () => {
106
+ expect(isRootPath([0, 1])).toBe(false);
107
+ });
108
+
109
+ test('returns false for [1] path', () => {
110
+ expect(isRootPath([1])).toBe(false);
111
+ });
112
+
113
+ test('returns false for multi-element path', () => {
114
+ expect(isRootPath([0, 1, 2])).toBe(false);
115
+ });
116
+
117
+ test('returns false for non-zero single element', () => {
118
+ expect(isRootPath([5])).toBe(false);
119
+ });
120
+
121
+ test('returns false for path starting with non-zero', () => {
122
+ expect(isRootPath([1, 0])).toBe(false);
123
+ });
124
+ });
125
+
126
+ describe('InvalidPathError', () => {
127
+ test('creates error with message and path', () => {
128
+ const path = [0, 'bad' as any];
129
+ const error = new InvalidPathError('Test message', path);
130
+ expect(error.message).toBe('Test message');
131
+ expect(error.path).toBe(path);
132
+ expect(error.name).toBe('InvalidPathError');
133
+ });
134
+
135
+ test('creates error with index', () => {
136
+ const error = new InvalidPathError('Test', [0, 'bad' as any], 1);
137
+ expect(error.index).toBe(1);
138
+ });
139
+
140
+ test('is instance of Error', () => {
141
+ const error = new InvalidPathError('Test', [0]);
142
+ expect(error instanceof Error).toBe(true);
143
+ });
144
+
145
+ test('has proper prototype chain', () => {
146
+ const error = new InvalidPathError('Test', [0]);
147
+ expect(error instanceof InvalidPathError).toBe(true);
148
+ expect(error instanceof Error).toBe(true);
149
+ });
150
+
151
+ test('allows undefined index', () => {
152
+ const error = new InvalidPathError('Test', [0]);
153
+ expect(error.index).toBeUndefined();
154
+ });
155
+ });
156
+
157
+ describe('PathConversionError', () => {
158
+ test('creates error with message and input', () => {
159
+ const input = 'invalid input';
160
+ const error = new PathConversionError('Test message', input);
161
+ expect(error.message).toBe('Test message');
162
+ expect(error.input).toBe(input);
163
+ expect(error.name).toBe('PathConversionError');
164
+ });
165
+
166
+ test('is instance of Error', () => {
167
+ const error = new PathConversionError('Test', 'input');
168
+ expect(error instanceof Error).toBe(true);
169
+ });
170
+
171
+ test('has proper prototype chain', () => {
172
+ const error = new PathConversionError('Test', 'input');
173
+ expect(error instanceof PathConversionError).toBe(true);
174
+ expect(error instanceof Error).toBe(true);
175
+ });
176
+
177
+ test('stores any input type', () => {
178
+ const inputs = [
179
+ 'string',
180
+ 123,
181
+ null,
182
+ undefined,
183
+ {},
184
+ [],
185
+ true
186
+ ];
187
+ for (const input of inputs) {
188
+ const error = new PathConversionError('Test', input);
189
+ expect(error.input).toBe(input);
190
+ }
191
+ });
192
+ });
193
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Path Validation
3
+ * Validates path arrays and provides error handling
4
+ */
5
+
6
+ import type { Path } from './Path';
7
+
8
+ /**
9
+ * Custom error classes for path operations
10
+ */
11
+ export class InvalidPathError extends Error {
12
+ constructor(message: string, public readonly path: unknown, public readonly index?: number) {
13
+ super(message);
14
+ this.name = 'InvalidPathError';
15
+ }
16
+ }
17
+
18
+ export class PathConversionError extends Error {
19
+ constructor(message: string, public readonly input: unknown) {
20
+ super(message);
21
+ this.name = 'PathConversionError';
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Validate that a path contains only valid numeric values
27
+ * @throws {InvalidPathError} If path contains invalid values
28
+ */
29
+ export function validatePath(path: Path): void {
30
+ if (!Array.isArray(path)) {
31
+ throw new InvalidPathError('Path must be an array', path);
32
+ }
33
+
34
+ for (let i = 0; i < path.length; i++) {
35
+ const value = path[i];
36
+ if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) {
37
+ throw new InvalidPathError(
38
+ `Invalid path value at index ${i}: expected number, got ${typeof value}`,
39
+ path,
40
+ i
41
+ );
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Check if path is root
48
+ * [0] -> true, [] -> true, [0,1] -> false
49
+ */
50
+ export function isRootPath(path: Path): boolean {
51
+ return path.length === 0 || (path.length === 1 && path[0] === 0);
52
+ }
53
+