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,325 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
buildSlugIndex,
|
|
4
|
+
findPageBySlug,
|
|
5
|
+
translatePath,
|
|
6
|
+
getLocaleLinks,
|
|
7
|
+
resolveSlugToPageId,
|
|
8
|
+
type SlugMap,
|
|
9
|
+
} from './slugTranslator';
|
|
10
|
+
import type { I18nConfig } from './types';
|
|
11
|
+
|
|
12
|
+
describe('slugTranslator', () => {
|
|
13
|
+
let slugMappings: SlugMap[];
|
|
14
|
+
let slugIndex: Map<string, any>;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
slugMappings = [
|
|
18
|
+
{
|
|
19
|
+
pageId: 'about',
|
|
20
|
+
slugs: {
|
|
21
|
+
en: 'about',
|
|
22
|
+
pl: 'o-nas',
|
|
23
|
+
de: 'uber',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pageId: 'contact',
|
|
28
|
+
slugs: {
|
|
29
|
+
en: 'contact',
|
|
30
|
+
pl: 'kontakt',
|
|
31
|
+
de: 'kontakt',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pageId: 'index',
|
|
36
|
+
slugs: {
|
|
37
|
+
en: '',
|
|
38
|
+
pl: '',
|
|
39
|
+
de: '',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
slugIndex = buildSlugIndex(slugMappings);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('buildSlugIndex', () => {
|
|
48
|
+
test('should build index with locale:slug keys', () => {
|
|
49
|
+
expect(slugIndex.has('en:about')).toBe(true);
|
|
50
|
+
expect(slugIndex.has('pl:o-nas')).toBe(true);
|
|
51
|
+
expect(slugIndex.has('de:uber')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should store correct pageId for each slug', () => {
|
|
55
|
+
const aboutEn = slugIndex.get('en:about');
|
|
56
|
+
expect(aboutEn?.pageId).toBe('about');
|
|
57
|
+
|
|
58
|
+
const aboutPl = slugIndex.get('pl:o-nas');
|
|
59
|
+
expect(aboutPl?.pageId).toBe('about');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should store all slugs for each entry', () => {
|
|
63
|
+
const aboutEn = slugIndex.get('en:about');
|
|
64
|
+
expect(aboutEn?.slugs).toEqual({
|
|
65
|
+
en: 'about',
|
|
66
|
+
pl: 'o-nas',
|
|
67
|
+
de: 'uber',
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should handle empty slugs', () => {
|
|
72
|
+
expect(slugIndex.has('en:')).toBe(true);
|
|
73
|
+
expect(slugIndex.has('pl:')).toBe(true);
|
|
74
|
+
expect(slugIndex.has('de:')).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should handle empty mappings array', () => {
|
|
78
|
+
const emptyIndex = buildSlugIndex([]);
|
|
79
|
+
expect(emptyIndex.size).toBe(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('should handle multiple locales', () => {
|
|
83
|
+
const mappings: SlugMap[] = [
|
|
84
|
+
{
|
|
85
|
+
pageId: 'test',
|
|
86
|
+
slugs: {
|
|
87
|
+
en: 'test',
|
|
88
|
+
fr: 'tester',
|
|
89
|
+
es: 'prueba',
|
|
90
|
+
it: 'prova',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const index = buildSlugIndex(mappings);
|
|
96
|
+
expect(index.has('en:test')).toBe(true);
|
|
97
|
+
expect(index.has('fr:tester')).toBe(true);
|
|
98
|
+
expect(index.has('es:prueba')).toBe(true);
|
|
99
|
+
expect(index.has('it:prova')).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('findPageBySlug', () => {
|
|
104
|
+
test('should find page by English slug', () => {
|
|
105
|
+
const result = findPageBySlug('about', 'en', slugIndex);
|
|
106
|
+
expect(result?.pageId).toBe('about');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('should find page by Polish slug', () => {
|
|
110
|
+
const result = findPageBySlug('o-nas', 'pl', slugIndex);
|
|
111
|
+
expect(result?.pageId).toBe('about');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should find page by German slug', () => {
|
|
115
|
+
const result = findPageBySlug('uber', 'de', slugIndex);
|
|
116
|
+
expect(result?.pageId).toBe('about');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should return undefined for non-existent slug', () => {
|
|
120
|
+
const result = findPageBySlug('nonexistent', 'en', slugIndex);
|
|
121
|
+
expect(result).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('should return undefined for wrong locale', () => {
|
|
125
|
+
const result = findPageBySlug('about', 'fr', slugIndex);
|
|
126
|
+
expect(result).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should find page with empty slug', () => {
|
|
130
|
+
const result = findPageBySlug('', 'en', slugIndex);
|
|
131
|
+
expect(result?.pageId).toBe('index');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('translatePath', () => {
|
|
136
|
+
test('should translate Polish path to English', () => {
|
|
137
|
+
const result = translatePath('/pl/o-nas', 'en', 'pl', 'en', slugIndex);
|
|
138
|
+
expect(result).toBe('/about');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('should translate English path to Polish', () => {
|
|
142
|
+
const result = translatePath('/about', 'pl', 'en', 'en', slugIndex);
|
|
143
|
+
expect(result).toBe('/pl/o-nas');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should translate English path to German', () => {
|
|
147
|
+
const result = translatePath('/about', 'de', 'en', 'en', slugIndex);
|
|
148
|
+
expect(result).toBe('/de/uber');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should handle root path translation', () => {
|
|
152
|
+
const result = translatePath('/', 'pl', 'en', 'en', slugIndex);
|
|
153
|
+
expect(result).toBe('/pl');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('should handle root path from non-default locale', () => {
|
|
157
|
+
const result = translatePath('/pl', 'en', 'pl', 'en', slugIndex);
|
|
158
|
+
expect(result).toBe('/');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('should handle locale-only path', () => {
|
|
162
|
+
const result = translatePath('/pl', 'de', 'pl', 'en', slugIndex);
|
|
163
|
+
expect(result).toBe('/de');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('should preserve slug if translation not found', () => {
|
|
167
|
+
const result = translatePath('/unknown', 'pl', 'en', 'en', slugIndex);
|
|
168
|
+
expect(result).toBe('/pl/unknown');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should handle path with leading slash', () => {
|
|
172
|
+
const result = translatePath('/about', 'pl', 'en', 'en', slugIndex);
|
|
173
|
+
expect(result).toBe('/pl/o-nas');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('should handle path without leading slash', () => {
|
|
177
|
+
const result = translatePath('about', 'pl', 'en', 'en', slugIndex);
|
|
178
|
+
expect(result).toBe('/pl/o-nas');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('should translate to default locale without prefix', () => {
|
|
182
|
+
const result = translatePath('/pl/kontakt', 'en', 'pl', 'en', slugIndex);
|
|
183
|
+
expect(result).toBe('/contact');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('should translate from default locale to non-default', () => {
|
|
187
|
+
const result = translatePath('/contact', 'pl', 'en', 'en', slugIndex);
|
|
188
|
+
expect(result).toBe('/pl/kontakt');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('should handle same source and target locale', () => {
|
|
192
|
+
const result = translatePath('/about', 'en', 'en', 'en', slugIndex);
|
|
193
|
+
expect(result).toBe('/about');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('should fallback to default locale slug if target not found', () => {
|
|
197
|
+
const mappings: SlugMap[] = [
|
|
198
|
+
{
|
|
199
|
+
pageId: 'test',
|
|
200
|
+
slugs: {
|
|
201
|
+
en: 'test',
|
|
202
|
+
pl: 'test-pl',
|
|
203
|
+
// de missing
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
const index = buildSlugIndex(mappings);
|
|
208
|
+
const result = translatePath('/test', 'de', 'en', 'en', index);
|
|
209
|
+
expect(result).toBe('/de/test'); // Falls back to English slug
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('getLocaleLinks', () => {
|
|
214
|
+
const i18nConfig: I18nConfig = {
|
|
215
|
+
defaultLocale: 'en',
|
|
216
|
+
locales: [
|
|
217
|
+
{ code: 'en', langTag: 'en-US', nativeName: 'English' },
|
|
218
|
+
{ code: 'pl', langTag: 'pl-PL', nativeName: 'Polski' },
|
|
219
|
+
{ code: 'de', langTag: 'de-DE', nativeName: 'Deutsch' },
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
test('should generate locale links for all locales', () => {
|
|
224
|
+
const links = getLocaleLinks('/about', 'en', i18nConfig, slugIndex);
|
|
225
|
+
expect(links.length).toBe(3);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('should include correct locale codes', () => {
|
|
229
|
+
const links = getLocaleLinks('/about', 'en', i18nConfig, slugIndex);
|
|
230
|
+
const codes = links.map(link => link.locale);
|
|
231
|
+
expect(codes).toEqual(['en', 'pl', 'de']);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('should include correct native names', () => {
|
|
235
|
+
const links = getLocaleLinks('/about', 'en', i18nConfig, slugIndex);
|
|
236
|
+
const names = links.map(link => link.nativeName);
|
|
237
|
+
expect(names).toEqual(['English', 'Polski', 'Deutsch']);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('should mark current locale correctly', () => {
|
|
241
|
+
const links = getLocaleLinks('/about', 'pl', i18nConfig, slugIndex);
|
|
242
|
+
const currentLink = links.find(link => link.locale === 'pl');
|
|
243
|
+
expect(currentLink?.isCurrent).toBe(true);
|
|
244
|
+
|
|
245
|
+
const otherLinks = links.filter(link => link.locale !== 'pl');
|
|
246
|
+
otherLinks.forEach(link => {
|
|
247
|
+
expect(link.isCurrent).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('should translate paths correctly for each locale', () => {
|
|
252
|
+
const links = getLocaleLinks('/about', 'en', i18nConfig, slugIndex);
|
|
253
|
+
|
|
254
|
+
const enLink = links.find(link => link.locale === 'en');
|
|
255
|
+
expect(enLink?.path).toBe('/about');
|
|
256
|
+
|
|
257
|
+
const plLink = links.find(link => link.locale === 'pl');
|
|
258
|
+
expect(plLink?.path).toBe('/pl/o-nas');
|
|
259
|
+
|
|
260
|
+
const deLink = links.find(link => link.locale === 'de');
|
|
261
|
+
expect(deLink?.path).toBe('/de/uber');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('should handle root path', () => {
|
|
265
|
+
const links = getLocaleLinks('/', 'en', i18nConfig, slugIndex);
|
|
266
|
+
|
|
267
|
+
const enLink = links.find(link => link.locale === 'en');
|
|
268
|
+
expect(enLink?.path).toBe('/');
|
|
269
|
+
|
|
270
|
+
const plLink = links.find(link => link.locale === 'pl');
|
|
271
|
+
expect(plLink?.path).toBe('/pl');
|
|
272
|
+
|
|
273
|
+
const deLink = links.find(link => link.locale === 'de');
|
|
274
|
+
expect(deLink?.path).toBe('/de');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('should handle Polish path', () => {
|
|
278
|
+
const links = getLocaleLinks('/pl/o-nas', 'pl', i18nConfig, slugIndex);
|
|
279
|
+
|
|
280
|
+
const enLink = links.find(link => link.locale === 'en');
|
|
281
|
+
expect(enLink?.path).toBe('/about');
|
|
282
|
+
|
|
283
|
+
const plLink = links.find(link => link.locale === 'pl');
|
|
284
|
+
expect(plLink?.path).toBe('/pl/o-nas');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('resolveSlugToPageId', () => {
|
|
289
|
+
test('should resolve English slug to pageId', () => {
|
|
290
|
+
const result = resolveSlugToPageId('about', 'en', slugIndex);
|
|
291
|
+
expect(result).toBe('about');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('should resolve Polish slug to pageId', () => {
|
|
295
|
+
const result = resolveSlugToPageId('o-nas', 'pl', slugIndex);
|
|
296
|
+
expect(result).toBe('about');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('should resolve German slug to pageId', () => {
|
|
300
|
+
const result = resolveSlugToPageId('uber', 'de', slugIndex);
|
|
301
|
+
expect(result).toBe('about');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('should return undefined for non-existent slug', () => {
|
|
305
|
+
const result = resolveSlugToPageId('nonexistent', 'en', slugIndex);
|
|
306
|
+
expect(result).toBeUndefined();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('should return undefined for wrong locale', () => {
|
|
310
|
+
const result = resolveSlugToPageId('about', 'fr', slugIndex);
|
|
311
|
+
expect(result).toBeUndefined();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('should resolve empty slug to index page', () => {
|
|
315
|
+
const result = resolveSlugToPageId('', 'en', slugIndex);
|
|
316
|
+
expect(result).toBe('index');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('should resolve contact page slugs', () => {
|
|
320
|
+
expect(resolveSlugToPageId('contact', 'en', slugIndex)).toBe('contact');
|
|
321
|
+
expect(resolveSlugToPageId('kontakt', 'pl', slugIndex)).toBe('contact');
|
|
322
|
+
expect(resolveSlugToPageId('kontakt', 'de', slugIndex)).toBe('contact');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slug Translation Service
|
|
3
|
+
* Handles translation of URL slugs between locales
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { I18nConfig } from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Slug mapping for a single page
|
|
10
|
+
*/
|
|
11
|
+
export interface SlugMap {
|
|
12
|
+
pageId: string;
|
|
13
|
+
slugs: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Index entry for reverse lookup (slug+locale → pageId)
|
|
18
|
+
*/
|
|
19
|
+
interface SlugIndexEntry {
|
|
20
|
+
pageId: string;
|
|
21
|
+
slugs: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build reverse lookup index: "locale:slug" → SlugIndexEntry
|
|
26
|
+
* This allows quick lookup of pageId from any locale's slug
|
|
27
|
+
*/
|
|
28
|
+
export function buildSlugIndex(mappings: SlugMap[]): Map<string, SlugIndexEntry> {
|
|
29
|
+
const index = new Map<string, SlugIndexEntry>();
|
|
30
|
+
|
|
31
|
+
for (const mapping of mappings) {
|
|
32
|
+
for (const [locale, slug] of Object.entries(mapping.slugs)) {
|
|
33
|
+
// Key format: "locale:slug" (e.g., "pl:o-nas" or "en:about")
|
|
34
|
+
const key = `${locale}:${slug}`;
|
|
35
|
+
index.set(key, {
|
|
36
|
+
pageId: mapping.pageId,
|
|
37
|
+
slugs: mapping.slugs,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return index;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find page by slug and locale
|
|
47
|
+
* @returns The SlugIndexEntry if found, undefined otherwise
|
|
48
|
+
*/
|
|
49
|
+
export function findPageBySlug(
|
|
50
|
+
slug: string,
|
|
51
|
+
locale: string,
|
|
52
|
+
index: Map<string, SlugIndexEntry>
|
|
53
|
+
): SlugIndexEntry | undefined {
|
|
54
|
+
const key = `${locale}:${slug}`;
|
|
55
|
+
return index.get(key);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Translate a path to another locale
|
|
60
|
+
*
|
|
61
|
+
* @param currentPath - Current URL path (e.g., "/pl/o-nas" or "/about")
|
|
62
|
+
* @param targetLocale - Target locale (e.g., "en")
|
|
63
|
+
* @param currentLocale - Current locale (e.g., "pl")
|
|
64
|
+
* @param defaultLocale - Default locale that doesn't use prefix (e.g., "en")
|
|
65
|
+
* @param index - Slug index from buildSlugIndex()
|
|
66
|
+
* @returns Translated path (e.g., "/about" for en default, "/de/uber" for de)
|
|
67
|
+
*/
|
|
68
|
+
export function translatePath(
|
|
69
|
+
currentPath: string,
|
|
70
|
+
targetLocale: string,
|
|
71
|
+
currentLocale: string,
|
|
72
|
+
defaultLocale: string,
|
|
73
|
+
index: Map<string, SlugIndexEntry>
|
|
74
|
+
): string {
|
|
75
|
+
// Extract slug from current path (remove locale prefix if present)
|
|
76
|
+
let slug = currentPath;
|
|
77
|
+
|
|
78
|
+
// Remove leading slash
|
|
79
|
+
if (slug.startsWith('/')) {
|
|
80
|
+
slug = slug.substring(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Remove locale prefix if present (e.g., "pl/o-nas" → "o-nas")
|
|
84
|
+
// Also handle case where slug IS the locale (e.g., "pl" for Polish homepage)
|
|
85
|
+
if (currentLocale !== defaultLocale) {
|
|
86
|
+
if (slug.startsWith(`${currentLocale}/`)) {
|
|
87
|
+
slug = slug.substring(currentLocale.length + 1);
|
|
88
|
+
} else if (slug === currentLocale) {
|
|
89
|
+
// Path is just the locale prefix (e.g., "/pl" → index page)
|
|
90
|
+
slug = '';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle root path
|
|
95
|
+
if (slug === '' || slug === '/') {
|
|
96
|
+
slug = '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Look up page by current slug and locale
|
|
100
|
+
let entry = findPageBySlug(slug, currentLocale, index);
|
|
101
|
+
|
|
102
|
+
// If not found for current locale, try default locale
|
|
103
|
+
// This handles cases like /de/about where German uses the English slug
|
|
104
|
+
if (!entry && currentLocale !== defaultLocale) {
|
|
105
|
+
entry = findPageBySlug(slug, defaultLocale, index);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!entry) {
|
|
109
|
+
// No translation found - return path with just locale prefix change
|
|
110
|
+
if (targetLocale === defaultLocale) {
|
|
111
|
+
return slug === '' ? '/' : `/${slug}`;
|
|
112
|
+
}
|
|
113
|
+
return slug === '' ? `/${targetLocale}` : `/${targetLocale}/${slug}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get translated slug for target locale
|
|
117
|
+
const targetSlug = entry.slugs[targetLocale] ?? entry.slugs[defaultLocale] ?? slug;
|
|
118
|
+
|
|
119
|
+
// Build target path
|
|
120
|
+
if (targetLocale === defaultLocale) {
|
|
121
|
+
return targetSlug === '' ? '/' : `/${targetSlug}`;
|
|
122
|
+
}
|
|
123
|
+
return targetSlug === '' ? `/${targetLocale}` : `/${targetLocale}/${targetSlug}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Locale link information for locale switcher
|
|
128
|
+
*/
|
|
129
|
+
export interface LocaleLink {
|
|
130
|
+
locale: string; // Locale code (e.g., "pl")
|
|
131
|
+
langTag: string; // BCP 47 language tag (e.g., "pl-PL")
|
|
132
|
+
nativeName: string; // Display name in native language (e.g., "Polski")
|
|
133
|
+
path: string; // Translated path for this locale
|
|
134
|
+
isCurrent: boolean; // Whether this is the current locale
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get all available locales with their translated paths for the current page
|
|
139
|
+
* Useful for rendering locale switcher
|
|
140
|
+
*
|
|
141
|
+
* @param currentPath - Current URL path
|
|
142
|
+
* @param currentLocale - Current locale
|
|
143
|
+
* @param i18nConfig - i18n configuration with locales
|
|
144
|
+
* @param index - Slug index
|
|
145
|
+
* @returns Array of LocaleLink objects
|
|
146
|
+
*/
|
|
147
|
+
export function getLocaleLinks(
|
|
148
|
+
currentPath: string,
|
|
149
|
+
currentLocale: string,
|
|
150
|
+
i18nConfig: I18nConfig,
|
|
151
|
+
index: Map<string, SlugIndexEntry>
|
|
152
|
+
): LocaleLink[] {
|
|
153
|
+
return i18nConfig.locales.map(localeConfig => ({
|
|
154
|
+
locale: localeConfig.code,
|
|
155
|
+
langTag: localeConfig.langTag,
|
|
156
|
+
nativeName: localeConfig.nativeName,
|
|
157
|
+
path: translatePath(currentPath, localeConfig.code, currentLocale, i18nConfig.defaultLocale, index),
|
|
158
|
+
isCurrent: localeConfig.code === currentLocale,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolve a slug to its pageId (for server-side page loading)
|
|
164
|
+
*
|
|
165
|
+
* @param slug - URL slug (e.g., "o-nas")
|
|
166
|
+
* @param locale - Current locale (e.g., "pl")
|
|
167
|
+
* @param index - Slug index
|
|
168
|
+
* @returns pageId if found (e.g., "about"), undefined otherwise
|
|
169
|
+
*/
|
|
170
|
+
export function resolveSlugToPageId(
|
|
171
|
+
slug: string,
|
|
172
|
+
locale: string,
|
|
173
|
+
index: Map<string, SlugIndexEntry>
|
|
174
|
+
): string | undefined {
|
|
175
|
+
const entry = findPageBySlug(slug, locale, index);
|
|
176
|
+
return entry?.pageId;
|
|
177
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { applyStylesToNode, mergeNodeStyles, extractStylesFromNode } from './styleNodeUtils';
|
|
3
|
+
import type { HtmlNode, ComponentInstanceNode } from './types';
|
|
4
|
+
import { NODE_TYPE } from './constants';
|
|
5
|
+
|
|
6
|
+
describe('styleNodeUtils', () => {
|
|
7
|
+
describe('applyStylesToNode', () => {
|
|
8
|
+
test('should apply styles to HTML node', () => {
|
|
9
|
+
const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div', children: [] };
|
|
10
|
+
const styles = { color: 'red', fontSize: '16px' };
|
|
11
|
+
const result = applyStylesToNode(node, styles);
|
|
12
|
+
expect(result.style).toEqual(styles);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('should apply styles to component instance in props.style', () => {
|
|
16
|
+
const node: ComponentInstanceNode = { type: NODE_TYPE.COMPONENT, component: 'Button', props: {} };
|
|
17
|
+
const styles = { padding: '10px' };
|
|
18
|
+
const result = applyStylesToNode(node, styles);
|
|
19
|
+
expect(result.props?.style).toEqual(styles);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should handle null styles', () => {
|
|
23
|
+
const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div', children: [] };
|
|
24
|
+
const result = applyStylesToNode(node, null);
|
|
25
|
+
expect(result).toEqual(node);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should handle undefined styles', () => {
|
|
29
|
+
const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div', children: [] };
|
|
30
|
+
const result = applyStylesToNode(node, undefined);
|
|
31
|
+
expect(result).toEqual(node);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should apply styles to embed node', () => {
|
|
35
|
+
const node = { type: NODE_TYPE.EMBED, url: 'https://example.com' };
|
|
36
|
+
const styles = { width: '100%' };
|
|
37
|
+
const result = applyStylesToNode(node as any, styles);
|
|
38
|
+
expect(result.style).toEqual(styles);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('mergeNodeStyles', () => {
|
|
43
|
+
test('should merge styles for HTML node', () => {
|
|
44
|
+
const node: HtmlNode = {
|
|
45
|
+
type: NODE_TYPE.NODE,
|
|
46
|
+
tag: 'div',
|
|
47
|
+
children: [],
|
|
48
|
+
style: { color: 'blue' }
|
|
49
|
+
};
|
|
50
|
+
const instanceStyles = { fontSize: '16px' };
|
|
51
|
+
const result = mergeNodeStyles(node, instanceStyles);
|
|
52
|
+
expect(result.style).toEqual({ color: 'blue', fontSize: '16px' });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('should merge styles for component instance', () => {
|
|
56
|
+
const node: ComponentInstanceNode = {
|
|
57
|
+
type: NODE_TYPE.COMPONENT,
|
|
58
|
+
component: 'Button',
|
|
59
|
+
props: { style: { color: 'blue' } }
|
|
60
|
+
};
|
|
61
|
+
const instanceStyles = { padding: '10px' };
|
|
62
|
+
const result = mergeNodeStyles(node, instanceStyles);
|
|
63
|
+
expect(result.props?.style).toEqual({ color: 'blue', padding: '10px' });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should override existing styles', () => {
|
|
67
|
+
const node: HtmlNode = {
|
|
68
|
+
type: NODE_TYPE.NODE,
|
|
69
|
+
tag: 'div',
|
|
70
|
+
children: [],
|
|
71
|
+
style: { color: 'blue', fontSize: '14px' }
|
|
72
|
+
};
|
|
73
|
+
const instanceStyles = { fontSize: '16px' };
|
|
74
|
+
const result = mergeNodeStyles(node, instanceStyles);
|
|
75
|
+
expect(result.style).toEqual({ color: 'blue', fontSize: '16px' });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should handle null instance styles', () => {
|
|
79
|
+
const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div', children: [] };
|
|
80
|
+
const result = mergeNodeStyles(node, null);
|
|
81
|
+
expect(result).toEqual(node);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should create props object if not exists for component', () => {
|
|
85
|
+
const node: ComponentInstanceNode = { type: NODE_TYPE.COMPONENT, component: 'Button' };
|
|
86
|
+
const instanceStyles = { padding: '10px' };
|
|
87
|
+
const result = mergeNodeStyles(node, instanceStyles);
|
|
88
|
+
expect(result.props?.style).toEqual(instanceStyles);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('extractStylesFromNode', () => {
|
|
93
|
+
test('should extract styles from HTML node', () => {
|
|
94
|
+
const node: HtmlNode = {
|
|
95
|
+
type: NODE_TYPE.NODE,
|
|
96
|
+
tag: 'div',
|
|
97
|
+
children: [],
|
|
98
|
+
style: { color: 'red', fontSize: '16px' }
|
|
99
|
+
};
|
|
100
|
+
const styles = extractStylesFromNode(node);
|
|
101
|
+
expect(styles).toEqual({ color: 'red', fontSize: '16px' });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should extract styles from component instance', () => {
|
|
105
|
+
const node: ComponentInstanceNode = {
|
|
106
|
+
type: NODE_TYPE.COMPONENT,
|
|
107
|
+
component: 'Button',
|
|
108
|
+
props: { style: { padding: '10px' } }
|
|
109
|
+
};
|
|
110
|
+
const styles = extractStylesFromNode(node);
|
|
111
|
+
expect(styles).toEqual({ padding: '10px' });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should return empty object when no styles', () => {
|
|
115
|
+
const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div', children: [] };
|
|
116
|
+
const styles = extractStylesFromNode(node);
|
|
117
|
+
expect(styles).toEqual({});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should handle component without props', () => {
|
|
121
|
+
const node: ComponentInstanceNode = { type: NODE_TYPE.COMPONENT, component: 'Button' };
|
|
122
|
+
const styles = extractStylesFromNode(node);
|
|
123
|
+
expect(styles).toEqual({});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should extract styles from embed node', () => {
|
|
127
|
+
const node = { type: NODE_TYPE.EMBED, url: 'https://example.com', style: { width: '100%' } };
|
|
128
|
+
const styles = extractStylesFromNode(node as any);
|
|
129
|
+
expect(styles).toEqual({ width: '100%' });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|