214 lines
6.3 KiB
TypeScript
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
|