tobias cb0a2f77a8
Some checks failed
continuous-integration/drone/push Build is failing
fix build errors
2024-06-24 17:34:40 +02:00

214 lines
6.3 KiB
TypeScript

/* eslint-disable react/no-children-prop */
import normalizePath from '@/utils/normalizePath'
import clsx from 'clsx'
import Link from 'next/link'
import React, { CSSProperties, type FC, type ReactElement } from 'react'
import Image from 'next/image'
import {
IS_BOLD,
IS_CODE,
IS_ITALIC,
IS_STRIKETHROUGH,
IS_SUBSCRIPT,
IS_SUPERSCRIPT,
IS_UNDERLINE,
} from './RichTextNodeFormat'
type SerializedLexicalNode = {
children?: SerializedLexicalNode[]
direction: string
format: number
indent?: string | number
type: string
version: number
style?: string
mode?: string
text?: string
[other: string]: any
}
type TextComponentProps = {
children: ReactElement | string
format: number
}
const getLinkForDocument = (doc: any, locale?: string): string => {
let path = doc?.path
if (!path || path.startsWith('/home') || path === '/' || path === '') path = '/'
return normalizePath(`/${locale}${path}`)
}
function gcd(a: number, b: number): number {
return b === 0 ? a : gcd(b, a % b)
}
function calculateAspectRatio(width: number, height: number): string {
const divisor = gcd(width, height)
const simplifiedWidth = width / divisor
const simplifiedHeight = height / divisor
return `${simplifiedWidth}x${simplifiedHeight}`
}
const TextComponent: FC<TextComponentProps> = ({ children, format }) => {
const formatFunctions: { [key: number]: (child: ReactElement | string) => ReactElement } = {
[IS_BOLD]: (child) => <strong>{child}</strong>,
[IS_ITALIC]: (child) => <em>{child}</em>,
[IS_STRIKETHROUGH]: (child) => <del>{child}</del>,
[IS_UNDERLINE]: (child) => <u>{child}</u>,
[IS_CODE]: (child) => <code>{child}</code>,
[IS_SUBSCRIPT]: (child) => <sub>{child}</sub>,
[IS_SUPERSCRIPT]: (child) => <sup>{child}</sup>,
}
const formattedText = Object.entries(formatFunctions).reduce(
(formattedText, [key, formatter]) => {
return format & Number(key) ? formatter(formattedText) : formattedText
},
children,
)
return <>{formattedText}</>
}
const SerializedLink: React.FC<{
node: SerializedLexicalNode
locale: string
children: React.JSX.Element | null
}> = ({ node, locale, children }) => {
const { doc, url, newTab, linkType } = node.fields as any
const document = doc?.value
const href = linkType === 'custom' ? url : getLinkForDocument(document, locale)
const target = newTab ? '_blank' : undefined
return (
<Link href={href} target={target}>
{children}
</Link>
)
}
const getNodeClassNames = (node: SerializedLexicalNode) => {
const attributes: Record<string, any> = {}
if (!node) return attributes
let classNames = ''
if (String(node?.format).toString()?.includes('left') && node.direction !== 'ltr')
classNames += 'text-left '
if (String(node?.format).toString()?.includes('center')) classNames += 'text-center '
if (String(node?.format).toString()?.includes('right') && node.direction !== 'rtl')
classNames += 'text-right '
if (classNames.length > 0) attributes.className = classNames.trim()
const indent = parseInt(`${node?.indent || 0}`)
if (!isNaN(indent) && indent !== 0) {
attributes.style = { '--indent': `${indent * 10}px` } as CSSProperties
attributes.className = `${attributes.className ?? ''} ml-[--indent]`.trim()
}
return attributes
}
const LexicalContent: React.FC<{
childrenNodes: SerializedLexicalNode[]
locale: string
className?: string
lazyLoadImages: boolean
}> = ({ childrenNodes, locale, lazyLoadImages = false }) => {
if (!Array.isArray(childrenNodes)) return null
const renderedChildren = childrenNodes.map((node, ix) => {
if (!node) return null
const attributes = getNodeClassNames(node || '')
if (node.type === 'text') {
return (
<TextComponent key={ix} format={node.format}>
<>
{Object.keys(attributes).length > 0 && <span {...attributes}>{node?.text || ''}</span>}
{(Object.keys(attributes).length === 0 && node?.text) || ''}
</>
</TextComponent>
)
}
const serializedChildren = node.children ? (
<LexicalContent
key={ix}
childrenNodes={node.children}
locale={locale}
lazyLoadImages={lazyLoadImages}
/>
) : null
switch (node.type) {
case 'linebreak':
return <br key={ix} />
case 'link':
return <SerializedLink key={ix} node={node} locale={locale} children={serializedChildren} />
case 'list':
const ListTag = node.listType === 'bullet' ? 'ul' : 'ol'
attributes.className = clsx(
attributes.className,
'mb-4 pl-8',
ListTag === 'ol' ? 'list-decimal' : 'list-disc',
)
return (
<ListTag key={ix} {...attributes}>
{serializedChildren}
</ListTag>
)
case 'listitem':
return (
<li key={ix} {...attributes}>
{serializedChildren}
</li>
)
case 'heading':
const HeadingTag = node.tag as keyof React.JSX.IntrinsicElements
return (
<HeadingTag key={ix} {...attributes}>
{serializedChildren}
</HeadingTag>
)
case 'quote':
return (
<blockquote key={ix} {...attributes}>
{serializedChildren}
</blockquote>
)
case 'upload':
const upload = node?.value
if (!upload) return null
const imageAspectRatio = calculateAspectRatio(upload.width, upload.height)
return (
<Image
key={ix}
width={upload.width}
height={upload.height}
src={upload?.url}
loading={lazyLoadImages ? 'lazy' : 'eager'}
fetchPriority={lazyLoadImages ? 'low' : 'high'}
sizes="(max-width: 768px) 65ch, 100vw"
className="max-w-[calc(100%+40px)] translate-x-[-20px]"
alt={upload?.alt || upload.filename}
/>
)
default:
if (
Array.isArray(serializedChildren?.props?.childrenNodes) &&
serializedChildren?.props?.childrenNodes.length === 0
)
return <br key={ix} />
return (
<p key={ix} {...attributes}>
{serializedChildren}
</p>
)
}
})
return <>{renderedChildren.filter((node) => node !== null)}</>
}
export default LexicalContent