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,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
|
+
|