meno-core 1.0.53 → 1.1.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/.claude/settings.local.json +1 -3
- package/bin/cli.ts +66 -261
- package/build-astro.ts +150 -128
- package/dist/bin/cli.js +42 -185
- package/dist/bin/cli.js.map +2 -2
- package/dist/chunks/chunk-2AR55GYH.js +42 -0
- package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
- package/dist/chunks/{chunk-JGWFTO6P.js → chunk-2IIQK7T3.js} +2624 -1389
- package/dist/chunks/chunk-2IIQK7T3.js.map +7 -0
- package/dist/chunks/chunk-4ZRU52J2.js +169 -0
- package/dist/chunks/chunk-4ZRU52J2.js.map +7 -0
- package/dist/chunks/{chunk-UB44F4Z2.js → chunk-5X4HCN7J.js} +5 -7
- package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
- package/dist/chunks/{chunk-WQFG7PAH.js → chunk-7CCVOL5M.js} +15 -19
- package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
- package/dist/chunks/{chunk-YBLHKYFF.js → chunk-AR7LHIFY.js} +8 -2
- package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
- package/dist/chunks/{chunk-O3NAGJP4.js → chunk-NUP7H7D3.js} +135 -41
- package/dist/chunks/chunk-NUP7H7D3.js.map +7 -0
- package/dist/chunks/{chunk-X754AHS5.js → chunk-QWTQZHG3.js} +194 -90
- package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
- package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-XTKNX4FW.js} +216 -87
- package/dist/chunks/chunk-XTKNX4FW.js.map +7 -0
- package/dist/chunks/{fs-JGINUXGL.js → fs-G3ANWEFP.js} +2 -2
- package/dist/lib/client/index.js +1003 -475
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +14393 -6371
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +342 -101
- package/dist/lib/shared/index.js.map +4 -4
- package/dist/lib/shared/richtext/index.js +1 -1
- package/dist/lib/test-utils/index.js +64 -80
- package/dist/lib/test-utils/index.js.map +2 -2
- package/entries/client-router.tsx +10 -8
- package/lib/client/ClientInitializer.ts +8 -8
- package/lib/client/ErrorBoundary.test.tsx +246 -186
- package/lib/client/ErrorBoundary.tsx +183 -132
- package/lib/client/componentRegistry.test.ts +96 -108
- package/lib/client/componentRegistry.ts +1 -3
- package/lib/client/contexts/ThemeContext.tsx +3 -2
- package/lib/client/core/ComponentBuilder.test.ts +663 -560
- package/lib/client/core/ComponentBuilder.ts +427 -155
- package/lib/client/core/ComponentRenderer.test.tsx +2 -4
- package/lib/client/core/ComponentRenderer.tsx +46 -33
- package/lib/client/core/builders/embedBuilder.ts +242 -54
- package/lib/client/core/builders/linkBuilder.ts +72 -45
- package/lib/client/core/builders/linkNodeBuilder.ts +79 -54
- package/lib/client/core/builders/listBuilder.ts +155 -89
- package/lib/client/core/builders/localeListBuilder.ts +97 -62
- package/lib/client/core/builders/types.ts +5 -5
- package/lib/client/core/cmsTemplateProcessor.ts +8 -9
- package/lib/client/elementRegistry.ts +3 -3
- package/lib/client/fontFamiliesService.test.ts +2 -10
- package/lib/client/fontFamiliesService.ts +3 -3
- package/lib/client/hmr/HMRManager.tsx +47 -30
- package/lib/client/hmrCssReload.ts +49 -14
- package/lib/client/hmrWebSocket.ts +14 -19
- package/lib/client/hooks/useColorVariables.test.ts +21 -21
- package/lib/client/hooks/useColorVariables.ts +23 -18
- package/lib/client/hooks/usePropertyAutocomplete.ts +4 -6
- package/lib/client/hooks/useVariables.ts +6 -4
- package/lib/client/hydration/HydrationUtils.test.ts +24 -25
- package/lib/client/hydration/HydrationUtils.ts +3 -4
- package/lib/client/i18nConfigService.test.ts +2 -7
- package/lib/client/i18nConfigService.ts +2 -2
- package/lib/client/meno-filter/MenoFilter.test.ts +47 -49
- package/lib/client/meno-filter/MenoFilter.ts +14 -15
- package/lib/client/meno-filter/bindings.ts +18 -43
- package/lib/client/meno-filter/init.ts +4 -4
- package/lib/client/meno-filter/renderer.ts +30 -32
- package/lib/client/meno-filter/script.generated.ts +1 -3
- package/lib/client/meno-filter/ui.ts +4 -6
- package/lib/client/meno-filter/updates.ts +18 -23
- package/lib/client/meno-filter/utils.ts +1 -1
- package/lib/client/navigation.test.ts +174 -178
- package/lib/client/navigation.ts +0 -1
- package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
- package/lib/client/responsiveStyleResolver.test.ts +234 -232
- package/lib/client/responsiveStyleResolver.ts +74 -20
- package/lib/client/routing/RouteLoader.test.ts +24 -25
- package/lib/client/routing/RouteLoader.ts +27 -35
- package/lib/client/routing/Router.tsx +210 -153
- package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
- package/lib/client/scripts/ScriptExecutor.ts +18 -36
- package/lib/client/services/PrefetchService.test.ts +3 -3
- package/lib/client/services/PrefetchService.ts +23 -29
- package/lib/client/styleProcessor.test.ts +9 -9
- package/lib/client/styleProcessor.ts +24 -15
- package/lib/client/styles/StyleInjector.test.ts +122 -115
- package/lib/client/styles/StyleInjector.ts +10 -7
- package/lib/client/styles/UtilityClassCollector.ts +62 -28
- package/lib/client/styles/cspNonce.test.ts +2 -5
- package/lib/client/templateEngine.test.ts +554 -488
- package/lib/client/templateEngine.ts +203 -73
- package/lib/client/theme.test.ts +1 -1
- package/lib/client/theme.ts +0 -1
- package/lib/client/utils/toast.ts +0 -1
- package/lib/server/__integration__/api-routes.test.ts +8 -4
- package/lib/server/__integration__/cms-integration.test.ts +1 -4
- package/lib/server/__integration__/server-lifecycle.test.ts +4 -5
- package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
- package/lib/server/__integration__/static-assets.test.ts +3 -3
- package/lib/server/__integration__/test-helpers.ts +87 -74
- package/lib/server/ab/generateFunctions.ts +12 -10
- package/lib/server/astro/cmsPageEmitter.ts +26 -42
- package/lib/server/astro/componentEmitter.ts +35 -43
- package/lib/server/astro/cssCollector.ts +10 -26
- package/lib/server/astro/nodeToAstro.test.ts +1754 -34
- package/lib/server/astro/nodeToAstro.ts +232 -216
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +115 -119
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +11 -18
- package/lib/server/astro/pageEmitter.ts +9 -13
- package/lib/server/astro/tailwindMapper.test.ts +10 -37
- package/lib/server/astro/tailwindMapper.ts +33 -40
- package/lib/server/astro/templateTransformer.ts +17 -20
- package/lib/server/createServer.ts +7 -19
- package/lib/server/cssGenerator.test.ts +35 -44
- package/lib/server/cssGenerator.ts +8 -19
- package/lib/server/fileWatcher.test.ts +164 -13
- package/lib/server/fileWatcher.ts +209 -124
- package/lib/server/index.ts +14 -17
- package/lib/server/jsonLoader.test.ts +39 -2
- package/lib/server/jsonLoader.ts +39 -34
- package/lib/server/middleware/cors.test.ts +21 -21
- package/lib/server/middleware/cors.ts +28 -4
- package/lib/server/middleware/errorHandler.test.ts +7 -5
- package/lib/server/middleware/errorHandler.ts +3 -8
- package/lib/server/middleware/index.ts +0 -1
- package/lib/server/middleware/logger.test.ts +10 -8
- package/lib/server/middleware/logger.ts +17 -29
- package/lib/server/migrateTemplates.ts +3 -3
- package/lib/server/pageCache.test.ts +76 -77
- package/lib/server/pageCache.ts +0 -1
- package/lib/server/projectContext.ts +5 -4
- package/lib/server/providers/fileSystemCMSProvider.test.ts +142 -99
- package/lib/server/providers/fileSystemCMSProvider.ts +62 -38
- package/lib/server/routes/api/cms.test.ts +26 -14
- package/lib/server/routes/api/cms.ts +9 -14
- package/lib/server/routes/api/colors.test.ts +2 -2
- package/lib/server/routes/api/components.ts +31 -35
- package/lib/server/routes/api/config.ts +3 -4
- package/lib/server/routes/api/core-routes.ts +49 -76
- package/lib/server/routes/api/enums.test.ts +2 -2
- package/lib/server/routes/api/functions.ts +9 -17
- package/lib/server/routes/api/index.ts +3 -6
- package/lib/server/routes/api/pages.ts +8 -11
- package/lib/server/routes/api/shared.test.ts +1 -1
- package/lib/server/routes/api/shared.ts +12 -9
- package/lib/server/routes/api/variables.test.ts +3 -5
- package/lib/server/routes/api/variables.ts +1 -1
- package/lib/server/routes/index.ts +25 -28
- package/lib/server/routes/pages.ts +35 -31
- package/lib/server/routes/static.ts +17 -6
- package/lib/server/runtime/bundler.ts +98 -61
- package/lib/server/runtime/fs.ts +16 -26
- package/lib/server/runtime/httpServer.ts +8 -12
- package/lib/server/services/ColorService.test.ts +3 -3
- package/lib/server/services/ColorService.ts +37 -29
- package/lib/server/services/EnumService.test.ts +4 -8
- package/lib/server/services/EnumService.ts +11 -3
- package/lib/server/services/VariableService.test.ts +8 -12
- package/lib/server/services/VariableService.ts +11 -3
- package/lib/server/services/cmsService.test.ts +158 -120
- package/lib/server/services/cmsService.ts +36 -146
- package/lib/server/services/componentService.test.ts +125 -45
- package/lib/server/services/componentService.ts +70 -46
- package/lib/server/services/configService.test.ts +22 -45
- package/lib/server/services/configService.ts +50 -27
- package/lib/server/services/fileWatcherService.ts +42 -8
- package/lib/server/services/index.ts +0 -1
- package/lib/server/services/pageService.test.ts +8 -10
- package/lib/server/services/pageService.ts +32 -18
- package/lib/server/ssr/attributeBuilder.ts +18 -8
- package/lib/server/ssr/buildErrorOverlay.ts +11 -12
- package/lib/server/ssr/clientDataInjector.ts +7 -21
- package/lib/server/ssr/cmsSSRProcessor.ts +4 -8
- package/lib/server/ssr/cssCollector.ts +1 -1
- package/lib/server/ssr/errorOverlay.test.ts +1 -1
- package/lib/server/ssr/errorOverlay.ts +3 -9
- package/lib/server/ssr/htmlGenerator.nonce.test.ts +9 -14
- package/lib/server/ssr/htmlGenerator.test.ts +120 -44
- package/lib/server/ssr/htmlGenerator.ts +126 -91
- package/lib/server/ssr/imageMetadata.test.ts +3 -1
- package/lib/server/ssr/imageMetadata.ts +27 -21
- package/lib/server/ssr/jsCollector.test.ts +8 -18
- package/lib/server/ssr/jsCollector.ts +3 -8
- package/lib/server/ssr/liveReloadIntegration.test.ts +76 -36
- package/lib/server/ssr/metaTagGenerator.ts +2 -2
- package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
- package/lib/server/ssr/ssrRenderer.test.ts +316 -256
- package/lib/server/ssr/ssrRenderer.ts +795 -243
- package/lib/server/ssrRenderer.test.ts +1048 -954
- package/lib/server/utils/jsonLineMapper.test.ts +28 -28
- package/lib/server/utils/jsonLineMapper.ts +7 -7
- package/lib/server/validateStyleCoverage.ts +18 -21
- package/lib/server/websocketManager.ts +28 -19
- package/lib/shared/attributeNodeUtils.test.ts +15 -15
- package/lib/shared/attributeNodeUtils.ts +5 -12
- package/lib/shared/breakpoints.test.ts +5 -5
- package/lib/shared/breakpoints.ts +4 -11
- package/lib/shared/cmsQuery.test.ts +81 -0
- package/lib/shared/cmsQuery.ts +92 -0
- package/lib/shared/cmsQueryParser.test.ts +50 -42
- package/lib/shared/cmsQueryParser.ts +60 -38
- package/lib/shared/colorConversions.ts +9 -9
- package/lib/shared/colorVariableUtils.test.ts +27 -5
- package/lib/shared/colorVariableUtils.ts +21 -12
- package/lib/shared/componentRefs.ts +1 -5
- package/lib/shared/constants.test.ts +3 -3
- package/lib/shared/constants.ts +8 -8
- package/lib/shared/cssGeneration.test.ts +285 -144
- package/lib/shared/cssGeneration.ts +217 -531
- package/lib/shared/cssNamedColors.ts +152 -30
- package/lib/shared/cssProperties.test.ts +69 -70
- package/lib/shared/cssProperties.ts +298 -114
- package/lib/shared/elementClassName.test.ts +109 -109
- package/lib/shared/elementClassName.ts +3 -3
- package/lib/shared/elementUtils.ts +12 -16
- package/lib/shared/errorLogger.test.ts +12 -12
- package/lib/shared/errorLogger.ts +2 -10
- package/lib/shared/errors.test.ts +2 -13
- package/lib/shared/errors.ts +2 -8
- package/lib/shared/expressionEvaluator.test.ts +145 -0
- package/lib/shared/expressionEvaluator.ts +52 -24
- package/lib/shared/fontLoader.test.ts +32 -18
- package/lib/shared/fontLoader.ts +2 -2
- package/lib/shared/friendlyError.ts +2 -3
- package/lib/shared/gradientUtils.test.ts +3 -7
- package/lib/shared/gradientUtils.ts +17 -14
- package/lib/shared/hrefRefs.test.ts +2 -2
- package/lib/shared/hrefRefs.ts +2 -10
- package/lib/shared/i18n.test.ts +4 -4
- package/lib/shared/i18n.ts +18 -38
- package/lib/shared/index.ts +27 -0
- package/lib/shared/inlineSvgStyleRules.ts +14 -13
- package/lib/shared/interactiveStyleMappings.test.ts +32 -54
- package/lib/shared/interactiveStyleMappings.ts +11 -24
- package/lib/shared/interactiveStyles.test.ts +165 -188
- package/lib/shared/interfaces/contentProvider.ts +1 -1
- package/lib/shared/itemTemplateUtils.test.ts +11 -17
- package/lib/shared/itemTemplateUtils.ts +27 -39
- package/lib/shared/jsonRepair.ts +16 -10
- package/lib/shared/libraryLoader.test.ts +21 -55
- package/lib/shared/libraryLoader.ts +7 -22
- package/lib/shared/linkUtils.ts +1 -1
- package/lib/shared/logger.test.ts +66 -0
- package/lib/shared/logger.ts +94 -0
- package/lib/shared/markdown.ts +25 -0
- package/lib/shared/netlifyLocale404.test.ts +179 -0
- package/lib/shared/netlifyLocale404.ts +110 -0
- package/lib/shared/nodeUtils.test.ts +28 -16
- package/lib/shared/nodeUtils.ts +76 -23
- package/lib/shared/pathArrayUtils.test.ts +1 -2
- package/lib/shared/pathArrayUtils.ts +1 -1
- package/lib/shared/pathSecurity.ts +2 -2
- package/lib/shared/pathUtils.test.ts +4 -6
- package/lib/shared/pathUtils.ts +42 -49
- package/lib/shared/paths/Path.test.ts +2 -2
- package/lib/shared/paths/Path.ts +0 -1
- package/lib/shared/paths/PathConverter.test.ts +1 -1
- package/lib/shared/paths/PathConverter.ts +17 -20
- package/lib/shared/paths/PathUtils.ts +13 -13
- package/lib/shared/paths/PathValidator.test.ts +2 -15
- package/lib/shared/paths/PathValidator.ts +12 -10
- package/lib/shared/paths/index.ts +1 -2
- package/lib/shared/permissions.test.ts +168 -0
- package/lib/shared/permissions.ts +162 -0
- package/lib/shared/propResolver.test.ts +240 -244
- package/lib/shared/propResolver.ts +15 -26
- package/lib/shared/pxToRem.test.ts +7 -6
- package/lib/shared/pxToRem.ts +2 -5
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +12 -6
- package/lib/shared/registry/ClientRegistry.ts +0 -2
- package/lib/shared/registry/ComponentRegistry.test.ts +46 -32
- package/lib/shared/registry/ComponentRegistry.ts +12 -9
- package/lib/shared/registry/NodeTypeDefinition.ts +16 -9
- package/lib/shared/registry/RegistryManager.ts +1 -2
- package/lib/shared/registry/SSRRegistry.ts +0 -2
- package/lib/shared/registry/createNodeType.ts +7 -9
- package/lib/shared/registry/defineNodeType.ts +2 -6
- package/lib/shared/registry/fieldPresets.ts +3 -1
- package/lib/shared/registry/index.ts +0 -1
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
- package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +48 -19
- package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +117 -77
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
- package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +12 -8
- package/lib/shared/registry/nodeTypes/index.ts +26 -3
- package/lib/shared/responsiveScaling.test.ts +16 -34
- package/lib/shared/responsiveScaling.ts +61 -42
- package/lib/shared/responsiveStyleUtils.test.ts +0 -1
- package/lib/shared/responsiveStyleUtils.ts +11 -13
- package/lib/shared/richtext/htmlToTiptap.test.ts +139 -130
- package/lib/shared/richtext/htmlToTiptap.ts +2 -4
- package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
- package/lib/shared/richtext/types.ts +1 -8
- package/lib/shared/slugTranslator.test.ts +13 -13
- package/lib/shared/slugTranslator.ts +12 -16
- package/lib/shared/slugify.ts +9 -15
- package/lib/shared/styleNodeUtils.test.ts +8 -8
- package/lib/shared/styleNodeUtils.ts +18 -20
- package/lib/shared/styleUtils.test.ts +87 -61
- package/lib/shared/styleUtils.ts +5 -6
- package/lib/shared/themeDefaults.test.ts +11 -14
- package/lib/shared/themeDefaults.ts +3 -4
- package/lib/shared/tree/PathBuilder.test.ts +63 -145
- package/lib/shared/tree/PathBuilder.ts +59 -47
- package/lib/shared/treePathUtils.test.ts +51 -12
- package/lib/shared/treePathUtils.ts +91 -60
- package/lib/shared/types/api.ts +105 -8
- package/lib/shared/types/cms.test.ts +34 -0
- package/lib/shared/types/cms.ts +90 -34
- package/lib/shared/types/comment.ts +49 -16
- package/lib/shared/types/components.ts +51 -25
- package/lib/shared/types/errors.test.ts +1 -6
- package/lib/shared/types/errors.ts +3 -7
- package/lib/shared/types/experiments.ts +28 -28
- package/lib/shared/types/index.ts +25 -2
- package/lib/shared/types/permissions.ts +139 -0
- package/lib/shared/types/styles.ts +0 -1
- package/lib/shared/types/variables.test.ts +4 -13
- package/lib/shared/types/variables.ts +51 -30
- package/lib/shared/types.ts +1 -2
- package/lib/shared/utilityClassConfig.ts +751 -321
- package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
- package/lib/shared/utilityClassMapper.test.ts +433 -95
- package/lib/shared/utilityClassMapper.ts +322 -249
- package/lib/shared/utilityClassNames.test.ts +38 -0
- package/lib/shared/utilityClassNames.ts +458 -0
- package/lib/shared/utils.test.ts +2 -10
- package/lib/shared/utils.ts +19 -10
- package/lib/shared/validation/cmsValidators.ts +2 -1
- package/lib/shared/validation/commentValidators.test.ts +53 -0
- package/lib/shared/validation/commentValidators.ts +12 -1
- package/lib/shared/validation/index.ts +1 -0
- package/lib/shared/validation/permissionsValidators.test.ts +49 -0
- package/lib/shared/validation/permissionsValidators.ts +49 -0
- package/lib/shared/validation/propValidator.test.ts +19 -21
- package/lib/shared/validation/propValidator.ts +13 -18
- package/lib/shared/validation/schemas.test.ts +64 -32
- package/lib/shared/validation/schemas.ts +616 -345
- package/lib/shared/validation/validators.test.ts +3 -8
- package/lib/shared/validation/validators.ts +89 -68
- package/lib/shared/viewportUnits.integration.test.ts +14 -10
- package/lib/shared/viewportUnits.test.ts +24 -23
- package/lib/shared/viewportUnits.ts +12 -5
- package/lib/test-utils/dom-setup.ts +1 -1
- package/lib/test-utils/factories/ConsoleMockFactory.ts +10 -14
- package/lib/test-utils/factories/DomMockFactory.ts +17 -23
- package/lib/test-utils/factories/EventMockFactory.ts +7 -13
- package/lib/test-utils/factories/FetchMockFactory.ts +39 -57
- package/lib/test-utils/factories/ServerMockFactory.ts +6 -10
- package/lib/test-utils/factories/StoreMockFactory.ts +18 -29
- package/lib/test-utils/fixtures.ts +45 -45
- package/lib/test-utils/helpers/asyncHelpers.test.ts +15 -18
- package/lib/test-utils/helpers/asyncHelpers.ts +11 -20
- package/lib/test-utils/helpers.ts +1 -5
- package/lib/test-utils/index.ts +0 -4
- package/lib/test-utils/mockFactories.ts +13 -19
- package/lib/test-utils/mocks.ts +6 -4
- package/package.json +3 -5
- package/scripts/build-for-publish.mjs +2 -4
- package/scripts/build-meno-filter.ts +4 -7
- package/tsconfig.json +1 -0
- package/vite.config.ts +4 -4
- package/build-next.ts +0 -1361
- package/build-static.test.ts +0 -424
- package/build-static.ts +0 -1074
- package/dist/build-static.js +0 -36
- package/dist/chunks/chunk-2MHDV5BF.js +0 -251
- package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
- package/dist/chunks/chunk-AZQYF6KE.js +0 -559
- package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
- package/dist/chunks/chunk-GZHGVVW3.js +0 -322
- package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
- package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
- package/dist/chunks/chunk-I7YIGZXT.js +0 -467
- package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
- package/dist/chunks/chunk-IGYR22T6.js +0 -6192
- package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
- package/dist/chunks/chunk-JGP5A3Y5.js +0 -1136
- package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
- package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
- package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
- package/dist/chunks/chunk-QB2LNO4W.js +0 -77
- package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
- package/dist/chunks/chunk-R6XHAFBF.js +0 -3821
- package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
- package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
- package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
- package/dist/chunks/chunk-X754AHS5.js.map +0 -7
- package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
- package/dist/chunks/configService-R3OGU2UD.js +0 -13
- package/dist/chunks/configService-R3OGU2UD.js.map +0 -7
- package/dist/chunks/constants-STK2YBIW.js +0 -46
- package/dist/chunks/constants-STK2YBIW.js.map +0 -7
- package/dist/chunks/fs-JGINUXGL.js.map +0 -7
- package/dist/entries/server-router.js +0 -67
- package/dist/entries/server-router.js.map +0 -7
- package/entries/server-router.tsx +0 -81
- package/lib/server/providers/fileSystemPageProvider.ts +0 -160
- package/lib/server/webflow/buildWebflow.ts +0 -635
- package/lib/server/webflow/index.ts +0 -23
- package/lib/server/webflow/nodeToWebflow.test.ts +0 -3170
- package/lib/server/webflow/nodeToWebflow.ts +0 -2435
- package/lib/server/webflow/styleMapper.test.ts +0 -389
- package/lib/server/webflow/styleMapper.ts +0 -695
- package/lib/server/webflow/templateWrapper.ts +0 -49
- package/lib/server/webflow/types.ts +0 -396
- /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
|
@@ -1,361 +1,376 @@
|
|
|
1
|
-
import { test, expect, describe
|
|
2
|
-
import { setGlobalTemplateContext, resetGlobalTemplateContext } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
describe("Template Engine - evaluateTemplate", () => {
|
|
12
|
-
describe("Complex template expressions", () => {
|
|
13
|
-
test("should evaluate nested property access", () => {
|
|
14
|
-
const template = "{{variants[variant].background}}";
|
|
1
|
+
import { test, expect, describe } from 'bun:test';
|
|
2
|
+
import { setGlobalTemplateContext, resetGlobalTemplateContext } from '../shared/globalTemplateContext';
|
|
3
|
+
import { evaluateTemplate, resolveStyleMapping, processStructure, normalizeStyle } from './templateEngine';
|
|
4
|
+
import type { ComponentNode, StructuredComponentDefinition, StyleMapping, TemplateContext } from '../shared/types';
|
|
5
|
+
|
|
6
|
+
describe('Template Engine - evaluateTemplate', () => {
|
|
7
|
+
describe('Complex template expressions', () => {
|
|
8
|
+
test('should evaluate nested property access', () => {
|
|
9
|
+
const template = '{{variants[variant].background}}';
|
|
15
10
|
const context = {
|
|
16
11
|
variants: {
|
|
17
|
-
primary: { background:
|
|
18
|
-
secondary: { background:
|
|
12
|
+
primary: { background: '#0070f3' },
|
|
13
|
+
secondary: { background: '#f3f4f6' },
|
|
19
14
|
},
|
|
20
|
-
variant:
|
|
15
|
+
variant: 'primary',
|
|
21
16
|
};
|
|
22
|
-
|
|
17
|
+
|
|
23
18
|
const result = evaluateTemplate(template, context);
|
|
24
|
-
expect(result).toBe(
|
|
19
|
+
expect(result).toBe('#0070f3');
|
|
25
20
|
});
|
|
26
21
|
|
|
27
|
-
test(
|
|
22
|
+
test('should evaluate ternary operators', () => {
|
|
28
23
|
const template = "{{disabled ? '0.5' : '1'}}";
|
|
29
24
|
const context = { disabled: true };
|
|
30
|
-
|
|
25
|
+
|
|
31
26
|
const result = evaluateTemplate(template, context);
|
|
32
|
-
expect(result).toBe(
|
|
27
|
+
expect(result).toBe('0.5');
|
|
33
28
|
});
|
|
34
29
|
|
|
35
|
-
test(
|
|
30
|
+
test('should evaluate ternary operators with false condition', () => {
|
|
36
31
|
const template = "{{disabled ? '0.5' : '1'}}";
|
|
37
32
|
const context = { disabled: false };
|
|
38
|
-
|
|
33
|
+
|
|
39
34
|
const result = evaluateTemplate(template, context);
|
|
40
|
-
expect(result).toBe(
|
|
35
|
+
expect(result).toBe('1');
|
|
41
36
|
});
|
|
42
37
|
|
|
43
|
-
test(
|
|
44
|
-
const template =
|
|
38
|
+
test('should evaluate mathematical operations - addition', () => {
|
|
39
|
+
const template = '{{count + 1}}';
|
|
45
40
|
const context = { count: 5 };
|
|
46
|
-
|
|
41
|
+
|
|
47
42
|
const result = evaluateTemplate(template, context);
|
|
48
43
|
expect(result).toBe(6);
|
|
49
44
|
});
|
|
50
45
|
|
|
51
|
-
test(
|
|
52
|
-
const template =
|
|
46
|
+
test('should evaluate mathematical operations - multiplication', () => {
|
|
47
|
+
const template = '{{price * 1.1}}';
|
|
53
48
|
const context = { price: 100 };
|
|
54
|
-
|
|
49
|
+
|
|
55
50
|
const result = evaluateTemplate(template, context);
|
|
56
51
|
expect(result).toBeCloseTo(110, 5);
|
|
57
52
|
});
|
|
58
53
|
|
|
59
|
-
test(
|
|
54
|
+
test('should evaluate string concatenation', () => {
|
|
60
55
|
const template = "{{'Hello ' + name}}";
|
|
61
|
-
const context = { name:
|
|
62
|
-
|
|
56
|
+
const context = { name: 'World' };
|
|
57
|
+
|
|
63
58
|
const result = evaluateTemplate(template, context);
|
|
64
|
-
expect(result).toBe(
|
|
59
|
+
expect(result).toBe('Hello World');
|
|
65
60
|
});
|
|
66
61
|
|
|
67
|
-
test(
|
|
62
|
+
test('should evaluate logical AND operator', () => {
|
|
68
63
|
const template = "{{isActive && 'active'}}";
|
|
69
64
|
const context = { isActive: true };
|
|
70
|
-
|
|
65
|
+
|
|
71
66
|
const result = evaluateTemplate(template, context);
|
|
72
|
-
expect(result).toBe(
|
|
67
|
+
expect(result).toBe('active');
|
|
73
68
|
});
|
|
74
69
|
|
|
75
|
-
test(
|
|
70
|
+
test('should evaluate logical AND operator with false', () => {
|
|
76
71
|
const template = "{{isActive && 'active'}}";
|
|
77
72
|
const context = { isActive: false };
|
|
78
|
-
|
|
73
|
+
|
|
79
74
|
const result = evaluateTemplate(template, context);
|
|
80
75
|
expect(result).toBe(false);
|
|
81
76
|
});
|
|
82
77
|
|
|
83
|
-
test(
|
|
78
|
+
test('should evaluate complex nested expressions', () => {
|
|
84
79
|
const template = "{{user.profile.name || 'Anonymous'}}";
|
|
85
|
-
const context = { user: { profile: { name:
|
|
86
|
-
|
|
80
|
+
const context = { user: { profile: { name: 'John' } } };
|
|
81
|
+
|
|
87
82
|
const result = evaluateTemplate(template, context);
|
|
88
|
-
expect(result).toBe(
|
|
83
|
+
expect(result).toBe('John');
|
|
89
84
|
});
|
|
90
85
|
|
|
91
|
-
test(
|
|
86
|
+
test('should evaluate expressions with fallback', () => {
|
|
92
87
|
const template = "{{user.profile.name || 'Anonymous'}}";
|
|
93
88
|
const context = { user: { profile: {} } };
|
|
94
|
-
|
|
89
|
+
|
|
95
90
|
const result = evaluateTemplate(template, context);
|
|
96
|
-
expect(result).toBe(
|
|
91
|
+
expect(result).toBe('Anonymous');
|
|
97
92
|
});
|
|
98
93
|
});
|
|
99
94
|
|
|
100
|
-
describe(
|
|
101
|
-
test(
|
|
102
|
-
const template =
|
|
103
|
-
const context = { name:
|
|
104
|
-
|
|
95
|
+
describe('Edge cases', () => {
|
|
96
|
+
test('should return original string if not a template', () => {
|
|
97
|
+
const template = 'plain text';
|
|
98
|
+
const context = { name: 'John' };
|
|
99
|
+
|
|
105
100
|
const result = evaluateTemplate(template, context);
|
|
106
|
-
expect(result).toBe(
|
|
101
|
+
expect(result).toBe('plain text');
|
|
107
102
|
});
|
|
108
103
|
|
|
109
|
-
test(
|
|
110
|
-
const template =
|
|
104
|
+
test('should return template string on invalid expression', () => {
|
|
105
|
+
const template = '{{invalid.expression}}';
|
|
111
106
|
const context = {};
|
|
112
|
-
|
|
107
|
+
|
|
113
108
|
const result = evaluateTemplate(template, context);
|
|
114
109
|
expect(result).toBe(template); // Should return original template on error
|
|
115
110
|
});
|
|
116
111
|
|
|
117
|
-
test(
|
|
118
|
-
const template =
|
|
112
|
+
test('should return template string on syntax error', () => {
|
|
113
|
+
const template = '{{count +}}'; // Missing operand
|
|
119
114
|
const context = { count: 5 };
|
|
120
|
-
|
|
115
|
+
|
|
121
116
|
const result = evaluateTemplate(template, context);
|
|
122
117
|
expect(result).toBe(template); // Should return original template on error
|
|
123
118
|
});
|
|
124
119
|
|
|
125
|
-
test(
|
|
126
|
-
const template =
|
|
120
|
+
test('should handle missing context variables', () => {
|
|
121
|
+
const template = '{{undefinedVar}}';
|
|
127
122
|
const context = {};
|
|
128
|
-
|
|
123
|
+
|
|
129
124
|
const result = evaluateTemplate(template, context);
|
|
130
125
|
expect(result).toBe(template); // Should return original template when var is undefined
|
|
131
126
|
});
|
|
132
127
|
|
|
133
|
-
test(
|
|
128
|
+
test('should handle non-string templates', () => {
|
|
134
129
|
const template = 123 as unknown as string;
|
|
135
130
|
const context = {};
|
|
136
|
-
|
|
131
|
+
|
|
137
132
|
const result = evaluateTemplate(template, context);
|
|
138
133
|
expect(result).toBe(123); // Should pass through non-strings
|
|
139
134
|
});
|
|
140
135
|
|
|
141
|
-
test(
|
|
142
|
-
const template =
|
|
136
|
+
test('should handle empty template expression', () => {
|
|
137
|
+
const template = '{{}}';
|
|
143
138
|
const context = {};
|
|
144
|
-
|
|
139
|
+
|
|
145
140
|
const result = evaluateTemplate(template, context);
|
|
146
141
|
expect(result).toBe(template); // Invalid expression
|
|
147
142
|
});
|
|
148
143
|
|
|
149
|
-
test(
|
|
150
|
-
const template =
|
|
151
|
-
const context = { name:
|
|
152
|
-
|
|
144
|
+
test('should handle template with whitespace', () => {
|
|
145
|
+
const template = '{{ name }}';
|
|
146
|
+
const context = { name: 'John' };
|
|
147
|
+
|
|
153
148
|
const result = evaluateTemplate(template, context);
|
|
154
|
-
expect(result).toBe(
|
|
149
|
+
expect(result).toBe('John'); // Should trim whitespace
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('NaN result (arithmetic over a missing identifier) preserves the template', () => {
|
|
153
|
+
// A loop-var stagger evaluated by the props pass BEFORE the per-item pass:
|
|
154
|
+
// itemIndex isn't in context yet → 0.4 + undefined*0.2 = NaN. Returning NaN
|
|
155
|
+
// poisoned the attribute (React: "Received NaN for the `fade` attribute") and
|
|
156
|
+
// destroyed the template before the item pass could resolve it.
|
|
157
|
+
const template = '{{0.4+itemIndex*0.2}}';
|
|
158
|
+
|
|
159
|
+
expect(evaluateTemplate(template, {})).toBe(template);
|
|
160
|
+
expect(evaluateTemplate(template, { fade: '0.2' })).toBe(template);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('the same arithmetic template resolves once the identifier is in context', () => {
|
|
164
|
+
expect(evaluateTemplate('{{0.4+itemIndex*0.2}}', { itemIndex: 2 })).toBeCloseTo(0.8);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('a legitimately-numeric result still passes through (only NaN is preserved)', () => {
|
|
168
|
+
expect(evaluateTemplate('{{count / 0}}', { count: 5 })).toBe(Infinity);
|
|
169
|
+
expect(evaluateTemplate('{{count - 5}}', { count: 5 })).toBe(0);
|
|
155
170
|
});
|
|
156
171
|
});
|
|
157
172
|
});
|
|
158
173
|
|
|
159
|
-
describe(
|
|
160
|
-
describe(
|
|
161
|
-
test(
|
|
174
|
+
describe('Template Engine - resolveStyleMapping', () => {
|
|
175
|
+
describe('Valid style mappings', () => {
|
|
176
|
+
test('should resolve prop-based style mapping', () => {
|
|
162
177
|
const mapping: StyleMapping = {
|
|
163
178
|
_mapping: true,
|
|
164
|
-
prop:
|
|
179
|
+
prop: 'variant',
|
|
165
180
|
values: {
|
|
166
|
-
primary:
|
|
167
|
-
secondary:
|
|
168
|
-
}
|
|
181
|
+
primary: '#0070f3',
|
|
182
|
+
secondary: '#f3f4f6',
|
|
183
|
+
},
|
|
169
184
|
};
|
|
170
|
-
const props = { variant:
|
|
171
|
-
|
|
185
|
+
const props = { variant: 'primary' };
|
|
186
|
+
|
|
172
187
|
const result = resolveStyleMapping(mapping, props);
|
|
173
|
-
expect(result).toBe(
|
|
188
|
+
expect(result).toBe('#0070f3');
|
|
174
189
|
});
|
|
175
190
|
|
|
176
|
-
test(
|
|
191
|
+
test('should resolve style mapping for secondary variant', () => {
|
|
177
192
|
const mapping: StyleMapping = {
|
|
178
193
|
_mapping: true,
|
|
179
|
-
prop:
|
|
194
|
+
prop: 'variant',
|
|
180
195
|
values: {
|
|
181
|
-
primary:
|
|
182
|
-
secondary:
|
|
183
|
-
}
|
|
196
|
+
primary: '#0070f3',
|
|
197
|
+
secondary: '#f3f4f6',
|
|
198
|
+
},
|
|
184
199
|
};
|
|
185
|
-
const props = { variant:
|
|
186
|
-
|
|
200
|
+
const props = { variant: 'secondary' };
|
|
201
|
+
|
|
187
202
|
const result = resolveStyleMapping(mapping, props);
|
|
188
|
-
expect(result).toBe(
|
|
203
|
+
expect(result).toBe('#f3f4f6');
|
|
189
204
|
});
|
|
190
205
|
|
|
191
|
-
test(
|
|
206
|
+
test('should resolve numeric style mapping values', () => {
|
|
192
207
|
const mapping: StyleMapping = {
|
|
193
208
|
_mapping: true,
|
|
194
|
-
prop:
|
|
209
|
+
prop: 'size',
|
|
195
210
|
values: {
|
|
196
211
|
small: 12,
|
|
197
|
-
large: 24
|
|
198
|
-
}
|
|
212
|
+
large: 24,
|
|
213
|
+
},
|
|
199
214
|
};
|
|
200
|
-
const props = { size:
|
|
201
|
-
|
|
215
|
+
const props = { size: 'small' };
|
|
216
|
+
|
|
202
217
|
const result = resolveStyleMapping(mapping, props);
|
|
203
218
|
expect(result).toBe(12);
|
|
204
219
|
});
|
|
205
220
|
});
|
|
206
221
|
|
|
207
|
-
describe(
|
|
208
|
-
test(
|
|
222
|
+
describe('Missing or invalid mappings', () => {
|
|
223
|
+
test('should return undefined for missing prop value', () => {
|
|
209
224
|
const mapping: StyleMapping = {
|
|
210
225
|
_mapping: true,
|
|
211
|
-
prop:
|
|
226
|
+
prop: 'variant',
|
|
212
227
|
values: {
|
|
213
|
-
primary:
|
|
214
|
-
secondary:
|
|
215
|
-
}
|
|
228
|
+
primary: '#0070f3',
|
|
229
|
+
secondary: '#f3f4f6',
|
|
230
|
+
},
|
|
216
231
|
};
|
|
217
232
|
const props = {}; // variant not provided
|
|
218
|
-
|
|
233
|
+
|
|
219
234
|
const result = resolveStyleMapping(mapping, props);
|
|
220
235
|
expect(result).toBeUndefined();
|
|
221
236
|
});
|
|
222
237
|
|
|
223
|
-
test(
|
|
238
|
+
test('should return undefined for null prop value', () => {
|
|
224
239
|
const mapping: StyleMapping = {
|
|
225
240
|
_mapping: true,
|
|
226
|
-
prop:
|
|
241
|
+
prop: 'variant',
|
|
227
242
|
values: {
|
|
228
|
-
primary:
|
|
229
|
-
}
|
|
243
|
+
primary: '#0070f3',
|
|
244
|
+
},
|
|
230
245
|
};
|
|
231
246
|
const props = { variant: null };
|
|
232
|
-
|
|
247
|
+
|
|
233
248
|
const result = resolveStyleMapping(mapping, props);
|
|
234
249
|
expect(result).toBeUndefined();
|
|
235
250
|
});
|
|
236
251
|
|
|
237
|
-
test(
|
|
252
|
+
test('should return undefined for prop value not in mapping', () => {
|
|
238
253
|
const mapping: StyleMapping = {
|
|
239
254
|
_mapping: true,
|
|
240
|
-
prop:
|
|
255
|
+
prop: 'variant',
|
|
241
256
|
values: {
|
|
242
|
-
primary:
|
|
243
|
-
}
|
|
257
|
+
primary: '#0070f3',
|
|
258
|
+
},
|
|
244
259
|
};
|
|
245
|
-
const props = { variant:
|
|
246
|
-
|
|
260
|
+
const props = { variant: 'invalid' };
|
|
261
|
+
|
|
247
262
|
const result = resolveStyleMapping(mapping, props);
|
|
248
263
|
expect(result).toBeUndefined();
|
|
249
264
|
});
|
|
250
265
|
|
|
251
|
-
test(
|
|
266
|
+
test('should return undefined for invalid mapping object (no _mapping)', () => {
|
|
252
267
|
const mapping = {
|
|
253
|
-
prop:
|
|
254
|
-
values: { primary:
|
|
268
|
+
prop: 'variant',
|
|
269
|
+
values: { primary: '#0070f3' },
|
|
255
270
|
};
|
|
256
|
-
const props = { variant:
|
|
257
|
-
|
|
271
|
+
const props = { variant: 'primary' };
|
|
272
|
+
|
|
258
273
|
const result = resolveStyleMapping(mapping, props);
|
|
259
274
|
expect(result).toBeUndefined();
|
|
260
275
|
});
|
|
261
276
|
|
|
262
|
-
test(
|
|
263
|
-
const mapping =
|
|
264
|
-
const props = { variant:
|
|
265
|
-
|
|
277
|
+
test('should return undefined for non-object mapping', () => {
|
|
278
|
+
const mapping = 'not an object';
|
|
279
|
+
const props = { variant: 'primary' };
|
|
280
|
+
|
|
266
281
|
const result = resolveStyleMapping(mapping, props);
|
|
267
282
|
expect(result).toBeUndefined();
|
|
268
283
|
});
|
|
269
284
|
|
|
270
|
-
test(
|
|
285
|
+
test('should return undefined for null mapping', () => {
|
|
271
286
|
const mapping = null;
|
|
272
|
-
const props = { variant:
|
|
273
|
-
|
|
287
|
+
const props = { variant: 'primary' };
|
|
288
|
+
|
|
274
289
|
const result = resolveStyleMapping(mapping, props);
|
|
275
290
|
expect(result).toBeUndefined();
|
|
276
291
|
});
|
|
277
292
|
|
|
278
|
-
test(
|
|
293
|
+
test('should return undefined for mapping with missing prop field', () => {
|
|
279
294
|
const mapping = {
|
|
280
295
|
_mapping: true,
|
|
281
|
-
values: { primary:
|
|
296
|
+
values: { primary: '#0070f3' },
|
|
282
297
|
};
|
|
283
|
-
const props = { variant:
|
|
284
|
-
|
|
298
|
+
const props = { variant: 'primary' };
|
|
299
|
+
|
|
285
300
|
const result = resolveStyleMapping(mapping, props);
|
|
286
301
|
expect(result).toBeUndefined();
|
|
287
302
|
});
|
|
288
303
|
|
|
289
|
-
test(
|
|
304
|
+
test('should return undefined for mapping with missing values field', () => {
|
|
290
305
|
const mapping = {
|
|
291
306
|
_mapping: true,
|
|
292
|
-
prop:
|
|
307
|
+
prop: 'variant',
|
|
293
308
|
};
|
|
294
|
-
const props = { variant:
|
|
295
|
-
|
|
309
|
+
const props = { variant: 'primary' };
|
|
310
|
+
|
|
296
311
|
const result = resolveStyleMapping(mapping, props);
|
|
297
312
|
expect(result).toBeUndefined();
|
|
298
313
|
});
|
|
299
314
|
});
|
|
300
315
|
});
|
|
301
316
|
|
|
302
|
-
describe(
|
|
317
|
+
describe('Template Engine - processStructure', () => {
|
|
303
318
|
const mockComponentDef: StructuredComponentDefinition = {
|
|
304
319
|
interface: {},
|
|
305
|
-
structure: { type: 'node' as const, tag: 'div' }
|
|
320
|
+
structure: { type: 'node' as const, tag: 'div' },
|
|
306
321
|
};
|
|
307
322
|
|
|
308
323
|
// Helper to create TemplateContext
|
|
309
324
|
const createContext = (props: Record<string, unknown>, componentDef = mockComponentDef): TemplateContext => ({
|
|
310
325
|
props,
|
|
311
|
-
componentDef
|
|
326
|
+
componentDef,
|
|
312
327
|
});
|
|
313
328
|
|
|
314
|
-
describe(
|
|
315
|
-
test(
|
|
316
|
-
const structure =
|
|
317
|
-
const props = { name:
|
|
329
|
+
describe('String processing', () => {
|
|
330
|
+
test('should process template strings in structure', () => {
|
|
331
|
+
const structure = '{{name}}';
|
|
332
|
+
const props = { name: 'John' };
|
|
318
333
|
|
|
319
334
|
const result = processStructure(structure, createContext(props));
|
|
320
|
-
expect(result).toBe(
|
|
335
|
+
expect(result).toBe('John');
|
|
321
336
|
});
|
|
322
337
|
|
|
323
|
-
test(
|
|
324
|
-
const structure =
|
|
338
|
+
test('should process non-template strings', () => {
|
|
339
|
+
const structure = 'plain text';
|
|
325
340
|
const props = {};
|
|
326
341
|
|
|
327
342
|
const result = processStructure(structure, createContext(props));
|
|
328
|
-
expect(result).toBe(
|
|
343
|
+
expect(result).toBe('plain text');
|
|
329
344
|
});
|
|
330
345
|
});
|
|
331
346
|
|
|
332
|
-
describe(
|
|
333
|
-
test(
|
|
334
|
-
const structure = [
|
|
335
|
-
const props = { name:
|
|
347
|
+
describe('Array processing', () => {
|
|
348
|
+
test('should process array of strings', () => {
|
|
349
|
+
const structure = ['Hello', '{{name}}', 'World'] as any;
|
|
350
|
+
const props = { name: 'John' };
|
|
336
351
|
|
|
337
352
|
const result = processStructure(structure, createContext(props));
|
|
338
353
|
expect(Array.isArray(result)).toBe(true);
|
|
339
354
|
const resultArray = result as (string | ComponentNode)[];
|
|
340
|
-
expect(resultArray).toEqual([
|
|
355
|
+
expect(resultArray).toEqual(['Hello', 'John', 'World']);
|
|
341
356
|
});
|
|
342
357
|
|
|
343
|
-
test(
|
|
358
|
+
test('should process array of component nodes', () => {
|
|
344
359
|
const structure: ComponentNode[] = [
|
|
345
|
-
{ type:
|
|
346
|
-
{ type:
|
|
360
|
+
{ type: 'node', tag: 'div', children: ['{{text}}'] },
|
|
361
|
+
{ type: 'node', tag: 'span', children: ['Static'] },
|
|
347
362
|
];
|
|
348
|
-
const props = { text:
|
|
363
|
+
const props = { text: 'Dynamic' };
|
|
349
364
|
|
|
350
365
|
const result = processStructure(structure, createContext(props));
|
|
351
366
|
expect(Array.isArray(result)).toBe(true);
|
|
352
367
|
const resultArray = result as ComponentNode[];
|
|
353
|
-
expect(resultArray[0]
|
|
354
|
-
expect(resultArray[1]
|
|
368
|
+
expect(resultArray[0]!.children).toEqual(['Dynamic']);
|
|
369
|
+
expect(resultArray[1]!.children).toEqual(['Static']);
|
|
355
370
|
});
|
|
356
371
|
|
|
357
|
-
test(
|
|
358
|
-
const structure = [
|
|
372
|
+
test('should filter null values from arrays', () => {
|
|
373
|
+
const structure = ['Hello', null, 'World'] as any;
|
|
359
374
|
const props = {};
|
|
360
375
|
|
|
361
376
|
const result = processStructure(structure, createContext(props));
|
|
@@ -365,217 +380,224 @@ describe("Template Engine - processStructure", () => {
|
|
|
365
380
|
});
|
|
366
381
|
});
|
|
367
382
|
|
|
368
|
-
describe(
|
|
369
|
-
test(
|
|
383
|
+
describe('Component node processing', () => {
|
|
384
|
+
test('should process simple component node', () => {
|
|
370
385
|
const structure: ComponentNode = {
|
|
371
|
-
type:
|
|
372
|
-
tag:
|
|
373
|
-
children: [
|
|
386
|
+
type: 'node',
|
|
387
|
+
tag: 'div',
|
|
388
|
+
children: ['Hello World'],
|
|
374
389
|
};
|
|
375
390
|
const props = {};
|
|
376
391
|
|
|
377
392
|
const result = processStructure(structure, createContext(props));
|
|
378
393
|
expect(typeof result === 'object' && !Array.isArray(result)).toBe(true);
|
|
379
394
|
const node = result as ComponentNode;
|
|
380
|
-
expect(node.tag).toBe(
|
|
381
|
-
expect(node.children).toEqual([
|
|
395
|
+
expect(node.tag).toBe('div');
|
|
396
|
+
expect(node.children).toEqual(['Hello World']);
|
|
382
397
|
});
|
|
383
398
|
|
|
384
|
-
test(
|
|
399
|
+
test('should process component node with template in children', () => {
|
|
385
400
|
const structure: ComponentNode = {
|
|
386
|
-
type:
|
|
387
|
-
tag:
|
|
388
|
-
children: [
|
|
401
|
+
type: 'node',
|
|
402
|
+
tag: 'div',
|
|
403
|
+
children: ['{{greeting}}'],
|
|
389
404
|
};
|
|
390
|
-
const props = { greeting:
|
|
405
|
+
const props = { greeting: 'Hello' };
|
|
391
406
|
|
|
392
407
|
const result = processStructure(structure, createContext(props));
|
|
393
408
|
const node = result as ComponentNode;
|
|
394
|
-
expect(node.children).toEqual([
|
|
409
|
+
expect(node.children).toEqual(['Hello']);
|
|
395
410
|
});
|
|
396
411
|
|
|
397
|
-
test(
|
|
412
|
+
test('should process template in tag property', () => {
|
|
398
413
|
const structure: ComponentNode = {
|
|
399
|
-
type:
|
|
400
|
-
tag:
|
|
401
|
-
children: [
|
|
414
|
+
type: 'node',
|
|
415
|
+
tag: 'h{{size}}',
|
|
416
|
+
children: ['Heading'],
|
|
402
417
|
};
|
|
403
418
|
const props = { size: 1 };
|
|
404
419
|
|
|
405
420
|
const result = processStructure(structure, createContext(props));
|
|
406
421
|
const node = result as ComponentNode;
|
|
407
|
-
expect(node.tag).toBe(
|
|
408
|
-
expect(node.children).toEqual([
|
|
422
|
+
expect(node.tag).toBe('h1');
|
|
423
|
+
expect(node.children).toEqual(['Heading']);
|
|
409
424
|
});
|
|
410
425
|
|
|
411
|
-
test(
|
|
426
|
+
test('should process template in tag with string prop', () => {
|
|
412
427
|
const structure: ComponentNode = {
|
|
413
|
-
type:
|
|
414
|
-
tag:
|
|
415
|
-
children: [
|
|
428
|
+
type: 'node',
|
|
429
|
+
tag: 'h{{level}}',
|
|
430
|
+
children: ['Title'],
|
|
416
431
|
};
|
|
417
|
-
const props = { level:
|
|
432
|
+
const props = { level: '2' };
|
|
418
433
|
|
|
419
434
|
const result = processStructure(structure, createContext(props));
|
|
420
435
|
const node = result as ComponentNode;
|
|
421
|
-
expect(node.tag).toBe(
|
|
436
|
+
expect(node.tag).toBe('h2');
|
|
422
437
|
});
|
|
423
438
|
|
|
424
|
-
test(
|
|
439
|
+
test('should process nested component structures', () => {
|
|
425
440
|
const structure: ComponentNode = {
|
|
426
|
-
type:
|
|
427
|
-
tag:
|
|
441
|
+
type: 'node',
|
|
442
|
+
tag: 'div',
|
|
428
443
|
children: [
|
|
429
444
|
{
|
|
430
|
-
type:
|
|
431
|
-
tag:
|
|
432
|
-
children: [
|
|
433
|
-
}
|
|
434
|
-
]
|
|
445
|
+
type: 'node',
|
|
446
|
+
tag: 'span',
|
|
447
|
+
children: ['{{text}}'],
|
|
448
|
+
},
|
|
449
|
+
],
|
|
435
450
|
};
|
|
436
|
-
const props = { text:
|
|
451
|
+
const props = { text: 'Nested' };
|
|
437
452
|
|
|
438
453
|
const result = processStructure(structure, createContext(props));
|
|
439
454
|
const node = result as ComponentNode;
|
|
440
455
|
const children = node.children as (string | ComponentNode)[];
|
|
441
456
|
expect(children?.[0]).toBeDefined();
|
|
442
457
|
const child = children[0] as ComponentNode;
|
|
443
|
-
expect(child.children).toEqual([
|
|
458
|
+
expect(child.children).toEqual(['Nested']);
|
|
444
459
|
});
|
|
445
460
|
});
|
|
446
461
|
|
|
447
|
-
describe(
|
|
448
|
-
test(
|
|
462
|
+
describe('Style mapping resolution in structure', () => {
|
|
463
|
+
test('should resolve style mappings in flat style objects', () => {
|
|
449
464
|
const structure: ComponentNode = {
|
|
450
|
-
type:
|
|
451
|
-
tag:
|
|
465
|
+
type: 'node',
|
|
466
|
+
tag: 'button',
|
|
452
467
|
style: {
|
|
453
468
|
base: {
|
|
454
469
|
background: {
|
|
455
470
|
_mapping: true,
|
|
456
|
-
prop:
|
|
471
|
+
prop: 'variant',
|
|
457
472
|
values: {
|
|
458
|
-
primary:
|
|
459
|
-
secondary:
|
|
460
|
-
}
|
|
473
|
+
primary: '#0070f3',
|
|
474
|
+
secondary: '#f3f4f6',
|
|
475
|
+
},
|
|
461
476
|
} as StyleMapping,
|
|
462
|
-
color:
|
|
463
|
-
}
|
|
464
|
-
}
|
|
477
|
+
color: '#000',
|
|
478
|
+
},
|
|
479
|
+
},
|
|
465
480
|
};
|
|
466
|
-
const props = { variant:
|
|
481
|
+
const props = { variant: 'primary' };
|
|
467
482
|
|
|
468
483
|
const result = processStructure(structure, createContext(props));
|
|
469
484
|
const node = result as ComponentNode;
|
|
470
485
|
const baseStyle = (node.style as any)?.base;
|
|
471
|
-
expect(baseStyle?.background).toBe(
|
|
472
|
-
expect(baseStyle?.color).toBe(
|
|
486
|
+
expect(baseStyle?.background).toBe('#0070f3');
|
|
487
|
+
expect(baseStyle?.color).toBe('#000');
|
|
473
488
|
});
|
|
474
489
|
|
|
475
|
-
test(
|
|
490
|
+
test('should resolve style mappings in responsive breakpoint styles (tablet/mobile)', () => {
|
|
476
491
|
const structure: ComponentNode = {
|
|
477
|
-
type:
|
|
478
|
-
tag:
|
|
492
|
+
type: 'node',
|
|
493
|
+
tag: 'h1',
|
|
479
494
|
style: {
|
|
480
495
|
base: {
|
|
481
496
|
fontSize: {
|
|
482
497
|
_mapping: true,
|
|
483
|
-
prop:
|
|
484
|
-
values: {
|
|
498
|
+
prop: 'size',
|
|
499
|
+
values: { '1': '48px', '2': '40px', default: '48px' },
|
|
485
500
|
} as StyleMapping,
|
|
486
|
-
fontWeight:
|
|
501
|
+
fontWeight: '700',
|
|
487
502
|
},
|
|
488
503
|
tablet: {
|
|
489
504
|
fontSize: {
|
|
490
505
|
_mapping: true,
|
|
491
|
-
prop:
|
|
492
|
-
values: {
|
|
506
|
+
prop: 'size',
|
|
507
|
+
values: { '1': '132px', '2': '35px', default: '42px' },
|
|
493
508
|
} as StyleMapping,
|
|
494
509
|
},
|
|
495
|
-
mobile: {}
|
|
496
|
-
}
|
|
510
|
+
mobile: {},
|
|
511
|
+
},
|
|
497
512
|
};
|
|
498
|
-
const props = { size:
|
|
513
|
+
const props = { size: '1' };
|
|
499
514
|
|
|
500
515
|
const result = processStructure(structure, createContext(props));
|
|
501
516
|
const node = result as ComponentNode;
|
|
502
517
|
const style = node.style as any;
|
|
503
|
-
expect(style?.base?.fontSize).toBe(
|
|
504
|
-
expect(style?.base?.fontWeight).toBe(
|
|
505
|
-
expect(style?.tablet?.fontSize).toBe(
|
|
518
|
+
expect(style?.base?.fontSize).toBe('48px');
|
|
519
|
+
expect(style?.base?.fontWeight).toBe('700');
|
|
520
|
+
expect(style?.tablet?.fontSize).toBe('132px');
|
|
506
521
|
expect(style?.mobile).toEqual({});
|
|
507
522
|
});
|
|
508
523
|
|
|
509
|
-
test(
|
|
524
|
+
test('should resolve responsive style mappings with different prop values', () => {
|
|
510
525
|
const structure: ComponentNode = {
|
|
511
|
-
type:
|
|
512
|
-
tag:
|
|
526
|
+
type: 'node',
|
|
527
|
+
tag: 'h2',
|
|
513
528
|
style: {
|
|
514
529
|
base: {
|
|
515
530
|
fontSize: {
|
|
516
531
|
_mapping: true,
|
|
517
|
-
prop:
|
|
518
|
-
values: {
|
|
532
|
+
prop: 'size',
|
|
533
|
+
values: { '1': '48px', '2': '40px' },
|
|
519
534
|
} as StyleMapping,
|
|
520
535
|
},
|
|
521
536
|
tablet: {
|
|
522
537
|
fontSize: {
|
|
523
538
|
_mapping: true,
|
|
524
|
-
prop:
|
|
525
|
-
values: {
|
|
539
|
+
prop: 'size',
|
|
540
|
+
values: { '1': '132px', '2': '35px' },
|
|
526
541
|
} as StyleMapping,
|
|
527
|
-
}
|
|
528
|
-
}
|
|
542
|
+
},
|
|
543
|
+
},
|
|
529
544
|
};
|
|
530
|
-
const props = { size:
|
|
545
|
+
const props = { size: '2' };
|
|
531
546
|
|
|
532
547
|
const result = processStructure(structure, createContext(props));
|
|
533
548
|
const node = result as ComponentNode;
|
|
534
549
|
const style = node.style as any;
|
|
535
|
-
expect(style?.base?.fontSize).toBe(
|
|
536
|
-
expect(style?.tablet?.fontSize).toBe(
|
|
550
|
+
expect(style?.base?.fontSize).toBe('40px');
|
|
551
|
+
expect(style?.tablet?.fontSize).toBe('35px');
|
|
537
552
|
});
|
|
538
553
|
|
|
539
|
-
test(
|
|
554
|
+
test('should resolve multiple style mappings', () => {
|
|
540
555
|
const structure: ComponentNode = {
|
|
541
|
-
type:
|
|
542
|
-
tag:
|
|
556
|
+
type: 'node',
|
|
557
|
+
tag: 'button',
|
|
543
558
|
style: {
|
|
544
559
|
base: {
|
|
545
560
|
background: {
|
|
546
561
|
_mapping: true,
|
|
547
|
-
prop:
|
|
548
|
-
values: { primary:
|
|
562
|
+
prop: 'variant',
|
|
563
|
+
values: { primary: 'blue', secondary: 'gray' },
|
|
549
564
|
} as StyleMapping,
|
|
550
565
|
color: {
|
|
551
566
|
_mapping: true,
|
|
552
|
-
prop:
|
|
553
|
-
values: { primary:
|
|
554
|
-
} as StyleMapping
|
|
555
|
-
}
|
|
556
|
-
}
|
|
567
|
+
prop: 'variant',
|
|
568
|
+
values: { primary: 'white', secondary: 'black' },
|
|
569
|
+
} as StyleMapping,
|
|
570
|
+
},
|
|
571
|
+
},
|
|
557
572
|
};
|
|
558
|
-
const props = { variant:
|
|
573
|
+
const props = { variant: 'primary' };
|
|
559
574
|
|
|
560
575
|
const result = processStructure(structure, createContext(props));
|
|
561
576
|
const node = result as ComponentNode;
|
|
562
577
|
const baseStyle = (node.style as any)?.base;
|
|
563
|
-
expect(baseStyle?.background).toBe(
|
|
564
|
-
expect(baseStyle?.color).toBe(
|
|
578
|
+
expect(baseStyle?.background).toBe('blue');
|
|
579
|
+
expect(baseStyle?.color).toBe('white');
|
|
565
580
|
});
|
|
566
581
|
});
|
|
567
582
|
|
|
568
|
-
describe(
|
|
569
|
-
test(
|
|
583
|
+
describe('Template evaluation in style objects', () => {
|
|
584
|
+
test('should evaluate templates in style values', () => {
|
|
585
|
+
// NOTE: the ternary deliberately avoids decimal literals like '0.5'.
|
|
586
|
+
// Since 2bd781ed, hasItemTemplates() treats any `\w+.\w+` inside a full
|
|
587
|
+
// {{...}} template as a potential item-field access and DEFERS evaluation
|
|
588
|
+
// to item-template processing — a decimal literal ('0.5') false-positives
|
|
589
|
+
// that check, so the template would pass through unresolved here. That
|
|
590
|
+
// false positive is a known product issue (see report); this test covers
|
|
591
|
+
// the eager-evaluation path that still works.
|
|
570
592
|
const structure: ComponentNode = {
|
|
571
|
-
type:
|
|
572
|
-
tag:
|
|
593
|
+
type: 'node',
|
|
594
|
+
tag: 'div',
|
|
573
595
|
style: {
|
|
574
596
|
base: {
|
|
575
|
-
opacity: "{{disabled ? '0
|
|
576
|
-
fontSize:
|
|
577
|
-
}
|
|
578
|
-
}
|
|
597
|
+
opacity: "{{disabled ? '0' : '1'}}",
|
|
598
|
+
fontSize: '{{size}}px',
|
|
599
|
+
},
|
|
600
|
+
},
|
|
579
601
|
};
|
|
580
602
|
const props = { disabled: true, size: 16 };
|
|
581
603
|
|
|
@@ -583,127 +605,159 @@ describe("Template Engine - processStructure", () => {
|
|
|
583
605
|
const node = result as ComponentNode;
|
|
584
606
|
const baseStyle = (node.style as any)?.base;
|
|
585
607
|
expect(baseStyle).toBeDefined();
|
|
586
|
-
expect(baseStyle?.opacity).toBe(
|
|
587
|
-
expect(baseStyle?.fontSize).toBe(
|
|
608
|
+
expect(baseStyle?.opacity).toBe('0');
|
|
609
|
+
expect(baseStyle?.fontSize).toBe('16px');
|
|
588
610
|
});
|
|
589
611
|
|
|
590
|
-
test(
|
|
612
|
+
test('defers full templates containing dotted terms (e.g. decimals) to item processing', () => {
|
|
613
|
+
// Documents the current (arguably buggy) behavior: '0.5' matches the
|
|
614
|
+
// item-field-access pattern in hasItemTemplates(), so processStructure
|
|
615
|
+
// leaves the whole template unresolved for later item-template passes —
|
|
616
|
+
// even though no item context exists. Reported as a product issue.
|
|
617
|
+
const structure: ComponentNode = {
|
|
618
|
+
type: 'node',
|
|
619
|
+
tag: 'div',
|
|
620
|
+
style: {
|
|
621
|
+
base: {
|
|
622
|
+
opacity: "{{disabled ? '0.5' : '1'}}",
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
const props = { disabled: true };
|
|
627
|
+
|
|
628
|
+
const result = processStructure(structure, createContext(props));
|
|
629
|
+
const node = result as ComponentNode;
|
|
630
|
+
const baseStyle = (node.style as any)?.base;
|
|
631
|
+
expect(baseStyle?.opacity).toBe("{{disabled ? '0.5' : '1'}}");
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('should evaluate templates using global template context variables', () => {
|
|
591
635
|
setGlobalTemplateContext({ isEditorMode: true });
|
|
592
636
|
|
|
593
637
|
const structure: ComponentNode = {
|
|
594
|
-
type:
|
|
595
|
-
tag:
|
|
638
|
+
type: 'node',
|
|
639
|
+
tag: 'div',
|
|
596
640
|
style: {
|
|
597
641
|
base: {
|
|
598
|
-
display: "{{isEditorMode ? 'none' : 'block'}}"
|
|
599
|
-
}
|
|
600
|
-
}
|
|
642
|
+
display: "{{isEditorMode ? 'none' : 'block'}}",
|
|
643
|
+
},
|
|
644
|
+
},
|
|
601
645
|
};
|
|
602
646
|
|
|
603
647
|
const result = processStructure(structure, createContext({}));
|
|
604
648
|
const node = result as ComponentNode;
|
|
605
649
|
const baseStyle = (node.style as any)?.base;
|
|
606
|
-
expect(baseStyle?.display).toBe(
|
|
650
|
+
expect(baseStyle?.display).toBe('none');
|
|
607
651
|
|
|
608
652
|
resetGlobalTemplateContext();
|
|
609
653
|
});
|
|
610
654
|
});
|
|
611
655
|
|
|
612
|
-
describe(
|
|
613
|
-
test(
|
|
656
|
+
describe('Edge cases and error handling', () => {
|
|
657
|
+
test('should return null for null structure', () => {
|
|
614
658
|
const result = processStructure(null, createContext({}));
|
|
615
659
|
expect(result).toBeNull();
|
|
616
660
|
});
|
|
617
661
|
|
|
618
|
-
test(
|
|
662
|
+
test('should return null for undefined structure', () => {
|
|
619
663
|
const result = processStructure(undefined, createContext({}));
|
|
620
664
|
expect(result).toBeNull();
|
|
621
665
|
});
|
|
622
666
|
|
|
623
|
-
test(
|
|
667
|
+
test('should handle numbers in structure', () => {
|
|
624
668
|
const result = processStructure(42, createContext({}));
|
|
625
669
|
expect(result).toBe(42);
|
|
626
670
|
});
|
|
627
671
|
|
|
628
|
-
test(
|
|
672
|
+
test('should handle empty objects', () => {
|
|
629
673
|
const structure: ComponentNode = {
|
|
630
|
-
type:
|
|
631
|
-
tag:
|
|
632
|
-
children: []
|
|
674
|
+
type: 'node',
|
|
675
|
+
tag: 'div',
|
|
676
|
+
children: [],
|
|
633
677
|
};
|
|
634
678
|
|
|
635
679
|
const result = processStructure(structure, createContext({}));
|
|
636
680
|
const node = result as ComponentNode;
|
|
637
|
-
expect(node.tag).toBe(
|
|
638
|
-
expect(node.type).toBe(
|
|
681
|
+
expect(node.tag).toBe('div');
|
|
682
|
+
expect(node.type).toBe('node');
|
|
639
683
|
});
|
|
640
684
|
|
|
641
|
-
test(
|
|
685
|
+
test('should handle component definition context in templates', () => {
|
|
642
686
|
const componentDef = {
|
|
643
687
|
interface: {},
|
|
644
688
|
structure: { type: 'node' as const, tag: 'div' },
|
|
689
|
+
category: 'buttons',
|
|
645
690
|
variants: {
|
|
646
|
-
primary: { background:
|
|
647
|
-
}
|
|
691
|
+
primary: { background: 'blue' },
|
|
692
|
+
},
|
|
648
693
|
} as unknown as StructuredComponentDefinition & { variants?: Record<string, any> };
|
|
649
|
-
const structure = "{{variants.primary.background}}";
|
|
650
694
|
const props = {};
|
|
651
695
|
|
|
652
|
-
|
|
653
|
-
|
|
696
|
+
// componentDef fields are merged into the eval context, so a non-dotted
|
|
697
|
+
// full template referencing one resolves eagerly.
|
|
698
|
+
expect(processStructure('{{category}}', createContext(props, componentDef))).toBe('buttons');
|
|
699
|
+
|
|
700
|
+
// Dotted paths like {{variants.primary.background}} are NO LONGER eagerly
|
|
701
|
+
// resolved: since 2bd781ed, hasItemTemplates() reserves `\w+.\w+` paths
|
|
702
|
+
// for item-template processing (e.g. {{post.title}} inside lists), so
|
|
703
|
+
// processStructure passes them through unresolved. Previously this
|
|
704
|
+
// returned "blue".
|
|
705
|
+
expect(processStructure('{{variants.primary.background}}', createContext(props, componentDef))).toBe(
|
|
706
|
+
'{{variants.primary.background}}',
|
|
707
|
+
);
|
|
654
708
|
});
|
|
655
709
|
});
|
|
656
710
|
|
|
657
|
-
describe(
|
|
658
|
-
test(
|
|
711
|
+
describe('Integration with Button.json example', () => {
|
|
712
|
+
test('should process Button component structure with style mappings', () => {
|
|
659
713
|
// Based on components/Button.json
|
|
660
714
|
const structure: ComponentNode = {
|
|
661
|
-
type:
|
|
662
|
-
tag:
|
|
715
|
+
type: 'node',
|
|
716
|
+
tag: 'button',
|
|
663
717
|
style: {
|
|
664
718
|
base: {
|
|
665
719
|
padding: {
|
|
666
720
|
_mapping: true,
|
|
667
|
-
prop:
|
|
721
|
+
prop: 'variant',
|
|
668
722
|
values: {
|
|
669
|
-
primary:
|
|
670
|
-
secondary:
|
|
671
|
-
}
|
|
723
|
+
primary: '16px 34px',
|
|
724
|
+
secondary: '16px 34px',
|
|
725
|
+
},
|
|
672
726
|
} as StyleMapping,
|
|
673
727
|
background: {
|
|
674
728
|
_mapping: true,
|
|
675
|
-
prop:
|
|
729
|
+
prop: 'variant',
|
|
676
730
|
values: {
|
|
677
|
-
primary:
|
|
678
|
-
secondary:
|
|
679
|
-
}
|
|
731
|
+
primary: 'blue',
|
|
732
|
+
secondary: '#f3f4f6',
|
|
733
|
+
},
|
|
680
734
|
} as StyleMapping,
|
|
681
735
|
color: {
|
|
682
736
|
_mapping: true,
|
|
683
|
-
prop:
|
|
737
|
+
prop: 'variant',
|
|
684
738
|
values: {
|
|
685
|
-
primary:
|
|
686
|
-
secondary:
|
|
687
|
-
}
|
|
739
|
+
primary: 'white',
|
|
740
|
+
secondary: '#374151',
|
|
741
|
+
},
|
|
688
742
|
} as StyleMapping,
|
|
689
|
-
borderRadius:
|
|
690
|
-
fontSize:
|
|
691
|
-
}
|
|
743
|
+
borderRadius: '0px',
|
|
744
|
+
fontSize: '15px',
|
|
745
|
+
},
|
|
692
746
|
},
|
|
693
|
-
children:
|
|
747
|
+
children: '{{children}}',
|
|
694
748
|
} as unknown as ComponentNode;
|
|
695
|
-
const props = { variant:
|
|
749
|
+
const props = { variant: 'primary', children: 'Click me' };
|
|
696
750
|
|
|
697
751
|
const result = processStructure(structure, createContext(props));
|
|
698
752
|
const node = result as ComponentNode;
|
|
699
753
|
|
|
700
754
|
const baseStyle = (node.style as any)?.base;
|
|
701
|
-
expect(baseStyle?.background).toBe(
|
|
702
|
-
expect(baseStyle?.color).toBe(
|
|
703
|
-
expect(baseStyle?.padding).toBe(
|
|
755
|
+
expect(baseStyle?.background).toBe('blue');
|
|
756
|
+
expect(baseStyle?.color).toBe('white');
|
|
757
|
+
expect(baseStyle?.padding).toBe('16px 34px');
|
|
704
758
|
// processStructure converts string children template to evaluated result (array)
|
|
705
759
|
if (Array.isArray(node.children)) {
|
|
706
|
-
expect(node.children).toEqual([
|
|
760
|
+
expect(node.children).toEqual(['Click me']);
|
|
707
761
|
} else {
|
|
708
762
|
// If not array, it might still be the template string or evaluated string
|
|
709
763
|
expect(node.children).toBeDefined();
|
|
@@ -712,241 +766,253 @@ describe("Template Engine - processStructure", () => {
|
|
|
712
766
|
});
|
|
713
767
|
});
|
|
714
768
|
|
|
715
|
-
describe(
|
|
769
|
+
describe('Template Engine - parentProps cascade', () => {
|
|
716
770
|
const mockComponentDef: StructuredComponentDefinition = {
|
|
717
771
|
interface: {},
|
|
718
|
-
structure: { type: 'node' as const, tag: 'div' }
|
|
772
|
+
structure: { type: 'node' as const, tag: 'div' },
|
|
719
773
|
};
|
|
720
774
|
|
|
721
775
|
test("falls back to parentProps when child doesn't declare the prop", () => {
|
|
722
776
|
const structure: ComponentNode = {
|
|
723
|
-
type:
|
|
724
|
-
tag:
|
|
725
|
-
children: [
|
|
777
|
+
type: 'node',
|
|
778
|
+
tag: 'span',
|
|
779
|
+
children: ['{{ctaText}}'],
|
|
726
780
|
};
|
|
727
781
|
const ctx: TemplateContext = {
|
|
728
782
|
props: {},
|
|
729
|
-
parentProps: { ctaText:
|
|
730
|
-
componentDef: mockComponentDef
|
|
783
|
+
parentProps: { ctaText: 'Take the assessment' },
|
|
784
|
+
componentDef: mockComponentDef,
|
|
731
785
|
};
|
|
732
786
|
|
|
733
787
|
const result = processStructure(structure, ctx) as ComponentNode;
|
|
734
|
-
expect(result.children).toEqual([
|
|
788
|
+
expect(result.children).toEqual(['Take the assessment']);
|
|
735
789
|
});
|
|
736
790
|
|
|
737
|
-
test(
|
|
791
|
+
test('child props win over parentProps on key collision', () => {
|
|
738
792
|
const structure: ComponentNode = {
|
|
739
|
-
type:
|
|
740
|
-
tag:
|
|
741
|
-
children: [
|
|
793
|
+
type: 'node',
|
|
794
|
+
tag: 'span',
|
|
795
|
+
children: ['{{label}}'],
|
|
742
796
|
};
|
|
743
797
|
const ctx: TemplateContext = {
|
|
744
|
-
props: { label:
|
|
745
|
-
parentProps: { label:
|
|
746
|
-
componentDef: mockComponentDef
|
|
798
|
+
props: { label: 'child' },
|
|
799
|
+
parentProps: { label: 'parent' },
|
|
800
|
+
componentDef: mockComponentDef,
|
|
747
801
|
};
|
|
748
802
|
|
|
749
803
|
const result = processStructure(structure, ctx) as ComponentNode;
|
|
750
|
-
expect(result.children).toEqual([
|
|
804
|
+
expect(result.children).toEqual(['child']);
|
|
751
805
|
});
|
|
752
806
|
|
|
753
|
-
test(
|
|
807
|
+
test('partial templates (prefix-{{x}}) also cascade', () => {
|
|
754
808
|
const structure: ComponentNode = {
|
|
755
|
-
type:
|
|
756
|
-
tag:
|
|
757
|
-
attributes: { class:
|
|
809
|
+
type: 'node',
|
|
810
|
+
tag: 'div',
|
|
811
|
+
attributes: { class: 'btn-{{variant}}' },
|
|
758
812
|
};
|
|
759
813
|
const ctx: TemplateContext = {
|
|
760
814
|
props: {},
|
|
761
|
-
parentProps: { variant:
|
|
762
|
-
componentDef: mockComponentDef
|
|
815
|
+
parentProps: { variant: 'primary' },
|
|
816
|
+
componentDef: mockComponentDef,
|
|
763
817
|
};
|
|
764
818
|
|
|
765
819
|
const result = processStructure(structure, ctx) as ComponentNode;
|
|
766
|
-
expect((result as any).attributes?.class).toBe(
|
|
820
|
+
expect((result as any).attributes?.class).toBe('btn-primary');
|
|
767
821
|
});
|
|
768
822
|
|
|
769
|
-
test(
|
|
823
|
+
test('style mappings cascade against parentProps', () => {
|
|
770
824
|
const structure: ComponentNode = {
|
|
771
|
-
type:
|
|
772
|
-
tag:
|
|
825
|
+
type: 'node',
|
|
826
|
+
tag: 'div',
|
|
773
827
|
style: {
|
|
774
828
|
base: {
|
|
775
829
|
color: {
|
|
776
830
|
_mapping: true,
|
|
777
|
-
prop:
|
|
778
|
-
values: { default:
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
831
|
+
prop: 'variant',
|
|
832
|
+
values: { default: 'black', featured: 'white' },
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
},
|
|
782
836
|
} as ComponentNode;
|
|
783
837
|
const ctx: TemplateContext = {
|
|
784
838
|
props: {},
|
|
785
|
-
parentProps: { variant:
|
|
786
|
-
componentDef: mockComponentDef
|
|
839
|
+
parentProps: { variant: 'featured' },
|
|
840
|
+
componentDef: mockComponentDef,
|
|
787
841
|
};
|
|
788
842
|
|
|
789
843
|
const result = processStructure(structure, ctx) as ComponentNode;
|
|
790
844
|
const baseStyle = (result.style as any)?.base;
|
|
791
|
-
expect(baseStyle?.color).toBe(
|
|
845
|
+
expect(baseStyle?.color).toBe('white');
|
|
792
846
|
});
|
|
793
847
|
});
|
|
794
848
|
|
|
795
|
-
describe(
|
|
849
|
+
describe('Template Engine - Slot Default Values', () => {
|
|
796
850
|
const mockComponentDef: StructuredComponentDefinition = {
|
|
797
851
|
interface: {},
|
|
798
|
-
structure: { type: 'node' as const, tag: 'div' }
|
|
852
|
+
structure: { type: 'node' as const, tag: 'div' },
|
|
799
853
|
};
|
|
800
854
|
|
|
801
855
|
const createContext = (props: Record<string, unknown>): TemplateContext => ({
|
|
802
856
|
props,
|
|
803
|
-
componentDef: mockComponentDef
|
|
857
|
+
componentDef: mockComponentDef,
|
|
804
858
|
});
|
|
805
859
|
|
|
806
|
-
test(
|
|
860
|
+
test('should render slot default when no instance children provided', () => {
|
|
807
861
|
const structure = [
|
|
808
|
-
{
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
862
|
+
{
|
|
863
|
+
type: 'node',
|
|
864
|
+
tag: 'div',
|
|
865
|
+
children: [
|
|
866
|
+
{ type: 'node', tag: 'h1', children: 'Header' },
|
|
867
|
+
{
|
|
868
|
+
type: 'slot',
|
|
869
|
+
default: [{ type: 'node', tag: 'p', children: 'Default paragraph' }],
|
|
870
|
+
},
|
|
871
|
+
{ type: 'node', tag: 'footer', children: 'Footer' },
|
|
872
|
+
],
|
|
873
|
+
},
|
|
818
874
|
] as unknown as ComponentNode[];
|
|
819
875
|
|
|
820
876
|
const result = processStructure(structure, createContext({}), undefined, undefined);
|
|
821
877
|
expect(result).toBeInstanceOf(Array);
|
|
822
878
|
|
|
823
879
|
const [root] = result as ComponentNode[];
|
|
824
|
-
expect(root
|
|
825
|
-
expect(root
|
|
880
|
+
expect(root!.tag).toBe('div');
|
|
881
|
+
expect(root!.children).toBeInstanceOf(Array);
|
|
826
882
|
|
|
827
|
-
const children = root
|
|
883
|
+
const children = root!.children as ComponentNode[];
|
|
828
884
|
expect(children).toHaveLength(3);
|
|
829
|
-
expect(children[0]
|
|
830
|
-
expect((children[1] as any).tag).toBe(
|
|
831
|
-
expect((children[1] as any).children).toEqual([
|
|
832
|
-
expect(children[2]
|
|
885
|
+
expect(children[0]!.tag).toBe('h1');
|
|
886
|
+
expect((children[1] as any).tag).toBe('p'); // Default content
|
|
887
|
+
expect((children[1] as any).children).toEqual(['Default paragraph']);
|
|
888
|
+
expect(children[2]!.tag).toBe('footer');
|
|
833
889
|
});
|
|
834
890
|
|
|
835
|
-
test(
|
|
891
|
+
test('should render instance children instead of slot default when provided', () => {
|
|
836
892
|
const structure = [
|
|
837
|
-
{
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
893
|
+
{
|
|
894
|
+
type: 'node',
|
|
895
|
+
tag: 'div',
|
|
896
|
+
children: [
|
|
897
|
+
{ type: 'node', tag: 'h1', children: 'Header' },
|
|
898
|
+
{
|
|
899
|
+
type: 'slot',
|
|
900
|
+
default: [{ type: 'node', tag: 'p', children: 'Default paragraph' }],
|
|
901
|
+
},
|
|
902
|
+
{ type: 'node', tag: 'footer', children: 'Footer' },
|
|
903
|
+
],
|
|
904
|
+
},
|
|
847
905
|
] as unknown as ComponentNode[];
|
|
848
906
|
|
|
849
|
-
const instanceChildren = [
|
|
850
|
-
{ type: "node", tag: "span", children: "Custom content" }
|
|
851
|
-
] as ComponentNode[];
|
|
907
|
+
const instanceChildren = [{ type: 'node', tag: 'span', children: 'Custom content' }] as ComponentNode[];
|
|
852
908
|
|
|
853
909
|
const result = processStructure(structure, createContext({}), undefined, instanceChildren);
|
|
854
910
|
expect(result).toBeInstanceOf(Array);
|
|
855
911
|
|
|
856
912
|
const [root] = result as ComponentNode[];
|
|
857
|
-
const children = root
|
|
913
|
+
const children = root!.children as ComponentNode[];
|
|
858
914
|
|
|
859
915
|
expect(children).toHaveLength(3);
|
|
860
|
-
expect(children[0]
|
|
861
|
-
expect((children[1] as any).tag).toBe(
|
|
862
|
-
expect((children[1] as any).children).toEqual([
|
|
863
|
-
expect(children[2]
|
|
916
|
+
expect(children[0]!.tag).toBe('h1');
|
|
917
|
+
expect((children[1] as any).tag).toBe('span'); // Instance content, not default
|
|
918
|
+
expect((children[1] as any).children).toEqual(['Custom content']);
|
|
919
|
+
expect(children[2]!.tag).toBe('footer');
|
|
864
920
|
});
|
|
865
921
|
|
|
866
|
-
test(
|
|
922
|
+
test('should handle string default for slot', () => {
|
|
867
923
|
const structure = [
|
|
868
|
-
{
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
924
|
+
{
|
|
925
|
+
type: 'node',
|
|
926
|
+
tag: 'div',
|
|
927
|
+
children: [
|
|
928
|
+
{
|
|
929
|
+
type: 'slot',
|
|
930
|
+
default: 'Default text content',
|
|
931
|
+
},
|
|
932
|
+
],
|
|
933
|
+
},
|
|
874
934
|
] as unknown as ComponentNode[];
|
|
875
935
|
|
|
876
936
|
const result = processStructure(structure, createContext({}), undefined, undefined);
|
|
877
937
|
const [root] = result as ComponentNode[];
|
|
878
|
-
const children = root
|
|
938
|
+
const children = root!.children as string[];
|
|
879
939
|
|
|
880
|
-
expect(children).toContain(
|
|
940
|
+
expect(children).toContain('Default text content');
|
|
881
941
|
});
|
|
882
942
|
|
|
883
|
-
test(
|
|
943
|
+
test('should render nothing when no instance children and no default', () => {
|
|
884
944
|
const structure = [
|
|
885
|
-
{
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
945
|
+
{
|
|
946
|
+
type: 'node',
|
|
947
|
+
tag: 'div',
|
|
948
|
+
children: [
|
|
949
|
+
{ type: 'node', tag: 'h1', children: 'Header' },
|
|
950
|
+
{ type: 'slot' }, // No default
|
|
951
|
+
{ type: 'node', tag: 'footer', children: 'Footer' },
|
|
952
|
+
],
|
|
953
|
+
},
|
|
890
954
|
] as unknown as ComponentNode[];
|
|
891
955
|
|
|
892
956
|
const result = processStructure(structure, createContext({}), undefined, undefined);
|
|
893
957
|
const [root] = result as ComponentNode[];
|
|
894
|
-
const children = root
|
|
958
|
+
const children = root!.children as ComponentNode[];
|
|
895
959
|
|
|
896
960
|
expect(children).toHaveLength(2); // Only h1 and footer, slot is removed
|
|
897
|
-
expect(children[0]
|
|
898
|
-
expect(children[1]
|
|
961
|
+
expect(children[0]!.tag).toBe('h1');
|
|
962
|
+
expect(children[1]!.tag).toBe('footer');
|
|
899
963
|
});
|
|
900
964
|
|
|
901
|
-
test(
|
|
965
|
+
test('should process templates in slot default content', () => {
|
|
902
966
|
const structure = [
|
|
903
|
-
{
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
967
|
+
{
|
|
968
|
+
type: 'node',
|
|
969
|
+
tag: 'div',
|
|
970
|
+
children: [
|
|
971
|
+
{
|
|
972
|
+
type: 'slot',
|
|
973
|
+
default: [{ type: 'node', tag: 'p', children: 'Hello {{name}}' }],
|
|
974
|
+
},
|
|
975
|
+
],
|
|
976
|
+
},
|
|
911
977
|
] as unknown as ComponentNode[];
|
|
912
978
|
|
|
913
|
-
const result = processStructure(structure, createContext({ name:
|
|
979
|
+
const result = processStructure(structure, createContext({ name: 'World' }), undefined, undefined);
|
|
914
980
|
const [root] = result as ComponentNode[];
|
|
915
|
-
const children = root
|
|
981
|
+
const children = root!.children as ComponentNode[];
|
|
916
982
|
|
|
917
|
-
expect((children[0] as any).children).toEqual([
|
|
983
|
+
expect((children[0] as any).children).toEqual(['Hello World']);
|
|
918
984
|
});
|
|
919
985
|
});
|
|
920
986
|
|
|
921
|
-
describe(
|
|
922
|
-
test(
|
|
987
|
+
describe('Template Engine - normalizeStyle', () => {
|
|
988
|
+
test('should normalize flat style objects', () => {
|
|
923
989
|
const style = {
|
|
924
|
-
color:
|
|
925
|
-
fontSize:
|
|
990
|
+
color: 'red',
|
|
991
|
+
fontSize: '16px',
|
|
926
992
|
};
|
|
927
|
-
|
|
993
|
+
|
|
928
994
|
const result = normalizeStyle(style);
|
|
929
995
|
expect(result).toEqual(style);
|
|
930
996
|
});
|
|
931
997
|
|
|
932
998
|
test("should normalize responsive style objects using 'all' strategy", () => {
|
|
933
999
|
const style = {
|
|
934
|
-
base: { color:
|
|
935
|
-
tablet: { color:
|
|
936
|
-
mobile: { color:
|
|
1000
|
+
base: { color: 'red' },
|
|
1001
|
+
tablet: { color: 'blue' },
|
|
1002
|
+
mobile: { color: 'green' },
|
|
937
1003
|
};
|
|
938
|
-
|
|
1004
|
+
|
|
939
1005
|
const result = normalizeStyle(style);
|
|
940
1006
|
expect(result).toBeDefined();
|
|
941
|
-
expect(result?.color).toBe(
|
|
1007
|
+
expect(result?.color).toBe('green'); // 'all' strategy merges all, mobile wins
|
|
942
1008
|
});
|
|
943
1009
|
|
|
944
|
-
test(
|
|
1010
|
+
test('should return null for null style', () => {
|
|
945
1011
|
const result = normalizeStyle(null);
|
|
946
1012
|
expect(result).toBeNull();
|
|
947
1013
|
});
|
|
948
1014
|
|
|
949
|
-
test(
|
|
1015
|
+
test('should return null for undefined style', () => {
|
|
950
1016
|
const result = normalizeStyle(undefined);
|
|
951
1017
|
expect(result).toBeNull();
|
|
952
1018
|
});
|
|
@@ -957,175 +1023,175 @@ describe("Template Engine - normalizeStyle", () => {
|
|
|
957
1023
|
// These exercise specific type-cast paths to ensure safety before refactoring
|
|
958
1024
|
// ==========================================================================
|
|
959
1025
|
|
|
960
|
-
describe(
|
|
1026
|
+
describe('Template Engine - processStructure as-any paths', () => {
|
|
961
1027
|
const baseContext: TemplateContext = {
|
|
962
|
-
props: { text:
|
|
1028
|
+
props: { text: 'Hello', size: 'lg', variant: 'primary' },
|
|
963
1029
|
componentDef: {} as StructuredComponentDefinition,
|
|
964
1030
|
};
|
|
965
1031
|
|
|
966
|
-
describe(
|
|
967
|
-
test(
|
|
1032
|
+
describe('Boolean preservation (line 429)', () => {
|
|
1033
|
+
test('should preserve false boolean values in structure', () => {
|
|
968
1034
|
// processStructure should return false as-is, not convert to null
|
|
969
1035
|
const result = processStructure(false as unknown as ComponentNode, baseContext);
|
|
970
1036
|
expect(result as unknown).toBe(false);
|
|
971
1037
|
});
|
|
972
1038
|
|
|
973
|
-
test(
|
|
1039
|
+
test('should preserve true boolean values in structure', () => {
|
|
974
1040
|
const result = processStructure(true as unknown as ComponentNode, baseContext);
|
|
975
1041
|
expect(result as unknown).toBe(true);
|
|
976
1042
|
});
|
|
977
1043
|
});
|
|
978
1044
|
|
|
979
|
-
describe(
|
|
980
|
-
test(
|
|
1045
|
+
describe('Object template evaluation returning objects (line 454)', () => {
|
|
1046
|
+
test('should return object result from full template expression', () => {
|
|
981
1047
|
const context: TemplateContext = {
|
|
982
|
-
props: { link: { href:
|
|
1048
|
+
props: { link: { href: '/about', target: '_blank' } },
|
|
983
1049
|
componentDef: {} as StructuredComponentDefinition,
|
|
984
1050
|
};
|
|
985
1051
|
// Full template {{link}} should return the object as-is
|
|
986
|
-
const result = processStructure(
|
|
987
|
-
expect(result).toEqual({ href:
|
|
1052
|
+
const result = processStructure('{{link}}', context);
|
|
1053
|
+
expect(result).toEqual({ href: '/about', target: '_blank' });
|
|
988
1054
|
});
|
|
989
1055
|
});
|
|
990
1056
|
|
|
991
1057
|
describe("Slot default content via 'default' property (lines 483-486, 516-517)", () => {
|
|
992
|
-
test(
|
|
1058
|
+
test('should render slot default array when no instance children', () => {
|
|
993
1059
|
// Slot markers use type: "slot" (NODE_TYPE.SLOT)
|
|
994
1060
|
const structure = [
|
|
995
|
-
{
|
|
996
|
-
|
|
997
|
-
|
|
1061
|
+
{
|
|
1062
|
+
type: 'node',
|
|
1063
|
+
tag: 'div',
|
|
1064
|
+
children: [{ type: 'slot', default: [{ type: 'node', tag: 'span', children: 'fallback' }] }],
|
|
1065
|
+
},
|
|
998
1066
|
] as unknown as ComponentNode[];
|
|
999
1067
|
const result = processStructure(structure, baseContext);
|
|
1000
1068
|
expect(Array.isArray(result)).toBe(true);
|
|
1001
1069
|
const root = (result as any[])[0];
|
|
1002
|
-
expect(root.tag).toBe(
|
|
1070
|
+
expect(root.tag).toBe('div');
|
|
1003
1071
|
// The slot default should have been rendered
|
|
1004
1072
|
expect(root.children.length).toBe(1);
|
|
1005
|
-
expect(root.children[0].tag).toBe(
|
|
1073
|
+
expect(root.children[0].tag).toBe('span');
|
|
1006
1074
|
});
|
|
1007
1075
|
|
|
1008
|
-
test(
|
|
1076
|
+
test('should render slot default string when no instance children', () => {
|
|
1009
1077
|
const structure = [
|
|
1010
|
-
{ type:
|
|
1011
|
-
{ type: "slot", default: "Default text" }
|
|
1012
|
-
]}
|
|
1078
|
+
{ type: 'node', tag: 'div', children: [{ type: 'slot', default: 'Default text' }] },
|
|
1013
1079
|
] as unknown as ComponentNode[];
|
|
1014
1080
|
const result = processStructure(structure, baseContext);
|
|
1015
1081
|
const root = (result as any[])[0];
|
|
1016
|
-
expect(root.children[0]).toBe(
|
|
1082
|
+
expect(root.children[0]).toBe('Default text');
|
|
1017
1083
|
});
|
|
1018
1084
|
|
|
1019
|
-
test(
|
|
1085
|
+
test('should use instance children over slot default', () => {
|
|
1020
1086
|
const structure = [
|
|
1021
|
-
{
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
{ type: "node", tag: "b", children: "override" }
|
|
1087
|
+
{
|
|
1088
|
+
type: 'node',
|
|
1089
|
+
tag: 'div',
|
|
1090
|
+
children: [{ type: 'slot', default: [{ type: 'node', tag: 'span', children: 'fallback' }] }],
|
|
1091
|
+
},
|
|
1027
1092
|
] as unknown as ComponentNode[];
|
|
1093
|
+
const instanceChildren = [{ type: 'node', tag: 'b', children: 'override' }] as unknown as ComponentNode[];
|
|
1028
1094
|
const result = processStructure(structure, baseContext, undefined, instanceChildren);
|
|
1029
1095
|
const root = (result as any[])[0];
|
|
1030
|
-
expect(root.children[0].tag).toBe(
|
|
1096
|
+
expect(root.children[0].tag).toBe('b');
|
|
1031
1097
|
});
|
|
1032
1098
|
|
|
1033
|
-
test(
|
|
1099
|
+
test('should handle slot marker as standalone object with default', () => {
|
|
1034
1100
|
const slotMarker = {
|
|
1035
|
-
type:
|
|
1036
|
-
default: [{ type:
|
|
1101
|
+
type: 'slot',
|
|
1102
|
+
default: [{ type: 'node', tag: 'div', children: 'content' }],
|
|
1037
1103
|
};
|
|
1038
1104
|
const result = processStructure(slotMarker as any, baseContext);
|
|
1039
1105
|
expect(result).toBeDefined();
|
|
1040
1106
|
});
|
|
1041
1107
|
});
|
|
1042
1108
|
|
|
1043
|
-
describe(
|
|
1044
|
-
test(
|
|
1109
|
+
describe('Plain object processing (lines 538, 543)', () => {
|
|
1110
|
+
test('should process plain objects recursively and resolve templates', () => {
|
|
1045
1111
|
// An object without a valid node type gets treated as a plain props object
|
|
1046
|
-
const structure = { label:
|
|
1112
|
+
const structure = { label: '{{text}}', visible: true } as any;
|
|
1047
1113
|
const result = processStructure(structure, baseContext);
|
|
1048
1114
|
expect(result).toBeDefined();
|
|
1049
|
-
expect((result as any).label).toBe(
|
|
1115
|
+
expect((result as any).label).toBe('Hello');
|
|
1050
1116
|
expect((result as any).visible).toBe(true);
|
|
1051
1117
|
});
|
|
1052
1118
|
|
|
1053
|
-
test(
|
|
1119
|
+
test('should strip null/undefined values from plain objects', () => {
|
|
1054
1120
|
const context: TemplateContext = {
|
|
1055
|
-
props: { defined:
|
|
1121
|
+
props: { defined: 'value' },
|
|
1056
1122
|
componentDef: {} as StructuredComponentDefinition,
|
|
1057
1123
|
};
|
|
1058
|
-
const structure = { a:
|
|
1124
|
+
const structure = { a: '{{defined}}', b: '{{missing}}' } as any;
|
|
1059
1125
|
const result = processStructure(structure, context);
|
|
1060
|
-
expect((result as any).a).toBe(
|
|
1126
|
+
expect((result as any).a).toBe('value');
|
|
1061
1127
|
// Missing props resolve to empty string, not null
|
|
1062
1128
|
});
|
|
1063
1129
|
});
|
|
1064
1130
|
|
|
1065
|
-
describe(
|
|
1066
|
-
test(
|
|
1131
|
+
describe('Embed node creation (line 562)', () => {
|
|
1132
|
+
test('should process embed node with html content', () => {
|
|
1067
1133
|
const node: ComponentNode = {
|
|
1068
|
-
type:
|
|
1134
|
+
type: 'embed' as any,
|
|
1069
1135
|
html: "<script>alert('test')</script>",
|
|
1070
1136
|
} as any;
|
|
1071
1137
|
const result = processStructure(node, baseContext);
|
|
1072
1138
|
expect(result).toBeDefined();
|
|
1073
|
-
expect((result as any).type).toBe(
|
|
1139
|
+
expect((result as any).type).toBe('embed');
|
|
1074
1140
|
expect((result as any).html).toBe("<script>alert('test')</script>");
|
|
1075
1141
|
});
|
|
1076
1142
|
});
|
|
1077
1143
|
|
|
1078
|
-
describe(
|
|
1079
|
-
test(
|
|
1144
|
+
describe('Link node creation (line 569)', () => {
|
|
1145
|
+
test('should process link node with href', () => {
|
|
1080
1146
|
const node = {
|
|
1081
|
-
type:
|
|
1082
|
-
href:
|
|
1083
|
-
children: [
|
|
1147
|
+
type: 'link',
|
|
1148
|
+
href: '/about',
|
|
1149
|
+
children: ['About us'],
|
|
1084
1150
|
} as any;
|
|
1085
1151
|
const result = processStructure(node, baseContext);
|
|
1086
1152
|
expect(result).toBeDefined();
|
|
1087
|
-
expect((result as any).type).toBe(
|
|
1153
|
+
expect((result as any).type).toBe('link');
|
|
1088
1154
|
});
|
|
1089
1155
|
|
|
1090
|
-
test(
|
|
1156
|
+
test('should process link node with template href', () => {
|
|
1091
1157
|
const context: TemplateContext = {
|
|
1092
|
-
props: { url:
|
|
1158
|
+
props: { url: '/contact' },
|
|
1093
1159
|
componentDef: {} as StructuredComponentDefinition,
|
|
1094
1160
|
};
|
|
1095
1161
|
const node = {
|
|
1096
|
-
type:
|
|
1097
|
-
href:
|
|
1098
|
-
children: [
|
|
1162
|
+
type: 'link',
|
|
1163
|
+
href: '{{url}}',
|
|
1164
|
+
children: ['Contact'],
|
|
1099
1165
|
} as any;
|
|
1100
1166
|
const result = processStructure(node, context);
|
|
1101
1167
|
expect(result).toBeDefined();
|
|
1102
|
-
expect((result as any).type).toBe(
|
|
1168
|
+
expect((result as any).type).toBe('link');
|
|
1103
1169
|
});
|
|
1104
1170
|
});
|
|
1105
1171
|
|
|
1106
|
-
describe(
|
|
1107
|
-
test(
|
|
1172
|
+
describe('Locale-list node creation (line 574)', () => {
|
|
1173
|
+
test('should process locale-list node', () => {
|
|
1108
1174
|
const node = {
|
|
1109
|
-
type:
|
|
1110
|
-
style: { display:
|
|
1175
|
+
type: 'locale-list',
|
|
1176
|
+
style: { display: 'flex' },
|
|
1111
1177
|
} as any;
|
|
1112
1178
|
const result = processStructure(node, baseContext);
|
|
1113
1179
|
expect(result).toBeDefined();
|
|
1114
|
-
expect((result as any).type).toBe(
|
|
1180
|
+
expect((result as any).type).toBe('locale-list');
|
|
1115
1181
|
});
|
|
1116
1182
|
});
|
|
1117
1183
|
|
|
1118
|
-
describe(
|
|
1119
|
-
test(
|
|
1184
|
+
describe('List node with legacy cms-list type (line 575)', () => {
|
|
1185
|
+
test('should process list node', () => {
|
|
1120
1186
|
const node = {
|
|
1121
|
-
type:
|
|
1122
|
-
source:
|
|
1123
|
-
sourceType:
|
|
1124
|
-
children: [{ type:
|
|
1187
|
+
type: 'list',
|
|
1188
|
+
source: 'items',
|
|
1189
|
+
sourceType: 'prop',
|
|
1190
|
+
children: [{ type: 'html' as const, tag: 'div', children: [] }],
|
|
1125
1191
|
} as any;
|
|
1126
1192
|
const result = processStructure(node, baseContext);
|
|
1127
1193
|
expect(result).toBeDefined();
|
|
1128
|
-
expect((result as any).type).toBe(
|
|
1194
|
+
expect((result as any).type).toBe('list');
|
|
1129
1195
|
});
|
|
1130
1196
|
});
|
|
1131
1197
|
});
|