import { cx } from 'class-variance-authority' import { Divider } from '../Divider' import { MaterialIcon } from '../Icons/MaterialIcon' import Image from '../Image' import ImageContainer from '../ImageContainer' import Link from '../Link' import Table from '../Table' import { Typography } from '../Typography' import { extractAvailableListClassNames, hasAvailableParagraphFormat, } from './utils' import styles from './jsontohtml.module.css' import { DeprecatedImageVaultAssetResponse, ImageVaultAssetResponse, mapImageVaultAssetResponseToImageVaultAsset, mapInsertResponseToImageVaultAsset, } from '@scandic-hotels/common/utils/imageVault' import type { EmbedByUid } from './JsonToHtml' import type { Attributes } from './types/rte/attrs' import { AvailableParagraphFormatEnum, RTEItemTypeEnum, RTETypeEnum, } from './types/rte/enums' import { type RTEDefaultNode, type RTEImageNode, RTEMarkType, type RTENext, type RTENode, type RTERegularNode, type RTETextNode, } from './types/rte/node' import type { RenderOptions } from './types/rte/option' function noNestedLinksOrReferences(node: RTENode) { if ('type' in node) { if (node.type === RTETypeEnum.reference) { return node.children } else if (node.type === RTETypeEnum.a) { return node.children } } return node } function extractPossibleAttributes(attrs: Attributes | undefined) { if (!attrs) return {} const props: Record & { className?: string style?: React.CSSProperties | undefined } = {} if (attrs.id) { props.id = attrs.id } if (attrs.class && typeof attrs.class === 'string') { props.className = attrs.class } else if (attrs['class-name']) { props.className = attrs['class-name'] } else if (attrs.classname && typeof attrs.classname === 'string') { props.className = attrs.classname } if (attrs.style?.['text-align']) { props.style = { textAlign: attrs.style['text-align'] as React.CSSProperties['textAlign'], } } return props } export const renderOptions: RenderOptions = { [RTETypeEnum.a]: ( node: RTERegularNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { if (!node.attrs.url) { return } const { className, ...props } = extractPossibleAttributes(node.attrs) return ( {next( // Sometimes editors happen to nest a reference inside a link and vice versa. // In that case use the outermost link, i.e. ignore nested links. node.children.flatMap(noNestedLinksOrReferences), embeds, fullRenderOptions )} ) }, [RTETypeEnum.blockquote]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) return (
{next(node.children, embeds, fullRenderOptions)}
) }, [RTETypeEnum.code]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) return ( {next(node.children, embeds, fullRenderOptions)} ) }, [RTETypeEnum.embed]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) if (node.attrs.src) { props.src = node.attrs.src } if (node.attrs.url) { props.src = node.attrs.url } if (!props.src) { return null } return ( ) }, [RTETypeEnum.h1]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) return (

{next(node.children, embeds, fullRenderOptions)}

) }, [RTETypeEnum.h2]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) return (

{next(node.children, embeds, fullRenderOptions)}

) }, [RTETypeEnum.h3]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) return (

{next(node.children, embeds, fullRenderOptions)}

) }, [RTETypeEnum.h4]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) return (

{next(node.children, embeds, fullRenderOptions)}

) }, [RTETypeEnum.h5]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) return (
{next(node.children, embeds, fullRenderOptions)}
) }, [RTETypeEnum.hr]: () => { return }, [RTETypeEnum.li]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) const compatibleClassNames = extractAvailableListClassNames(className) return (
  • {next(node.children, embeds, fullRenderOptions)}
  • ) }, [RTETypeEnum.ol]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) const compatibleClassNames = extractAvailableListClassNames(className) return (
      {next(node.children, embeds, fullRenderOptions)}
    ) }, [RTETypeEnum.p]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) const hasFormat = node.children.some((item) => hasAvailableParagraphFormat((item as RTETextNode)?.classname) ) // If a child node has an available format as className, we wrap it in a // span and render the children with the correct component if (hasFormat) { return next(node.children, embeds, fullRenderOptions) } return (

    {next(node.children, embeds, fullRenderOptions)}

    ) }, [RTETypeEnum.span]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) let propsClassName = className if (className) { const availableClassNames = extractAvailableListClassNames(className) if (availableClassNames.length) { propsClassName = cx(availableClassNames) } } return ( {next(node.children, embeds, fullRenderOptions)} ) }, [RTETypeEnum.div]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) let propsClassName = className if (className) { const availableClassNames = extractAvailableListClassNames(className) if (availableClassNames.length) { propsClassName = cx(availableClassNames) } } return (
    {next(node.children, embeds, fullRenderOptions)}
    ) }, [RTETypeEnum.reference]: ( node: RTENode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { if (!('attrs' in node)) { return null } const type = node.attrs.type if (type === RTEItemTypeEnum.asset) { const imageTypeRegex = /^image\// const isImage = imageTypeRegex.test(node.attrs['asset-type'] as string) if (isImage) { const image = embeds?.[node?.attrs?.['asset-uid'] as string] if (image?.node.__typename === 'SysAsset') { if (image.node.url) { const alt = image?.node?.title ?? node.attrs.alt const { className, ...props } = extractPossibleAttributes( node.attrs ) return (
    {alt
    ) } } } else if (node.attrs['display-type'] === 'link' && node.attrs.href) { const { className, ...props } = extractPossibleAttributes(node.attrs) return ( {next( // Sometimes editors happen to nest a reference inside a link and vice versa. // In that case use the outermost link, i.e. ignore nested links. node.children.flatMap(noNestedLinksOrReferences), embeds, fullRenderOptions )} ) } } if (type === RTEItemTypeEnum.entry) { const entry = embeds?.[node?.attrs?.['entry-uid'] as string] if (entry?.node.__typename === 'ImageContainer') { if (entry.node.image_left && entry.node.image_right) { return ( ) } return null } else if ( entry?.node.__typename === 'AccountPage' || entry?.node.__typename === 'CampaignOverviewPage' || entry?.node.__typename === 'CampaignPage' || entry?.node.__typename === 'CollectionPage' || entry?.node.__typename === 'ContentPage' || entry?.node.__typename === 'DestinationCityPage' || entry?.node.__typename === 'DestinationCountryPage' || entry?.node.__typename === 'DestinationOverviewPage' || entry?.node.__typename === 'HotelPage' || entry?.node.__typename === 'LoyaltyPage' || entry?.node.__typename === 'StartPage' ) { // If entry is not an ImageContainer, it is a page and we return it as a link const { className, ...props } = extractPossibleAttributes(node.attrs) const entryHref = entry.node.url const nodeHref = node.attrs?.locale ? `/${node.attrs.locale}${node.attrs.href ?? node.attrs.url}` : ((node.attrs.href ?? node.attrs.url) as string) if (!entryHref && !nodeHref) { return null } return ( {next( // Sometimes editors happen to nest a reference inside a link and vice versa. // In that case use the outermost link, i.e. ignore nested links. node.children.flatMap(noNestedLinksOrReferences), embeds, fullRenderOptions )} ) } } }, [RTETypeEnum.ImageVault]: (node: RTEImageNode) => { const type = node.type if (!('attrs' in node) || type !== RTETypeEnum.ImageVault) { return null } let image = undefined if ('imageVaultId' in node.attrs && 'fileName' in node.attrs) { image = mapImageVaultAssetResponseToImageVaultAsset( node.attrs as unknown as ImageVaultAssetResponse ) } if ('Name' in node.attrs && 'Id' in node.attrs) { image = mapInsertResponseToImageVaultAsset( node.attrs as unknown as DeprecatedImageVaultAssetResponse ) } if (!image) { return null } const alt = image.meta.alt ?? image.title const caption = image.meta.caption const { className, ...props } = extractPossibleAttributes(node.attrs) return (
    {alt}
    {caption ? (

    {image.meta.caption}

    ) : null}
    ) }, [RTETypeEnum.table]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) return (
    {next(node.children, embeds, fullRenderOptions)}
    ) }, [RTETypeEnum.thead]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { // Override the styling of p tags inside the thead tag const theadChildPRenderOptions = { ...fullRenderOptions, [RTETypeEnum.p]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => ( {next(node.children, embeds, fullRenderOptions)} ), } const props = extractPossibleAttributes(node.attrs) return ( {next(node.children, embeds, theadChildPRenderOptions)} ) }, [RTETypeEnum.tbody]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const props = extractPossibleAttributes(node.attrs) return ( {next(node.children, embeds, fullRenderOptions)} ) }, [RTETypeEnum.tfoot]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const props = extractPossibleAttributes(node.attrs) return ( {next(node.children, embeds, fullRenderOptions)} ) }, [RTETypeEnum.fragment]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { return <>{next(node.children, embeds, fullRenderOptions)} }, [RTETypeEnum.tr]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const props = extractPossibleAttributes(node.attrs) return ( {next(node.children, embeds, fullRenderOptions)} ) }, [RTETypeEnum.th]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const props = extractPossibleAttributes(node.attrs) return ( {next(node.children, embeds, fullRenderOptions)} ) }, [RTETypeEnum.td]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const props = extractPossibleAttributes(node.attrs) return ( {next(node.children, embeds, fullRenderOptions)} ) }, [RTETypeEnum.ul]: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { const { className, ...props } = extractPossibleAttributes(node.attrs) const compatibleClassNames = extractAvailableListClassNames(className) return (
      {next(node.children, embeds, fullRenderOptions)}
    ) }, /** TextNode wrappers */ [RTEMarkType.bold]: (children: React.ReactNode) => { return ( {children} ) }, [RTEMarkType.italic]: (children: React.ReactNode) => { return {children} }, [RTEMarkType.underline]: (children: React.ReactNode) => { return ( {children} ) }, [RTEMarkType.strikethrough]: (children: React.ReactNode) => { return {children} }, [RTEMarkType.inlineCode]: (children: React.ReactNode) => { return {children} }, [RTEMarkType.subscript]: (children: React.ReactNode) => { return {children} }, [RTEMarkType.superscript]: (children: React.ReactNode) => { return {children} }, [RTEMarkType.break]: (children: React.ReactNode) => { return ( <>
    {children} ) }, [RTEMarkType.classnameOrId]: ( children: React.ReactNode, className?: string, id?: string ) => { const props = { id } let propsClassName = className if (!id) { delete props.id } if (className) { const availableClassNames = extractAvailableListClassNames(className) if (availableClassNames.length) { propsClassName = cx(availableClassNames) } } if (className === AvailableParagraphFormatEnum.footnote) { return (

    {children}

    ) } if (className === AvailableParagraphFormatEnum.caption) { return (

    {children}

    ) } if (className === AvailableParagraphFormatEnum['script-1']) { return (

    {children}

    ) } if (className === AvailableParagraphFormatEnum['script-2']) { return (

    {children}

    ) } if (className === AvailableParagraphFormatEnum['subtitle-1']) { return (

    {children}

    ) } if (className === AvailableParagraphFormatEnum['subtitle-2']) { return (

    {children}

    ) } return ( {children} ) }, /** * Contentstack can return something called `default` as seen here in their * own SDK (https://github.com/contentstack/contentstack-utils-javascript/blob/master/src/options/default-node-options.ts#L89) */ default: ( node: RTEDefaultNode, embeds: EmbedByUid, next: RTENext, fullRenderOptions: RenderOptions ) => { return next(node.children, embeds, fullRenderOptions) }, }