From 02ae938c238c9d18448d17a8ec92c0edd8c17463 Mon Sep 17 00:00:00 2001
From: Bertrand Yuan <bert.yuan@outlook.com>
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 (
+    <Fragment>
+      {nodes?.map((node, index): JSX.Element | null => {
+        if (node == null) {
+          return null
+        }
+
+        if (node.type === 'text') {
+          let text = <React.Fragment key={index}>{node.text}</React.Fragment>
+          const format = node.format || 0
+
+          if (format & IS_BOLD) {
+            text = <strong key={index}>{text}</strong>
+          }
+          if (format & IS_ITALIC) {
+            text = <em key={index}>{text}</em>
+          }
+          if (format & IS_STRIKETHROUGH) {
+            text = (
+              <span key={index} style={{ textDecoration: 'line-through' }}>
+                {text}
+              </span>
+            )
+          }
+          if (format & IS_UNDERLINE) {
+            text = (
+              <span key={index} style={{ textDecoration: 'underline' }}>
+                {text}
+              </span>
+            )
+          }
+          if (format & IS_CODE) {
+            text = <code key={index}>{node.text}</code>
+          }
+          if (format & IS_SUBSCRIPT) {
+            text = <sub key={index}>{text}</sub>
+          }
+          if (format & IS_SUPERSCRIPT) {
+            text = <sup key={index}>{text}</sup>
+          }
+
+          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 <br key={index} />
+          }
+          case 'paragraph': {
+            return <p key={index}>{serializedChildren}</p>
+          }
+          case 'heading': {
+            const Tag = node?.tag || 'h2'
+            return <Tag key={index}>{serializedChildren}</Tag>
+          }
+          case 'list': {
+            const Tag = node?.tag || 'ul'
+            return <Tag key={index}>{serializedChildren}</Tag>
+          }
+          case 'listitem': {
+            if (node?.checked != null) {
+              return (
+                <li
+                  aria-checked={node.checked ? 'true' : 'false'}
+                  key={index}
+                  role="checkbox"
+                  tabIndex={-1}
+                  value={node?.value}
+                >
+                  <input
+                    type="checkbox"
+                    checked={node.checked}
+                    readOnly
+                    className="mr-2"
+                  />
+                  {serializedChildren}
+                </li>
+              )
+            } else {
+              return (
+                <li key={index} value={node?.value}>
+                  {serializedChildren}
+                </li>
+              )
+            }
+          }
+          case 'quote': {
+            return <blockquote key={index}>{serializedChildren}</blockquote>
+          }
+          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 (
+                <Link key={index} href={href}>
+                  {serializedChildren}
+                </Link>
+              )
+            }
+
+            return (
+              <a
+                key={index}
+                href={fields?.url || '#'}
+                target={fields?.newTab ? '_blank' : undefined}
+                rel={fields?.newTab ? 'noopener noreferrer' : undefined}
+              >
+                {serializedChildren}
+              </a>
+            )
+          }
+          case 'code': {
+            // 代码块
+            return (
+              <pre key={index} className="overflow-x-auto">
+                <code>{serializedChildren}</code>
+              </pre>
+            )
+          }
+          case 'horizontalrule': {
+            return <hr key={index} />
+          }
+          default:
+            // 如果有子节点，递归渲染
+            if (node.children) {
+              return <Fragment key={index}>{serializedChildren}</Fragment>
+            }
+            return null
+        }
+      })}
+    </Fragment>
+  )
+}
-- 
cgit v1.2.3

