From 02ae938c238c9d18448d17a8ec92c0edd8c17463 Mon Sep 17 00:00:00 2001 From: Bertrand Yuan Date: Tue, 16 Dec 2025 00:12:49 +0800 Subject: feat(back-end): introduce payload Payload is the next.js Headless CMS and App Framework, I would like to pick it up and modify it as it is MIT licensed. Many features in Payload is not applicable for our project. So, I modify it so that it is light and clear. --- src/components/rich-text/serialize.tsx | 201 +++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/components/rich-text/serialize.tsx (limited to 'src/components/rich-text/serialize.tsx') diff --git a/src/components/rich-text/serialize.tsx b/src/components/rich-text/serialize.tsx new file mode 100644 index 0000000..d75c40f --- /dev/null +++ b/src/components/rich-text/serialize.tsx @@ -0,0 +1,201 @@ +import React, { Fragment, type JSX } from 'react' +import Link from 'next/link' +import { + IS_BOLD, + IS_CODE, + IS_ITALIC, + IS_STRIKETHROUGH, + IS_SUBSCRIPT, + IS_SUPERSCRIPT, + IS_UNDERLINE, +} from './node-format' + +// Lexical 节点类型 +interface LexicalNode { + type: string + format?: number + text?: string + tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'ul' | 'ol' + listType?: 'bullet' | 'number' | 'check' + checked?: boolean + value?: number + children?: LexicalNode[] + fields?: { + linkType?: 'internal' | 'custom' + url?: string + newTab?: boolean + doc?: { + value?: { + slug?: string + } + relationTo?: string + } + } + language?: string + version?: number +} + +type Props = { + nodes: LexicalNode[] +} + +export function serializeLexical({ nodes }: Props): JSX.Element { + return ( + + {nodes?.map((node, index): JSX.Element | null => { + if (node == null) { + return null + } + + if (node.type === 'text') { + let text = {node.text} + const format = node.format || 0 + + if (format & IS_BOLD) { + text = {text} + } + if (format & IS_ITALIC) { + text = {text} + } + if (format & IS_STRIKETHROUGH) { + text = ( + + {text} + + ) + } + if (format & IS_UNDERLINE) { + text = ( + + {text} + + ) + } + if (format & IS_CODE) { + text = {node.text} + } + if (format & IS_SUBSCRIPT) { + text = {text} + } + if (format & IS_SUPERSCRIPT) { + text = {text} + } + + return text + } + + // 处理子节点 + const serializedChildrenFn = (node: LexicalNode): JSX.Element | null => { + if (node.children == null) { + return null + } else { + // 处理 checkbox list + if (node?.type === 'list' && node?.listType === 'check') { + for (const item of node.children) { + if ('checked' in item) { + if (!item?.checked) { + item.checked = false + } + } + } + } + return serializeLexical({ nodes: node.children }) + } + } + + const serializedChildren = 'children' in node ? serializedChildrenFn(node) : '' + + switch (node.type) { + case 'linebreak': { + return
+ } + case 'paragraph': { + return

{serializedChildren}

+ } + case 'heading': { + const Tag = node?.tag || 'h2' + return {serializedChildren} + } + case 'list': { + const Tag = node?.tag || 'ul' + return {serializedChildren} + } + case 'listitem': { + if (node?.checked != null) { + return ( +
  • + + {serializedChildren} +
  • + ) + } else { + return ( +
  • + {serializedChildren} +
  • + ) + } + } + case 'quote': { + return
    {serializedChildren}
    + } + case 'link': { + const fields = node.fields + + if (fields?.linkType === 'internal' && fields?.doc?.value?.slug) { + const href = + fields.doc.relationTo === 'posts' + ? `/posts/${fields.doc.value.slug}` + : `/${fields.doc.value.slug}` + + return ( + + {serializedChildren} + + ) + } + + return ( + + {serializedChildren} + + ) + } + case 'code': { + // 代码块 + return ( +
    +                {serializedChildren}
    +              
    + ) + } + case 'horizontalrule': { + return
    + } + default: + // 如果有子节点,递归渲染 + if (node.children) { + return {serializedChildren} + } + return null + } + })} +
    + ) +} -- cgit v1.2.3