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,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
+ });