Files
web/apps/scandic-web/components/JsonToHtml/renderOptions.tsx
Anton Gunnarsson 80100e7631 Merged in monorepo-step-1 (pull request #1080)
Migrate to a monorepo setup - step 1

* Move web to subfolder /apps/scandic-web

* Yarn + transitive deps

- Move to yarn
- design-system package removed for now since yarn doesn't
support the parameter for token (ie project currently broken)
- Add missing transitive dependencies as Yarn otherwise
prevents these imports
- VS Code doesn't pick up TS path aliases unless you open
/apps/scandic-web instead of root (will be fixed with monorepo)

* Pin framer-motion to temporarily fix typing issue

https://github.com/adobe/react-spectrum/issues/7494

* Pin zod to avoid typ error

There seems to have been a breaking change in the types
returned by zod where error is now returned as undefined
instead of missing in the type. We should just handle this
but to avoid merge conflicts just pin the dependency for
now.

* Pin react-intl version

Pin version of react-intl to avoid tiny type issue where formatMessage
does not accept a generic any more. This will be fixed in a future
commit, but to avoid merge conflicts just pin for now.

* Pin typescript version

Temporarily pin version as newer versions as stricter and results in
a type error. Will be fixed in future commit after merge.

* Setup workspaces

* Add design-system as a monorepo package

* Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN

* Fix husky for monorepo setup

* Update netlify.toml

* Add lint script to root package.json

* Add stub readme

* Fix react-intl formatMessage types

* Test netlify.toml in root

* Remove root toml

* Update netlify.toml publish path

* Remove package-lock.json

* Update build for branch/preview builds


Approved-by: Linus Flood
2025-02-26 10:36:17 +00:00

769 lines
20 KiB
TypeScript

import Image from "@/components/Image"
import Link from "@/components/TempDesignSystem/Link"
import { insertResponseToImageVaultAsset } from "@/utils/imageVault"
import { OpenInNewSmallIcon } from "../Icons"
import ImageContainer from "../ImageContainer"
import Divider from "../TempDesignSystem/Divider"
import Table from "../TempDesignSystem/Table"
import BiroScript from "../TempDesignSystem/Text/BiroScript"
import Body from "../TempDesignSystem/Text/Body"
import Caption from "../TempDesignSystem/Text/Caption"
import Footnote from "../TempDesignSystem/Text/Footnote"
import Subtitle from "../TempDesignSystem/Text/Subtitle"
import Title from "../TempDesignSystem/Text/Title"
import {
hasAvailableParagraphFormat,
hasAvailableULFormat,
makeCssModuleCompatibleClassName,
} from "./utils"
import styles from "./jsontohtml.module.css"
import { ContentEnum } from "@/types/enums/content"
import type { EmbedByUid } from "@/types/transitionTypes/jsontohtml"
import type {
Attributes,
RTEImageVaultAttrs,
} from "@/types/transitionTypes/rte/attrs"
import {
AvailableParagraphFormatEnum,
RTEItemTypeEnum,
RTETypeEnum,
} from "@/types/transitionTypes/rte/enums"
import {
type RTEDefaultNode,
type RTEImageNode,
RTEMarkType,
type RTENext,
type RTENode,
type RTERegularNode,
type RTETextNode,
} from "@/types/transitionTypes/rte/node"
import type { RenderOptions } from "@/types/transitionTypes/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<string, any> = {}
if (attrs.id) {
props.id = attrs.id
}
if (attrs.class) {
props.className = attrs.class
} else if (attrs["class-name"]) {
props.className = attrs["class-name"]
} else if (attrs.classname) {
props.className = attrs.classname
}
if (attrs.style?.["text-align"]) {
props.style = {
textAlign: attrs.style["text-align"],
}
}
return props
}
export const renderOptions: RenderOptions = {
[RTETypeEnum.a]: (
node: RTERegularNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
if (node.attrs.url) {
const props = extractPossibleAttributes(node.attrs)
return (
<Link
{...props}
href={node.attrs.url}
key={node.uid}
target={node.attrs.target ?? "_blank"}
variant="underscored"
color="burgundy"
>
{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
)}
</Link>
)
}
return null
},
[RTETypeEnum.blockquote]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<BiroScript key={node.uid} {...props}>
{next(node.children, embeds, fullRenderOptions)}
</BiroScript>
)
},
[RTETypeEnum.code]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<code key={node.uid} {...props}>
{next(node.children, embeds, fullRenderOptions)}
</code>
)
},
[RTETypeEnum.embed]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const 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 (
<iframe key={node.uid} {...props}>
{next(node.children, embeds, fullRenderOptions)}
</iframe>
)
},
[RTETypeEnum.h1]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<Title key={node.uid} {...props} level="h1">
{next(node.children, embeds, fullRenderOptions)}
</Title>
)
},
[RTETypeEnum.h2]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<Title key={node.uid} {...props} level="h2">
{next(node.children, embeds, fullRenderOptions)}
</Title>
)
},
[RTETypeEnum.h3]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<Title key={node.uid} {...props} level="h3">
{next(node.children, embeds, fullRenderOptions)}
</Title>
)
},
[RTETypeEnum.h4]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<Title key={node.uid} {...props} level="h4">
{next(node.children, embeds, fullRenderOptions)}
</Title>
)
},
[RTETypeEnum.h5]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<Title key={node.uid} {...props} level="h5">
{next(node.children, embeds, fullRenderOptions)}
</Title>
)
},
[RTETypeEnum.hr]: () => {
return <Divider />
},
[RTETypeEnum.li]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
const compatibleClassName = makeCssModuleCompatibleClassName(
props.className,
"ul"
)
return (
<li
key={node.uid}
{...props}
className={`${styles.li} ${compatibleClassName}`}
>
{next(node.children, embeds, fullRenderOptions)}
</li>
)
},
[RTETypeEnum.ol]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
// Set the number of rows dynamically to create even rows for each column. We want the li:s
// to flow with the column, so therefore this is needed.
let numberOfRows: number | undefined
if (node.children.length > 4) {
const half = node.children.length / 2
numberOfRows = Math.ceil(half)
}
return (
<ol
key={node.uid}
{...props}
className={styles.ol}
style={
numberOfRows
? { gridTemplateRows: `repeat(${numberOfRows}, auto)` }
: {}
}
>
{next(node.children, embeds, fullRenderOptions)}
</ol>
)
},
[RTETypeEnum.p]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const 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 (
<Body {...props} key={node.uid}>
{next(node.children, embeds, fullRenderOptions)}
</Body>
)
},
[RTETypeEnum.span]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
let props = extractPossibleAttributes(node.attrs)
const className = props.className
if (className) {
if (hasAvailableULFormat(className)) {
// @ts-ignore: We want to set css modules classNames even if it does not correspond
// to an existing class in the module style sheet. Due to our css modules plugin for
// typescript, we cannot do this without the ts-ignore
props.className = styles[className]
}
}
return (
<span {...props}>{next(node.children, embeds, fullRenderOptions)}</span>
)
},
[RTETypeEnum.reference]: (
node: RTENode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
if ("attrs" in node) {
const type = node.attrs.type
if (type === RTEItemTypeEnum.asset) {
const imageTypeRegex = /^image\//
const isImage = imageTypeRegex.test(node.attrs["asset-type"])
if (isImage) {
const image = embeds?.[node?.attrs?.["asset-uid"]]
if (image?.node.__typename === ContentEnum.blocks.SysAsset) {
if (image.node.url) {
const alt = image?.node?.title ?? node.attrs.alt
const props = extractPossibleAttributes(node.attrs)
props.className = styles.image
return (
<div className={styles.imageContainer}>
<Image
alt={alt}
className={styles.image}
src={image.node.url}
fill
sizes="(min-width: 1367px) 800px, (max-width: 1366px) and (min-width: 1200px) 1200px, 100vw"
{...props}
/>
</div>
)
}
}
} else if (node.attrs["display-type"] === "link" && node.attrs.href) {
return (
<Link
href={node.attrs.href}
key={node.uid}
variant="icon"
textDecoration="underline"
color="burgundy"
target="_blank"
>
{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
)}
<OpenInNewSmallIcon color="burgundy" />
</Link>
)
}
} else if (type === RTEItemTypeEnum.entry) {
const entry = embeds?.[node?.attrs?.["entry-uid"]]
if (entry?.node.__typename === ContentEnum.blocks.ImageContainer) {
if (entry.node.image_left && entry.node.image_right) {
return (
<ImageContainer
leftImage={entry.node.image_left}
rightImage={entry.node.image_right}
/>
)
}
return null
} else if (
entry?.node.__typename === ContentEnum.blocks.AccountPage ||
entry?.node.__typename === ContentEnum.blocks.CollectionPage ||
entry?.node.__typename === ContentEnum.blocks.ContentPage ||
entry?.node.__typename === ContentEnum.blocks.DestinationCityPage ||
entry?.node.__typename ===
ContentEnum.blocks.DestinationCountryPage ||
entry?.node.__typename ===
ContentEnum.blocks.DestinationOverviewPage ||
entry?.node.__typename === ContentEnum.blocks.HotelPage ||
entry?.node.__typename === ContentEnum.blocks.LoyaltyPage ||
entry?.node.__typename === ContentEnum.blocks.StartPage
) {
// If entry is not an ImageContainer, it is a page and we return it as a link
const props = extractPossibleAttributes(node.attrs)
return (
<Link
{...props}
href={entry.node.url}
key={node.uid}
variant="underscored"
color="burgundy"
>
{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
)}
</Link>
)
}
}
}
return null
},
[RTETypeEnum.ImageVault]: (node: RTEImageNode) => {
if ("attrs" in node) {
const type = node.type
if (type === RTETypeEnum.ImageVault) {
const attrs = node.attrs as RTEImageVaultAttrs
let image = undefined
if ("dimensions" in attrs) {
image = attrs
} else {
image = insertResponseToImageVaultAsset(attrs)
}
const alt = image.meta.alt ?? image.title
const props = extractPossibleAttributes(attrs)
return (
<section key={node.uid}>
<div className={styles.imageContainer}>
<Image
alt={alt}
className={styles.image}
src={image.url}
fill
sizes="(min-width: 1367px) 800px, (max-width: 1366px) and (min-width: 1200px) 1200px, 100vw"
focalPoint={image.focalPoint}
{...props}
/>
</div>
<Caption>{image.meta.caption}</Caption>
</section>
)
}
}
return null
},
[RTETypeEnum.table]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<div className={styles.tableContainer}>
<Table key={node.uid} {...props}>
{next(node.children, embeds, fullRenderOptions)}
</Table>
</div>
)
},
[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
) => (
<Body color={"burgundy"} textTransform={"bold"}>
{next(node.children, embeds, fullRenderOptions)}
</Body>
),
}
const props = extractPossibleAttributes(node.attrs)
return (
<Table.THead key={node.uid} {...props}>
{next(node.children, embeds, theadChildPRenderOptions)}
</Table.THead>
)
},
[RTETypeEnum.tbody]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<Table.TBody key={node.uid} {...props}>
{next(node.children, embeds, fullRenderOptions)}
</Table.TBody>
)
},
[RTETypeEnum.tfoot]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<tfoot key={node.uid} {...props}>
{next(node.children, embeds, fullRenderOptions)}
</tfoot>
)
},
[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 (
<Table.TR key={node.uid} {...props}>
{next(node.children, embeds, fullRenderOptions)}
</Table.TR>
)
},
[RTETypeEnum.th]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<Table.TH key={node.uid} {...props}>
{next(node.children, embeds, fullRenderOptions)}
</Table.TH>
)
},
[RTETypeEnum.td]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
return (
<Table.TD key={node.uid} {...props}>
{next(node.children, embeds, fullRenderOptions)}
</Table.TD>
)
},
[RTETypeEnum.ul]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
const props = extractPossibleAttributes(node.attrs)
const compatibleClassName = makeCssModuleCompatibleClassName(
props.className,
"ul"
)
// Set the number of rows dynamically to create even rows for each column. We want the li:s
// to flow with the column, so therefore this is needed.
let numberOfRows: number | undefined
if (node.children.length > 4) {
const half = node.children.length / 2
numberOfRows = Math.ceil(half)
}
return (
<ul
key={node.uid}
{...props}
className={`${styles.ul} ${compatibleClassName}`}
style={
numberOfRows
? {
gridTemplateRows: `repeat(${numberOfRows}, auto)`,
}
: {}
}
>
{next(node.children, embeds, fullRenderOptions)}
</ul>
)
},
/** TextNode wrappers */
[RTEMarkType.bold]: (children: React.ReactNode) => {
return <strong>{children}</strong>
},
[RTEMarkType.italic]: (children: React.ReactNode) => {
return <em>{children}</em>
},
[RTEMarkType.underline]: (children: React.ReactNode) => {
return <u>{children}</u>
},
[RTEMarkType.strikethrough]: (children: React.ReactNode) => {
return <s>{children}</s>
},
[RTEMarkType.inlineCode]: (children: React.ReactNode) => {
return <span>{children}</span>
},
[RTEMarkType.subscript]: (children: React.ReactNode) => {
return <sub>{children}</sub>
},
[RTEMarkType.superscript]: (children: React.ReactNode) => {
return <sup>{children}</sup>
},
[RTEMarkType.break]: (children: React.ReactNode) => {
return (
<>
<br />
{children}
</>
)
},
[RTEMarkType.classnameOrId]: (
children: React.ReactNode,
className?: string,
id?: string
) => {
let props = {
className,
id,
}
if (!className) {
delete props.className
}
if (!id) {
delete props.id
}
if (className) {
if (hasAvailableULFormat(className)) {
// @ts-ignore: We want to set css modules classNames even if it does not correspond
// to an existing class in the module style sheet. Due to our css modules plugin for
// typescript, we cannot do this without the ts-ignore
props.className = styles[className]
}
}
if (className === AvailableParagraphFormatEnum.footnote) {
return (
<Footnote key={id} {...props}>
{children}
</Footnote>
)
}
if (className === AvailableParagraphFormatEnum.caption) {
return (
<Caption key={id} {...props}>
{children}
</Caption>
)
}
if (className === AvailableParagraphFormatEnum["script-1"]) {
return (
<BiroScript key={id} type="one" {...props}>
{children}
</BiroScript>
)
}
if (className === AvailableParagraphFormatEnum["script-2"]) {
return (
<BiroScript key={id} type="two" {...props}>
{children}
</BiroScript>
)
}
if (className === AvailableParagraphFormatEnum["subtitle-1"]) {
return (
<Subtitle key={id} {...props} type="one">
{children}
</Subtitle>
)
}
if (className === AvailableParagraphFormatEnum["subtitle-2"]) {
return (
<Subtitle key={id} {...props} type="two">
{children}
</Subtitle>
)
}
return (
<span key={id} {...props}>
{children}
</span>
)
},
/**
* 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)
},
}