From c007533018c9703f2f9358c0168674123659f740 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Wed, 6 Nov 2024 14:11:06 +0100 Subject: [PATCH] wip render processed components --- package-lock.json | 10 + package.json | 1 + src/ComponentRegistry/XpComponent.tsx | 136 +++++++ src/ComponentRegistry/XpComponentComment.tsx | 14 + src/ComponentRegistry/XpLayout.tsx | 32 ++ src/ComponentRegistry/XpPage.tsx | 50 +++ .../{BasePart.tsx => XpPart.tsx} | 26 +- src/ComponentRegistry/XpRegion.tsx | 42 ++ src/ComponentRegistry/XpRegions.tsx | 22 ++ src/Region.tsx | 10 +- src/Regions.tsx | 3 - src/XpComponent.tsx | 76 ---- src/index.ts | 2 +- src/processComponents.ts | 264 +++++++++++-- src/replaceMacroComments.ts | 89 +++-- src/types/index.ts | 63 ++- test/ComponentRegistry/BasePart.test.tsx | 121 +++--- test/ComponentRegistry/DefaultPage.tsx | 18 + test/ComponentRegistry/ExamplePart.tsx | 21 +- test/ComponentRegistry/TwoColumnLayout.tsx | 22 ++ test/RichText.ComponentRegistry.test.tsx | 82 ++-- test/processComponents/data.ts | 66 +++- .../processComponents.test.tsx | 366 +++++++++++++++--- test/replaceMacroComments.test.ts | 65 ++-- test/tsconfig.json | 3 + tsconfig.json | 8 +- 26 files changed, 1213 insertions(+), 399 deletions(-) create mode 100644 src/ComponentRegistry/XpComponent.tsx create mode 100644 src/ComponentRegistry/XpComponentComment.tsx create mode 100644 src/ComponentRegistry/XpLayout.tsx create mode 100644 src/ComponentRegistry/XpPage.tsx rename src/ComponentRegistry/{BasePart.tsx => XpPart.tsx} (57%) create mode 100644 src/ComponentRegistry/XpRegion.tsx create mode 100644 src/ComponentRegistry/XpRegions.tsx delete mode 100644 src/XpComponent.tsx create mode 100644 test/ComponentRegistry/DefaultPage.tsx create mode 100644 test/ComponentRegistry/TwoColumnLayout.tsx diff --git a/package-lock.json b/package-lock.json index c879cbd..e66809b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@enonic/js-utils": "^1.8.0", + "clsx": "^2.1.1", "domelementtype": "^2.3.0", "html-react-parser": "^5.1.10", "uri-js": "^4.4.1" @@ -2939,6 +2940,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", diff --git a/package.json b/package.json index 7dcf1e0..db5e3ad 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "types": "dist/index.d.ts", "dependencies": { "@enonic/js-utils": "^1.8.0", + "clsx": "^2.1.1", "domelementtype": "^2.3.0", "html-react-parser": "^5.1.10", "uri-js": "^4.4.1" diff --git a/src/ComponentRegistry/XpComponent.tsx b/src/ComponentRegistry/XpComponent.tsx new file mode 100644 index 0000000..d35401a --- /dev/null +++ b/src/ComponentRegistry/XpComponent.tsx @@ -0,0 +1,136 @@ +import type {Component} from '@enonic-types/core'; +import type {ComponentRegistry} from '../ComponentRegistry'; +// import type {MacroComponent} from './types'; +import type { + DecoratedLayoutComponent, + DecoratedPageComponent, +} from '../types'; + +import * as React from 'react'; +import {XP_COMPONENT_TYPE} from '../constants'; +import {XpComponentComment} from './XpComponentComment'; +// import {XpLayout} from './XpLayout'; +// import {XpPage} from './XpPage'; +import {XpPart} from './XpPart'; +// import {RichText} from './RichText'; +// import {Macro as FallbackMacro} from './RichText/Macro'; +// import {replaceMacroComments} from './replaceMacroComments'; + + +// interface RestProps { +// componentRegistry?: ComponentRegistry +// } + +// const Macro: MacroComponent = ({ +// componentRegistry, +// config, +// descriptor, +// children +// }) => { +// console.info('MacroWithComponentRegistry', {config, descriptor, children}); +// if (componentRegistry) { +// // @ts-ignore +// if (descriptor === 'info') { +// const InfoMacro = componentRegistry.getMacro<{ +// header: string +// text: string +// }>('macro'); +// console.info(InfoMacro); +// const { +// body, +// header, +// } = config; +// console.info('MacroWithComponentRegistry', {body, header}); +// return ( +// +// ); +// } +// } +// return null; +// } + +export function XpComponent({ + component, + componentRegistry +}: { + component: Component + componentRegistry?: ComponentRegistry +}) { + if (!componentRegistry) { + return ( + + ); + } + + const { + type + } = component; + // console.info('XpComponent type:', type); + + if (type === XP_COMPONENT_TYPE.PART) { + return ( + + ); + } + + if (type === XP_COMPONENT_TYPE.LAYOUT) { + const layoutDefinition = componentRegistry.getLayout(component.descriptor); + if (!layoutDefinition) { + throw new Error(`Layout definition not found for descriptor: ${component.descriptor}`); + } + const {View: LayoutView} = layoutDefinition; + if (!LayoutView) { + throw new Error(`Layout definition missing View for descriptor: ${component.descriptor}`); + } + const {props} = component as DecoratedLayoutComponent; + if (!props) { + throw new Error(`Layout component missing props: ${component.descriptor}`); + } + props.componentRegistry = componentRegistry; + return ( + + ); + } + + if (type === XP_COMPONENT_TYPE.PAGE) { + const pageDefinition = componentRegistry.getPage(component.descriptor); + if (!pageDefinition) { + throw new Error(`Page definition not found for descriptor: ${component.descriptor}`); + } + const {View: PageView} = pageDefinition; + if (!PageView) { + throw new Error(`Page definition missing View for descriptor: ${component.descriptor}`); + } + const {props} = component as DecoratedPageComponent; + if (!props) { + throw new Error(`Page component missing props: ${component.descriptor}`); + } + props.componentRegistry = componentRegistry; + return ( + + ); + } + + // if (type === XP_COMPONENT_TYPE.TEXT) { + // // console.info('XpComponent', {component}); + + // // const data = {}; + // const data = replaceMacroComments(component.text); + // console.info('data', data); + + // // componentRegistry={componentRegistry} + // // Macro={Macro} + // return ( + // + // data={data} + // /> + // ); + // } + + return ( + + ); +} diff --git a/src/ComponentRegistry/XpComponentComment.tsx b/src/ComponentRegistry/XpComponentComment.tsx new file mode 100644 index 0000000..11e1974 --- /dev/null +++ b/src/ComponentRegistry/XpComponentComment.tsx @@ -0,0 +1,14 @@ +import type {Component} from '@enonic-types/core'; + +export function XpComponentComment({ + component +}: { + component?: Component +}): JSX.Element | null { + if (!component || !component.path) { + return null; + } + return ( + <>{/*# COMPONENT ${component.path} */} + ); +} diff --git a/src/ComponentRegistry/XpLayout.tsx b/src/ComponentRegistry/XpLayout.tsx new file mode 100644 index 0000000..6c2d062 --- /dev/null +++ b/src/ComponentRegistry/XpLayout.tsx @@ -0,0 +1,32 @@ +import type {LayoutComponent} from '@enonic-types/core'; +import type {ClassValue} from 'clsx'; +import type {ComponentRegistry} from '../ComponentRegistry'; + +import cx from 'clsx'; +import {XpRegions} from './XpRegions'; + +export function XpLayout({ + as, + className, + component, + componentRegistry, +}: { + as?: string; + className?: ClassValue; + component: LayoutComponent; + componentRegistry: ComponentRegistry; +}) { + // console.debug('XpLayout component:', component.descriptor); + const {regions} = component; + const ElementType = (as || 'div') as keyof JSX.IntrinsicElements; + return ( + + + + ); +} diff --git a/src/ComponentRegistry/XpPage.tsx b/src/ComponentRegistry/XpPage.tsx new file mode 100644 index 0000000..cab0062 --- /dev/null +++ b/src/ComponentRegistry/XpPage.tsx @@ -0,0 +1,50 @@ +import type { + PageComponent, + PageContributions, +} from '@enonic-types/core'; +import type {ClassValue} from 'clsx'; +import type {ComponentRegistry} from '../ComponentRegistry'; + +import cx from 'clsx'; +import {XpRegions} from './XpRegions'; + +export function XpPage({ + bodyBegin, + bodyEnd, + className, + component, + componentRegistry, + headBegin, + headEnd, + title +}: { + bodyBegin?: React.ReactNode; + bodyEnd?: React.ReactNode; + className?: ClassValue + component: PageComponent + componentRegistry: ComponentRegistry + headBegin?: React.ReactNode; + headEnd?: React.ReactNode; + title?: string +}) { + const {regions} = component; + return ( + + + {headBegin ? headBegin : null} + {title ? {title} : null} + {headEnd ? headEnd : null} + + + {bodyBegin ? bodyBegin : null} + + {bodyEnd ? bodyEnd : null} + + + ); +} diff --git a/src/ComponentRegistry/BasePart.tsx b/src/ComponentRegistry/XpPart.tsx similarity index 57% rename from src/ComponentRegistry/BasePart.tsx rename to src/ComponentRegistry/XpPart.tsx index c1653bf..78e92f0 100644 --- a/src/ComponentRegistry/BasePart.tsx +++ b/src/ComponentRegistry/XpPart.tsx @@ -1,22 +1,26 @@ -import type {PartComponent} from '@enonic-types/core'; +// import type {PartComponent} from '@enonic-types/core'; +import type { + ComponentRegistry, + DecoratedPartComponent, +} from '../types'; -import type {ComponentRegistry} from '../types'; +import * as React from 'react'; - -export function BasePart({ +export function XpPart({ component, componentRegistry }: { - component: PartComponent + component: DecoratedPartComponent componentRegistry: ComponentRegistry }) { - // console.debug('BasePart component', component); + // console.info('XpPart component', component.); const { - config: props, - descriptor + config, + props = config, + descriptor, } = component; - // console.debug('BasePart descriptor', descriptor); + // console.debug('XpPart descriptor:', descriptor); const partDefinition = componentRegistry.getPart(descriptor); if (!partDefinition) { @@ -29,5 +33,7 @@ export function BasePart({ // TODO return ErrorBoundary instead of throwing. } props.componentRegistry = componentRegistry; - return (); + return ( + + ); } diff --git a/src/ComponentRegistry/XpRegion.tsx b/src/ComponentRegistry/XpRegion.tsx new file mode 100644 index 0000000..8c7ce56 --- /dev/null +++ b/src/ComponentRegistry/XpRegion.tsx @@ -0,0 +1,42 @@ +import type {Component} from '@enonic-types/core'; +import type {ClassValue} from 'clsx'; +import type {ComponentRegistry} from '../ComponentRegistry'; + +import cx from 'clsx'; +import {XpComponent} from './XpComponent'; + +export const XpRegion = ({ + as, + className, + components = [], + componentRegistry, + name, +}: { + as?: string + className?: ClassValue + components: Component[] + componentRegistry?: ComponentRegistry + name: string +}) => { + if (!((name || '').trim())) { + console.error(` name: ${JSON.stringify(name)}`); + throw Error(`Can't render without a 'name' prop.`); + } + // console.debug('XpRegion name:', name); + + const ElementType = (as || 'div') as keyof JSX.IntrinsicElements; + return ( + + { + components.map((component, i) => ) + } + + ); +} diff --git a/src/ComponentRegistry/XpRegions.tsx b/src/ComponentRegistry/XpRegions.tsx new file mode 100644 index 0000000..fb2371b --- /dev/null +++ b/src/ComponentRegistry/XpRegions.tsx @@ -0,0 +1,22 @@ +import type {Region} from '@enonic-types/core'; +import type {ComponentRegistry} from '../ComponentRegistry'; + +import {XpRegion} from './XpRegion'; + +export function XpRegions({ + componentRegistry, + regions +}: { + componentRegistry: ComponentRegistry; + regions: Record; +}) { + // console.debug('XpRegions regions:', regions); + return Object.keys(regions).map((name, i) => + + ) +} diff --git a/src/Region.tsx b/src/Region.tsx index 3dea13c..f474db1 100644 --- a/src/Region.tsx +++ b/src/Region.tsx @@ -1,9 +1,8 @@ import type {Region as RegionType} from '@enonic-types/core'; -import type {ComponentRegistry} from './ComponentRegistry'; // import * as PropTypes from 'prop-types'; import * as React from 'react'; -import {XpComponent} from './XpComponent'; +import ComponentTag from './ComponentTag'; /** * @param {string} name - Region name, as defined in a part's/page's/layout's XML definition @@ -14,13 +13,11 @@ import {XpComponent} from './XpComponent'; * @returns A react4xp-representation (react component) of an XP region. Must be SERVER-SIDE-rendered by react4xp! */ const Region = ({ - componentRegistry, name, regionData, tag, addClass }: { - componentRegistry?: ComponentRegistry name: string regionData: RegionType tag?: string @@ -48,10 +45,7 @@ const Region = ({ __html: `\t\t\t\t\t${ regionData.components && regionData.components.length > 0 ? regionData.components - .map(component => XpComponent({ - component, - componentRegistry - })) + .map(component => ComponentTag(component)) .join('\n') : '' }\t\t\t\t\t\n`, diff --git a/src/Regions.tsx b/src/Regions.tsx index 8bddb95..47f8ff0 100644 --- a/src/Regions.tsx +++ b/src/Regions.tsx @@ -6,7 +6,6 @@ import type {ComponentRegistry} from './ComponentRegistry'; import Region from './Region'; export interface RegionsProps { - componentRegistry?: ComponentRegistry regionsData: Record names?: string | string[] tags?: string | Record @@ -29,7 +28,6 @@ export interface RegionsProps { * @returns {Region[]} An array of elements. */ const Regions = ({ - componentRegistry, regionsData, names, tags, @@ -56,7 +54,6 @@ const Regions = ({ // TODO: sanitize tag and name: not all characters (or tags) are acceptable return selectedRegions.map(name => = ({ -// componentRegistry, -// config, -// descriptor, -// children -// }) => { -// console.info('MacroWithComponentRegistry', {config, descriptor, children}); -// if (componentRegistry) { -// // @ts-ignore -// if (descriptor === 'info') { -// const InfoMacro = componentRegistry.getMacro<{ -// header: string -// text: string -// }>('macro'); -// console.info(InfoMacro); -// const { -// body, -// header, -// } = config; -// console.info('MacroWithComponentRegistry', {body, header}); -// return ( -// -// ); -// } -// } -// return null; -// } - -export function XpComponent({ - component, - componentRegistry -}: { - component: Component - componentRegistry?: ComponentRegistry -}) { - // if (!componentRegistry) { - return ComponentTag(component); - // } - - // const { - // type - // } = component; - - // if (type === XP_COMPONENT_TYPE.TEXT) { - // // console.info('XpComponent', {component}); - - // // const data = {}; - // const data = replaceMacroComments(component.text); - // console.info('data', data); - - // // componentRegistry={componentRegistry} - // // Macro={Macro} - // return ( - // - // data={data} - // /> - // ); - // } - - // return ComponentTag(component); -} diff --git a/src/index.ts b/src/index.ts index e08f605..7766aeb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ export type { - Content, ContentUri, ImageComponent, ImageComponentParams, @@ -18,6 +17,7 @@ export type { MediaUri, Replacer, ReplacerResult, + RichtextContent, RichTextData, RichTextParams, } from './types'; diff --git a/src/processComponents.ts b/src/processComponents.ts index 134af83..9162740 100644 --- a/src/processComponents.ts +++ b/src/processComponents.ts @@ -1,12 +1,13 @@ import type { Component, - Content, + // Content, FragmentComponent, // Layout, LayoutComponent, PageComponent, // Part, PartComponent, + Request, // TODO TextComponent, } from '@enonic-types/core'; import type { @@ -24,13 +25,20 @@ import type { FormItemSet, MixinSchema, } from '@enonic-types/lib-schema'; +import type { + DecoratedLayoutComponent, + DecoratedPageComponent, + DecoratedPartComponent, + FragmentContent, + PageContent +} from './types'; import {getIn} from '@enonic/js-utils/object/getIn'; import {setIn} from '@enonic/js-utils/object/setIn'; -import {replaceMacroComments} from './replaceMacroComments'; +import {stringify} from 'q-i'; -type FragmentContent = Content; +import {replaceMacroComments} from './replaceMacroComments'; type ListSchemas = typeof listSchemas; type ProcessHtml = typeof processHtml; @@ -55,11 +63,40 @@ export interface GetComponentReturnType { }; type GetComponent = (params: GetDynamicComponentParams) => GetComponentReturnType; +interface LayoutComponentToPropsParams { + component: LayoutComponent; + processedComponent: DecoratedLayoutComponent; + processedConfig: Record; + content: PageContent; + request: Request; +} + +interface PageComponentToPropsParams { + component: PageComponent; + processedComponent: DecoratedPageComponent; + processedConfig: Record; + content: PageContent; + request: Request; +} + +interface PartComponentToPropsParams { + component: PartComponent; + processedConfig: Record; + content: PageContent; + request: Request; +} + +type LayoutComponentToPropsFunction = (params: LayoutComponentToPropsParams) => Record; +type PageComponentToPropsFunction = (params: PageComponentToPropsParams) => Record; +type PartComponentToPropsFunction = (params: PartComponentToPropsParams) => Record; -class ComponentProcessor { +export class ComponentProcessor { private getComponentSchema: GetComponent; private getContentByKey: GetContentByKey; private listSchemas: ListSchemas; + private layouts: Record = {}; + private pages: Record = {}; + private parts: Record = {}; private processHtml: ProcessHtml; private mixinSchemas: Record = {} @@ -196,7 +233,15 @@ class ComponentProcessor { return htmlAreas; } - private processFragment(component: FragmentComponent) { + private processFragment({ + component, + content, + request, + }: { + component: FragmentComponent; + content: PageContent; + request: Request; + }) { const { fragment: key, path @@ -204,13 +249,13 @@ class ComponentProcessor { // console.info('processFragment fragment key:', key); // @ts-expect-error Too complex/strict type generics. - const content = this.getContentByKey({key}); - if (!content) { + const fragmentContent = this.getContentByKey({key}); + if (!fragmentContent) { throw new Error(`processFragment: content not found for key: ${key}!`); } // console.info('processFragment content:', content); - const {fragment} = content; + const {fragment} = fragmentContent; if (!fragment) { throw new Error(`processFragment: fragment not found in content with key: ${key}!`); } @@ -224,10 +269,18 @@ class ComponentProcessor { // console.info('processFragment fragment:', fragment); if(type === 'part') { - return this.processPart(fragment); + return this.processPart({ + component: fragment, + content, + request, + }); } if(type === 'layout') { - return this.processLayout(fragment); + return this.processLayout({ + component: fragment, + content, + request, + }); } if(type === 'text') { return this.processTextComponent(fragment as TextComponent); @@ -235,31 +288,88 @@ class ComponentProcessor { throw new Error(`processFragment: fragment type not supported: ${type}!`); } - private processLayout(component: LayoutComponent) { + private processLayout({ + component, + content, + request, + }: { + component: LayoutComponent; + content: PageContent; + request: Request; + }): LayoutComponent | DecoratedLayoutComponent { const {descriptor} = component; const {form} = this.getComponentSchema({ key: descriptor, type: 'LAYOUT', }); - return this.processWithRegions({ + const processedComponent = this.processWithRegions({ component, + content, form, - }); + request, + }) as DecoratedLayoutComponent; + const toProps = this.layouts[descriptor]; + if (toProps) { + const decoratedComponent: DecoratedLayoutComponent = JSON.parse(JSON.stringify(component)); + decoratedComponent.props = toProps({ + component, + processedComponent: processedComponent, + processedConfig: processedComponent.config, + content, + request, + }); + // decoratedComponent.processedConfig = processedComponent.config; + return decoratedComponent; + } + return processedComponent as LayoutComponent; } - private processPage(component: PageComponent) { + private processPage({ + component, + content, + request, + }: { + component: PageComponent; + content: PageContent; + request: Request; + }): PageComponent | DecoratedPageComponent { + // console.debug('processPage component:', component); const {descriptor} = component; const {form} = this.getComponentSchema({ key: descriptor, type: 'PAGE', }); - return this.processWithRegions({ + const processedComponent = this.processWithRegions({ component, + content, form, - }); + request, + }) as DecoratedPageComponent; + const toProps = this.pages[descriptor]; + if (toProps) { + const decoratedComponent: DecoratedPageComponent = JSON.parse(JSON.stringify(component)); + decoratedComponent.props = toProps({ + component, + processedComponent: processedComponent, + processedConfig: processedComponent.config, + content, + request, + }); + // decoratedComponent.processedConfig = processedComponent.config; + return decoratedComponent; + } + return processedComponent as PageComponent; } - private processPart(component: PartComponent) { + private processPart({ + component, + content, + request, + }: { + component: PartComponent; + content: PageContent; + request: Request; + }): PartComponent | DecoratedPartComponent { const {descriptor} = component; const {form} = this.getComponentSchema({ key: descriptor, @@ -288,6 +398,18 @@ class ComponentProcessor { const data = replaceMacroComments(processedHtml); setIn(processedComponent, path, data); } + } // for + const toProps = this.parts[descriptor]; + if (toProps) { + const decoratedComponent: DecoratedPartComponent = JSON.parse(JSON.stringify(component)); + decoratedComponent.props = toProps({ + component, + processedConfig: processedComponent.config, + content, + request, + }); + // decoratedComponent.processedConfig = processedComponent.config; + return decoratedComponent; } return processedComponent; } @@ -301,26 +423,34 @@ class ComponentProcessor { } private processWithRegions({ - component, + component: layoutOrPageComponent, + content, form, + request, }: { component: LayoutComponent | PageComponent; + content: PageContent; form: NestedPartial[]; - }) { + request: Request; + }): DecoratedLayoutComponent | DecoratedPageComponent { const htmlAreas = this.getHtmlAreas({ ancestor: 'config', form, }); // console.info('processWithRegions htmlAreas:', htmlAreas); - const processedComponent = JSON.parse(JSON.stringify(component)); + const decoratedLayoutOrPageComponent: DecoratedLayoutComponent | DecoratedPageComponent = JSON.parse(JSON.stringify(layoutOrPageComponent)); + + //────────────────────────────────────────────────────────────────────── + // This modifies layoutOrPage.config: + //────────────────────────────────────────────────────────────────────── for (let i = 0; i < htmlAreas.length; i++) { // console.info('component:', component); const path = htmlAreas[i]; - // console.info('path:', path); + // console.debug('processWithRegions path:', path); - const html = getIn(component, path) as string; + const html = getIn(layoutOrPageComponent, path) as string; // console.info('html:', html); if (html) { @@ -328,11 +458,16 @@ class ComponentProcessor { value: html }); const data = replaceMacroComments(processedHtml); - setIn(processedComponent, path, data); + setIn(decoratedLayoutOrPageComponent, path, data); } } // for + // console.debug('processWithRegions config:', decoratedLayoutOrPageComponent.config); - const {regions} = component; + //────────────────────────────────────────────────────────────────────── + // This modifies layoutOrPage.regions: + //────────────────────────────────────────────────────────────────────── + const {regions} = layoutOrPageComponent; + // console.debug('processWithRegions regions:', stringify(regions, {maxItems: Infinity})); const regionNames = Object.keys(regions); for (let i = 0; i < regionNames.length; i++) { const regionName = regionNames[i]; @@ -340,20 +475,77 @@ class ComponentProcessor { const components = region.components; for (let j = 0; j < components.length; j++) { const component = components[j]; - processedComponent.regions[regionName].components[j] = this.process(component); + // @ts-expect-error + decoratedLayoutOrPageComponent.regions[regionName].components[j] = this.process({ + component, + content, + request, + }); } } - return processedComponent; + // console.debug('processWithRegions regions:', stringify(decoratedLayoutOrPageComponent.regions, {maxItems: Infinity})); + return decoratedLayoutOrPageComponent; + } // processWithRegions + + public addLayout(descriptor: string, { + toProps + }: { + toProps: (params: LayoutComponentToPropsParams) => Record; + }) { + // console.debug('addLayout:', descriptor); + this.layouts[descriptor] = toProps; + } + + public addPage(descriptor: string, { + toProps + }: { + toProps: (params: PageComponentToPropsParams) => Record; + }) { + // console.debug('addPage:', descriptor); + this.pages[descriptor] = toProps; + } + + public addPart(descriptor: string, { + toProps + }: { + toProps: (params: PartComponentToPropsParams) => Record; + }) { + // console.debug('addPart:', descriptor); + this.parts[descriptor] = toProps; } - public process(component:Component) { + public process({ + component, + content, + request, + }: { + component: Component; + content: PageContent; + request: Request; + }) { const {type} = component; switch (type) { - case 'part': return this.processPart(component); - case 'layout': return this.processLayout(component as LayoutComponent); - case 'page': return this.processPage(component as PageComponent); + case 'part': return this.processPart({ + component, + content, + request, + }); + case 'layout': return this.processLayout({ + component: component as LayoutComponent, + content, + request, + }); + case 'page': return this.processPage({ + component: component as PageComponent, + content, + request, + }); case 'text': return this.processTextComponent(component as TextComponent); - case 'fragment': return this.processFragment(component as FragmentComponent); + case 'fragment': return this.processFragment({ + component: component as FragmentComponent, + content, + request, + }); default: throw new Error(`processComponents: component type not supported: ${type}!`); } } @@ -361,16 +553,20 @@ class ComponentProcessor { export function processComponents({ component, + content, getComponentSchema, getContentByKey, listSchemas, processHtml, + request, }: { component: Component; + content: PageContent; getComponentSchema: GetComponent; getContentByKey: GetContentByKey; listSchemas: ListSchemas; processHtml: ProcessHtml; + request: Request; }) { const processor = new ComponentProcessor({ getComponentSchema, @@ -378,5 +574,9 @@ export function processComponents({ listSchemas, processHtml, }); - return processor.process(component); + return processor.process({ + component, + content, + request + }); } diff --git a/src/replaceMacroComments.ts b/src/replaceMacroComments.ts index fad042a..99f499f 100644 --- a/src/replaceMacroComments.ts +++ b/src/replaceMacroComments.ts @@ -9,48 +9,57 @@ export function replaceMacroComments(processedHtml: string): RichTextData { macros: [] }; let index = 0; - rv.processedHtml = processedHtml.replace( - //gm, - (_origHtmlMacroComment, attributesString) => { - // Replacer is executed once per match (macro comment) - index++; - const ref = index.toString(); - let name: string = ''; - const macro: Partial = { - config: {}, - ref, - }; - const replacedAttributes = attributesString.replace( - /([^=]+)="([^"]*)"\s*/g, - (_kv, key, value) => { - // Replacer is executed once per match (attribute key/value) - if (key === '_name') { - name = value; - macro.name = name; - macro.descriptor = `whatever:${name}`; - return `data-macro-name="${value}" data-macro-ref="${ref}"`; - } - if (key === '_document') { - return ''; - } - if (key === '_body') { - key = 'body'; - } - if (macro.config && name) { - if (!macro.config[name]) { - macro.config[name] = {}; + rv.processedHtml = processedHtml + .replace( + /

()<\/p>/gm, + '$1' + ) + .replace( + /

()<\/pre>/gm,
+			'$1'
+		)
+		.replace(
+			//gm,
+			(_origHtmlMacroComment, attributesString) => {
+				// Replacer is executed once per match (macro comment)
+				index++;
+				const ref = index.toString();
+				let name: string = '';
+				const macro: Partial = {
+					config: {},
+					ref,
+				};
+				const replacedAttributes = attributesString.replace(
+					/([^=]+)="([^"]*)"\s*/g,
+					(_kv, key, value) => {
+						// Replacer is executed once per match (attribute key/value)
+						if (key === '_name') {
+							name = value;
+							macro.name = name;
+							macro.descriptor = `whatever:${name}`;
+							return `data-macro-name="${value}" data-macro-ref="${ref}"`;
 						}
-						macro.config[name][key] = value;
+						if (key === '_document') {
+							return '';
+						}
+						if (key === '_body') {
+							key = 'body';
+						}
+						if (macro.config && name) {
+							if (!macro.config[name]) {
+								macro.config[name] = {};
+							}
+							macro.config[name][key] = value;
+						}
+						return '';
 					}
-					return '';
+				)
+				const replacedMacro = ``;
+				if (rv.macros) {
+					rv.macros.push(macro as MacroData);
 				}
-			)
-			const replacedMacro = ``;
-			if (rv.macros) {
-				rv.macros.push(macro as MacroData);
-			}
-			return replacedMacro;
-		} // single macro replacer
-	);
+				return replacedMacro;
+			} // single macro replacer
+		);
 	return rv;
 }
diff --git a/src/types/index.ts b/src/types/index.ts
index 059de71..72d8289 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,5 +1,14 @@
 // There is a difference between the core enonic types and what Guillotine returns:
-// import type {Content} from '@enonic-types/core';
+import type {
+	Content,
+	Component,
+	Layout,
+	LayoutComponent,
+	Part,
+	PartComponent,
+	Page,
+	PageComponent
+} from '@enonic-types/core';
 
 // The Guillotine types are similar, but uses complex types:
 // import type {Content} from '@enonic-types/guillotine/advanced';
@@ -36,19 +45,36 @@ export interface ComponentRegistry {
 	hasPart(name: string): boolean
 }
 
-export type Content<
+export type DecoratedLayoutComponent = LayoutComponent & {
+	// processedConfig: Record
+	processedComponent: LayoutComponent
+	props?: Record
+}
+
+export type DecoratedPageComponent = PageComponent & {
+	// processedConfig: Record
+	processedComponent: PageComponent
+	props?: Record
+}
+
+export type DecoratedPartComponent = PartComponent & {
+	// processedConfig: Record
+	props?: Record
+}
+
+export type RichtextContent<
 	Extensions extends Record = Record
 > = {
 	// Direct Properties
 	_id: string
 	_name: string
 	_path: string
-	_references: Content[]
+	_references: RichtextContent[]
 	_score?: number
 	// attachments: Attachment[]
-	children: Content[]
+	children: RichtextContent[]
 	// childrenConnection: ContentConnection
-	components: Content[]
+	components: RichtextContent[]
 	// contentType: ContentType
 	createdTime: string
 	// creator: PrincipalKey
@@ -60,9 +86,9 @@ export type Content<
 	// modifier: PrincipalKey
 	// owner: PrincipalKey
 	// pageAsJson: GraphQLJson
-	pageTemplate: Content
+	pageTemplate: RichtextContent
 	pageUrl: string
-	parent: Content
+	parent: RichtextContent
 	// permissions: Permissions
 	// publish: PublishInfo
 	// site: portal_Site
@@ -90,7 +116,11 @@ export interface CreateReplacerParams> {
 	replacer?: Replacer
 }
 
-export type ImageContent = Partial & {
+export type FragmentContent<
+	Component extends LayoutComponent | PartComponent = Layout | Part
+> = Content;
+
+export type ImageContent = Partial & {
 	imageUrl?: string
 }
 
@@ -130,7 +160,7 @@ export type LinkComponentParams<
 	RestProps = Record
 > = {
 	children: ReactNode
-	content?: Partial | null
+	content?: Partial | null
 	href: string
 	media?: LinkDataMedia | null
 	target?: string
@@ -140,13 +170,13 @@ export type LinkComponentParams<
 
 export interface LinkData {
 	ref: string
-	content?: Partial | null
+	content?: Partial | null
 	media?: LinkDataMedia | null
 	uri: string
 }
 
 export interface LinkDataMedia {
-	content: Partial & {
+	content: Partial & {
 		mediaUrl?: string
 	}
 	intent: 'inline' | 'download'
@@ -178,6 +208,17 @@ export type MacroDescriptor = `${string}:${string}`;
 
 export type MediaUri = `media://${string}`;
 
+export type PageContent<
+	Data = Record,
+	Type extends string = string,
+	Component extends PageComponent = Page
+> = Content<
+	Data,
+	Type,
+	// @ts-expect-error Does not satisfy the type constraint
+	Component
+>
+
 export interface ReplaceMacroParams> {
 	componentRegistry?: ComponentRegistry
 	createReplacer: CreateReplacer
diff --git a/test/ComponentRegistry/BasePart.test.tsx b/test/ComponentRegistry/BasePart.test.tsx
index 664e326..226a5e2 100644
--- a/test/ComponentRegistry/BasePart.test.tsx
+++ b/test/ComponentRegistry/BasePart.test.tsx
@@ -1,4 +1,8 @@
-import type {InfoPanelProps} from '../processComponents/InfoPanel';
+import type {
+	Request
+} from '@enonic-types/core';
+// import type {InfoPanelProps} from '../processComponents/InfoPanel';
+import type {DecoratedPartComponent} from '../../src/types';
 
 import {
 	// beforeAll,
@@ -9,22 +13,60 @@ import {
 } from '@jest/globals';
 import {render} from '@testing-library/react'
 import toDiffableHtml from 'diffable-html';
+import {print} from 'q-i';
 import * as React from 'react';
 
 // SRC imports
 import {ComponentRegistry} from '../../src/ComponentRegistry';
-import {BasePart} from '../../src/ComponentRegistry/BasePart';
-import {processComponents} from '../../src/processComponents';
+import {XpPart} from '../../src/ComponentRegistry/XpPart';
+import {ComponentProcessor} from '../../src/processComponents';
 
 // TEST imports
 import {InfoPanel} from '../processComponents/InfoPanel';
 import {
+	EXAMPLE_PART_DESCRIPTOR,
+	PAGE_CONTENT,
 	PART_COMPONENT,
 	PROCESSED_HTML
 } from '../processComponents/data'
 import {PART_SCHEMA} from '../processComponents/schema'
 import {ExamplePart} from './ExamplePart';
 
+const componentProcessor = new ComponentProcessor({
+	getComponentSchema: () => {
+		return PART_SCHEMA;
+	},
+	// @ts-expect-error
+	getContentByKey: ({key}) => {
+		// console.debug("getContentByKey:", key);
+		return {};
+	},
+	listSchemas: ({
+		application,
+		type
+	}) => {
+		// console.debug("listSchemas:", application, type);
+		return [];
+	},
+	processHtml: ({ value }) => {
+		// console.info("processHtml:", value);
+		return PROCESSED_HTML;
+	},
+});
+
+componentProcessor.addPart(EXAMPLE_PART_DESCRIPTOR, {
+	toProps: ({
+		component,
+		content,
+		processedConfig,
+		request,
+	}) => {
+		// console.debug("addPart:", { component, content, processedConfig, request });
+		return {
+			data: processedConfig.anHtmlArea
+		};
+	},
+});
 
 const componentRegistry = new ComponentRegistry;
 // const macroName = 'com.enonic.app.react4xp:info'; // NOPE, just 'info'
@@ -32,36 +74,19 @@ const macroName = 'info';
 componentRegistry.addMacro(macroName, {
 	View: InfoPanel
 });
-const partName = 'com.enonic.app.react4xp:example';
-componentRegistry.addPart(partName, {
+componentRegistry.addPart(EXAMPLE_PART_DESCRIPTOR, {
 	View: ExamplePart
 });
 
 describe('ComponentRegistry', () => {
 	it('should be able to render a part component', () => {
-		const processedComponent = processComponents({
+		const processedComponent = componentProcessor.process({
 			component: PART_COMPONENT,
-			getComponentSchema: () => {
-				return PART_SCHEMA;
-			},
-			// @ts-expect-error
-			getContentByKey: ({key}) => {
-				console.debug("getContentByKey:", key);
-				return {};
-			},
-			listSchemas: ({
-				application,
-				type
-			}) => {
-				console.debug("listSchemas:", application, type);
-				return [];
-			},
-			processHtml: ({ value }) => {
-				// console.info("processHtml:", value);
-				return PROCESSED_HTML;
-			},
-		});
-		const element = render().container;
@@ -69,40 +94,14 @@ describe('ComponentRegistry', () => {
 
-

-

- - - - Header - - Text -
-

-
-
-

-

- - - - Header - - Text -
-

-
-
-

-

- - - - Header - - Text -
-

+
+ + + + Header + + Text +
diff --git a/test/ComponentRegistry/DefaultPage.tsx b/test/ComponentRegistry/DefaultPage.tsx new file mode 100644 index 0000000..e7c7abf --- /dev/null +++ b/test/ComponentRegistry/DefaultPage.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import {XpRegions} from '../../src/ComponentRegistry/XpRegions'; + +export function DefaultPage(props) { + // console.debug('DefaultPage props', props); + const { + componentRegistry, + regions, + } = props; + return ( +
+ +
+ ); +} diff --git a/test/ComponentRegistry/ExamplePart.tsx b/test/ComponentRegistry/ExamplePart.tsx index b571a84..cbb22c3 100644 --- a/test/ComponentRegistry/ExamplePart.tsx +++ b/test/ComponentRegistry/ExamplePart.tsx @@ -5,31 +5,16 @@ export function ExamplePart(props) { // console.debug('ExamplePart props', props); const { - anHtmlArea, - anItemSet: { - anHtmlArea: anItemHtmlArea, - }, - anOptionSet, componentRegistry, + data, } = props; - // console.debug('ExamplePart anOptionSet', anOptionSet); - - const anOptionSetHtmlArea = anOptionSet[1].text.anHtmlArea; - // console.debug('ExamplePart anOptionSetHtmlArea', anOptionSetHtmlArea); + // console.debug('ExamplePart data', data); return (
- -
); diff --git a/test/ComponentRegistry/TwoColumnLayout.tsx b/test/ComponentRegistry/TwoColumnLayout.tsx new file mode 100644 index 0000000..231122b --- /dev/null +++ b/test/ComponentRegistry/TwoColumnLayout.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import {XpRegions} from '../../src/ComponentRegistry/XpRegions'; + +export function TwoColumnLayout(props) { + // console.debug('TwoColumnLayout props', props); + const { + componentRegistry, + regions, + } = props; + return ( +
+ +
+ ); +} diff --git a/test/RichText.ComponentRegistry.test.tsx b/test/RichText.ComponentRegistry.test.tsx index 3eed81f..4780246 100644 --- a/test/RichText.ComponentRegistry.test.tsx +++ b/test/RichText.ComponentRegistry.test.tsx @@ -120,48 +120,46 @@ describe('ComponentRegistry', () => { componentRegistry={componentRegistry} data={data} />).container; - expect(toDiffableHtml(element.outerHTML)).toEqual(` -
-
-

- - Home - -

-
-
- Can't replace image, when there are no images in the data object! -
-
- Caption -
-
-

-

- - - - Header1 - - Text1 -
-

-

-

- - - - Header2 - - Text2 -
-

-
-
-`); + expect(toDiffableHtml(element.outerHTML)).toEqual( + toDiffableHtml(` +
+
+

+ + Home + +

+
+
+ Can't replace image, when there are no images in the data object! +
+
+ Caption +
+
+
+ + + + Header1 + + Text1 +
+
+ + + + Header2 + + Text2 +
+
+
+ `) + ); // const props = {...REGIONS_PROPS, componentRegistry}; // // print(props, { maxItems: Infinity }); diff --git a/test/processComponents/data.ts b/test/processComponents/data.ts index 16af8e1..a075b38 100644 --- a/test/processComponents/data.ts +++ b/test/processComponents/data.ts @@ -1,12 +1,13 @@ import type { - // Content, + Content, LayoutComponent, PageComponent, PartComponent, FragmentComponent, TextComponent, } from '@enonic-types/core'; +import type {PageContent} from '../../src/types'; import { HTML_AREA_KEY, @@ -42,10 +43,12 @@ export const TEXT_COMPONENT: TextComponent = { text: UNPROCESSED_HTML, }; +export const EXAMPLE_PART_DESCRIPTOR = "com.enonic.app.react4xp:example"; + export const PART_COMPONENT: PartComponent = { path: "/main/0/left/0", type: "part", - descriptor: "com.enonic.app.react4xp:example", + descriptor: EXAMPLE_PART_DESCRIPTOR, config: CONFIG, }; @@ -71,6 +74,8 @@ export const LAYOUT_FRAGMENT_COMPONENT: FragmentComponent = { fragment: LAYOUT_FRAGMENT_CONTENT_ID, }; +export const TWO_COLUMNS_LAYOUT_DESCRIPTOR = "com.enonic.app.react4xp:twoColumns"; + export const LAYOUT_FRAGMENT_CONTENT = { _id: LAYOUT_FRAGMENT_CONTENT_ID, _name: "fragment-two-columns", @@ -89,7 +94,7 @@ export const LAYOUT_FRAGMENT_CONTENT = { x: {}, fragment: { type: "layout", - descriptor: "com.enonic.app.react4xp:twoColumns", + descriptor: TWO_COLUMNS_LAYOUT_DESCRIPTOR, config: { myhtmlarea: '

[info header="Header"]Text[/info]

\n', }, @@ -157,7 +162,7 @@ export const PART_FRAGMENT_CONTENT = { x: {}, fragment: { type: "part", - descriptor: "com.enonic.app.react4xp:example", + descriptor: EXAMPLE_PART_DESCRIPTOR, config: CONFIG, }, attachments: {}, @@ -165,16 +170,16 @@ export const PART_FRAGMENT_CONTENT = { }; export const LAYOUT_COMPONENT: LayoutComponent = { - path: "/main/0", - type: "layout", - descriptor: "com.enonic.app.react4xp:twoColumns", + path: '/main/0', + type: 'layout', + descriptor: TWO_COLUMNS_LAYOUT_DESCRIPTOR, config: CONFIG, regions: { left: { components: [ - TEXT_COMPONENT, - TEXT_FRAGMENT_COMPONENT, - // PART_COMPONENT, + // TEXT_COMPONENT, + // TEXT_FRAGMENT_COMPONENT, + {...PART_COMPONENT, path: '/main/0/left/0'}, // PART_FRAGMENT_COMPONENT, ], name: "left", @@ -191,10 +196,12 @@ export const LAYOUT_COMPONENT: LayoutComponent = { }, }; +export const DEFAULT_PAGE_DESCRIPTOR = "com.enonic.app.react4xp:default"; + export const PAGE_COMPONENT: PageComponent = { type: "page", path: "/", - descriptor: "com.enonic.app.react4xp:default", + descriptor: DEFAULT_PAGE_DESCRIPTOR, config: CONFIG, regions: { main: { @@ -202,7 +209,8 @@ export const PAGE_COMPONENT: PageComponent = { // TEXT_COMPONENT, // TEXT_FRAGMENT_COMPONENT, // LAYOUT_COMPONENT, - LAYOUT_FRAGMENT_COMPONENT, + {...PART_COMPONENT, path: "/main/0"}, + // LAYOUT_FRAGMENT_COMPONENT, // { // ...PART_FRAGMENT_COMPONENT, // path: "/main/1", @@ -212,3 +220,37 @@ export const PAGE_COMPONENT: PageComponent = { }, }, }; + +export const PAGE_CONTENT: PageContent = { + _id: "3e36f69a-fa2f-4943-a812-5a2d06f22e56", + _name: "mysite", + _path: "/mysite", + creator: "user:system:su", + modifier: "user:system:su", + createdTime: "2024-10-30T08:14:10.575402Z", + modifiedTime: "2024-11-05T09:36:11.782402Z", + owner: "user:system:su", + type: "portal:site", + displayName: "mysite", + hasChildren: true, + valid: true, + childOrder: "modifiedtime DESC", + data: { + siteConfig: [ + { + applicationKey: "com.enonic.app.react4xp", + config: {}, + }, + { + applicationKey: "com.enonic.app.panelmacros", + config: { + custom: false, + }, + }, + ], + }, + x: {}, + page: PAGE_COMPONENT, + attachments: {}, + publish: {}, +}; diff --git a/test/processComponents/processComponents.test.tsx b/test/processComponents/processComponents.test.tsx index 83a6512..0605951 100644 --- a/test/processComponents/processComponents.test.tsx +++ b/test/processComponents/processComponents.test.tsx @@ -1,3 +1,9 @@ +import type { + Component, + Part, + PartComponent, + Request, +} from '@enonic-types/core'; import type { ListDynamicSchemasParams, MixinSchema, @@ -5,35 +11,58 @@ import type { // import type {RegionsProps} from '../../src/Regions'; // import type {MacroComponent} from '../../src/types'; import type {InfoPanelProps} from './InfoPanel'; +import type { + DecoratedLayoutComponent, + DecoratedPageComponent, + DecoratedPartComponent +} from '../../src/types'; import { // beforeAll, // afterAll, describe, - // expect, + expect, test as it } from '@jest/globals'; -// import {render} from '@testing-library/react' -// import toDiffableHtml from 'diffable-html'; -// import {print} from 'q-i'; -// import React from 'react'; +import {render} from '@testing-library/react' +import toDiffableHtml from 'diffable-html'; +import {print, stringify} from 'q-i'; +import * as React from 'react'; +//────────────────────────────────────────────────────────────────────────────── +// SRC imports +//────────────────────────────────────────────────────────────────────────────── import {ComponentRegistry} from '../../src/ComponentRegistry'; -// import Regions from '../../../src/Regions'; +import {XpComponent} from '../../src/ComponentRegistry/XpComponent'; +import {XpRegion} from '../../src/ComponentRegistry/XpRegion'; +// import Regions from '../../src/Regions'; +// import Page from '../../src/Page'; // import {RichText} from '../../src/RichText'; // import {replaceMacroComments} from '../../src/replaceMacroComments'; -import {processComponents} from '../../src/processComponents'; +import { + ComponentProcessor, + // processComponents, +} from '../../src/processComponents'; import {InfoPanel} from './InfoPanel'; +//────────────────────────────────────────────────────────────────────────────── +// TEST imports +//────────────────────────────────────────────────────────────────────────────── import { + DEFAULT_PAGE_DESCRIPTOR, + EXAMPLE_PART_DESCRIPTOR, + LAYOUT_COMPONENT, LAYOUT_FRAGMENT_CONTENT_ID, LAYOUT_FRAGMENT_CONTENT, PAGE_COMPONENT, + PAGE_CONTENT, + PART_COMPONENT, PART_FRAGMENT_CONTENT_ID, PART_FRAGMENT_CONTENT, PROCESSED_HTML, TEXT_FRAGMENT_CONTENT_ID, TEXT_FRAGMENT_CONTENT, + TWO_COLUMNS_LAYOUT_DESCRIPTOR, } from './data'; import { LAYOUT_SCHEMA, @@ -41,55 +70,292 @@ import { PART_SCHEMA, PAGE_SCHEMA, } from './schema'; +import {DefaultPage} from '../ComponentRegistry/DefaultPage'; +import {ExamplePart} from '../ComponentRegistry/ExamplePart'; +import {TwoColumnLayout} from '../ComponentRegistry/TwoColumnLayout'; + +const componentProcessor = new ComponentProcessor({ + getComponentSchema: ({ + // key, + type, + }) => { + if (type === 'PART') return PART_SCHEMA; + if (type === 'LAYOUT') return LAYOUT_SCHEMA; + return PAGE_SCHEMA; + }, + // @ts-expect-error + getContentByKey: ({key}) => { + if (key === LAYOUT_FRAGMENT_CONTENT_ID) { + return LAYOUT_FRAGMENT_CONTENT; + } + if (key === PART_FRAGMENT_CONTENT_ID) { + return PART_FRAGMENT_CONTENT; + } + if (key === TEXT_FRAGMENT_CONTENT_ID) { + return TEXT_FRAGMENT_CONTENT; + } + console.error("getContentByKey:", key); + return undefined; + }, + listSchemas: ({ + application: _application, + type, + }: ListDynamicSchemasParams) => { + if (type === 'MIXIN') { + return MIXIN_SCHEMAS as MixinSchema[]; + } + // ContentSchemaType[] + // XDataSchema[] + throw new Error(`listSchemas: type: ${type} not mocked.`); + }, + processHtml: ({ value }) => { + // console.info("processHtml:", value); + return PROCESSED_HTML; + }, +}); +componentProcessor.addLayout(TWO_COLUMNS_LAYOUT_DESCRIPTOR, { + toProps: ({ + component, + content, + processedComponent, + processedConfig, + request, + }) => { + const {regions} = processedComponent; + // console.debug('layout toProps:', stringify({ + // // component, + // // content, + // processedComponent, + // processedConfig, + // // request + // })); + return { + // data: processedConfig.anHtmlArea, + regions + }; + }, +}); +componentProcessor.addPage(DEFAULT_PAGE_DESCRIPTOR, { + toProps: ({ + component, + content, + processedComponent, + processedConfig, + request, + }) => { + // console.debug('page toProps:', { + // // component, + // // content, + // processedComponent, + // processedConfig, + // // request + // }); + const {regions} = processedComponent; + return { + regions + }; + }, +}); +componentProcessor.addPart(EXAMPLE_PART_DESCRIPTOR, { + toProps: ({ + component, + content, + processedConfig, + request, + }) => { + // console.debug("part toProps:", { component, content, processedConfig, request }); + return { + data: processedConfig.anHtmlArea + }; + }, +}); const componentRegistry = new ComponentRegistry; componentRegistry.addMacro('info', { View: InfoPanel }); +componentRegistry.addPart(EXAMPLE_PART_DESCRIPTOR, { + View: ExamplePart +}); +componentRegistry.addLayout(TWO_COLUMNS_LAYOUT_DESCRIPTOR, { + View: TwoColumnLayout +}); +componentRegistry.addPage(DEFAULT_PAGE_DESCRIPTOR, { + View: DefaultPage +}); describe('processComponents', () => { - it('is able to process anything', () => { - const processed = processComponents({ - component: PAGE_COMPONENT, - getComponentSchema: ({ - // key, - type, - }) => { - if (type === 'PART') return PART_SCHEMA; - if (type === 'LAYOUT') return LAYOUT_SCHEMA; - return PAGE_SCHEMA; - }, - // @ts-expect-error - getContentByKey: ({key}) => { - if (key === LAYOUT_FRAGMENT_CONTENT_ID) { - return LAYOUT_FRAGMENT_CONTENT; - } - if (key === PART_FRAGMENT_CONTENT_ID) { - return PART_FRAGMENT_CONTENT; - } - if (key === TEXT_FRAGMENT_CONTENT_ID) { - return TEXT_FRAGMENT_CONTENT; - } - console.error("getContentByKey:", key); - return undefined; - }, - listSchemas: ({ - application: _application, - type, - }: ListDynamicSchemasParams) => { - if (type === 'MIXIN') { - return MIXIN_SCHEMAS as MixinSchema[]; - } - // ContentSchemaType[] - // XDataSchema[] - throw new Error(`listSchemas: type: ${type} not mocked.`); - }, - processHtml: ({ value }) => { - // console.info("processHtml:", value); - return PROCESSED_HTML; - }, - }); - // print(processed, { maxItems: Infinity }); - // expect(toDiffableHtml(element.outerHTML)).toEqual(``); + + // it('is able to process a part component', () => { +// const processedComponent = componentProcessor.process({ +// component: PART_COMPONENT as Component, +// content: PAGE_CONTENT, +// request: {} as Request, +// }); +// // print(processedComponent, { maxItems: Infinity }); +// expect(processedComponent.props).toEqual({ +// data: { +// processedHtml: '', +// macros: [ +// { +// config: { +// info: { +// header: 'Header', +// body: 'Text' +// } +// }, +// ref: '1', +// name: 'info', +// descriptor: 'whatever:info' +// } +// ] +// } +// }); +// // const element = render().container; +// // expect(toDiffableHtml(element.outerHTML)).toEqual(toDiffableHtml(` +// //
+// //
+// //
+// //
+// //
+// // +// // +// // +// // Header +// // +// // Text +// //
+// //
+// //
+// //
+// //
+// // `)); +// }); + + // it('is able to process a layout component', () => { + // const processedComponent = componentProcessor.process({ + // component: LAYOUT_COMPONENT as Component, + // content: PAGE_CONTENT, + // request: {} as Request, + // }) as DecoratedLayoutComponent; + // // print(processedComponent, { maxItems: Infinity }); + // // expect(processedComponent.props).toEqual({ + // // data: { + // // processedHtml: '', + // // macros: [ + // // { + // // config: { + // // info: { + // // header: 'Header', + // // body: 'Text' + // // } + // // }, + // // ref: '1', + // // name: 'info', + // // descriptor: 'whatever:info' + // // } + // // ] + // // } + // // }); + // const element = render().container; + // // console.debug(toDiffableHtml(element.outerHTML)); + // expect(toDiffableHtml(element.outerHTML)).toEqual(toDiffableHtml(` + //
+ //
+ //
+ //
+ //
+ //
+ // + // + // + // Header + // + // Text + //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // `)); + // }); + + it('is able to process a page component', () => { + const decoratedPageComponent = componentProcessor.process({ + component: PAGE_COMPONENT as Component, + content: PAGE_CONTENT, + request: {} as Request, + }) as DecoratedPageComponent; + // print(decoratedPageComponent, { maxItems: Infinity }); + // expect(decoratedPageComponent.props).toEqual({ + // data: { + // processedHtml: '', + // macros: [ + // { + // config: { + // info: { + // header: 'Header', + // body: 'Text' + // } + // }, + // ref: '1', + // name: 'info', + // descriptor: 'whatever:info' + // } + // ] + // } + // }); + const element = render().container; + // console.debug(toDiffableHtml(element.outerHTML)); + expect(toDiffableHtml(element.outerHTML)).toEqual(toDiffableHtml(` +
+
+
+
+
+
+ + + + Header + + Text +
+
+
+
+
+
+ `)); }); }); diff --git a/test/replaceMacroComments.test.ts b/test/replaceMacroComments.test.ts index ffe2039..e26689b 100644 --- a/test/replaceMacroComments.test.ts +++ b/test/replaceMacroComments.test.ts @@ -5,44 +5,49 @@ import { expect, test as it } from '@jest/globals'; +import toDiffableHtml from 'diffable-html'; import {replaceMacroComments} from '../src/replaceMacroComments'; describe('replaceMacroComments', () => { it('should replace macro comments', () => { - expect(replaceMacroComments(`

-

`)) - .toEqual({ - processedHtml: `

-

`, - macros: [ - { - "ref": "1", - "name": "info", - "descriptor": "whatever:info", - "config": { - "info": { - "body": "Text1
\nWith
\nNewlines", - "header": "Header1" +Newlines"-->
`); + replaced.processedHtml = toDiffableHtml(replaced.processedHtml); + expect(replaced) + .toEqual({ + processedHtml: toDiffableHtml(` + + + `), + macros: [ + { + "ref": "1", + "name": "info", + "descriptor": "whatever:info", + "config": { + "info": { + "body": "Text1
\nWith
\nNewlines", + "header": "Header1" + } } - } - }, - { - "ref": "2", - "name": "info", - "descriptor": "whatever:info", - "config": { - "info": { - "body": "Text2
\nWith
\nNewlines", - "header": "Header2" + }, + { + "ref": "2", + "name": "info", + "descriptor": "whatever:info", + "config": { + "info": { + "body": "Text2
\nWith
\nNewlines", + "header": "Header2" + } } } - } - ] - }); - }); -}); + ] + }); + }); // it +}); // describe diff --git a/test/tsconfig.json b/test/tsconfig.json index 037c385..0d6e1c1 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -6,6 +6,9 @@ "DOM" ], "paths": { + "@enonic-types/core": ["../../xp-comlock-9742/modules/lib/core/index.d.ts"], + "/lib/xp/content": ["../../xp-comlock-9742/modules/lib/lib-content/src/main/resources/lib/xp/content.ts"], + "/lib/xp/portal": ["../../xp-comlock-9742/modules/lib/lib-portal/src/main/resources/lib/xp/portal.ts"], "/lib/xp/schema": ["../node_modules/@enonic-types/lib-schema"], }, "types": [ diff --git a/tsconfig.json b/tsconfig.json index 7d7e5cc..caefdf9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,11 +25,9 @@ "moduleResolution": "bundler", "paths": { - // This is only needed buildtime - // "html-dom-parser": [ - // // "./node_modules/html-dom-parser/esm/server/html-to-dom.mjs", - // "./node_modules/html-dom-parser/lib/server/html-to-dom.js", - // ], + "@enonic-types/core": ["../xp-comlock-9742/modules/lib/core/index.d.ts"], + "/lib/xp/content": ["../xp-comlock-9742/modules/lib/lib-content/src/main/resources/lib/xp/content.ts"], + "/lib/xp/portal": ["../xp-comlock-9742/modules/lib/lib-portal/src/main/resources/lib/xp/portal.ts"], }, // Even though the setting disables type checking for d.ts files,