本指南涵盖将Plate编辑器内容转换为HTML(serializeHtml)以及将HTML解析回Plate格式(editor.api.html.deserialize)的操作。
HTML
'text/plain' data. While this is suitable for certain scenarios, there are times when you want users to be able to paste content while preserving its formatting. To achieve this, your editor should be capable of handling 'text/html' data.'use client';
import * as React from 'react';
import { Plate, usePlateEditor } from 'platejs/react';
import { EditorKit } from '@/components/editor/editor-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';
import { DEMO_VALUES } from './values/demo-values';
export default function Demo({ id }: { id: string }) {
const editor = usePlateEditor({
plugins: EditorKit,
value: DEMO_VALUES[id],
});
return (
<Plate editor={editor}>
<EditorContainer variant="demo">
<Editor />
</EditorContainer>
</Plate>
);
}


套件使用
安装
启用HTML序列化的最快方式是使用BaseEditorKit,它包含预配置的基础插件,支持大多数常见元素和标记的HTML转换。
import { BaseAlignKit } from './plugins/align-base-kit';
import { BaseBasicBlocksKit } from './plugins/basic-blocks-base-kit';
import { BaseBasicMarksKit } from './plugins/basic-marks-base-kit';
import { BaseCalloutKit } from './plugins/callout-base-kit';
import { BaseCodeBlockKit } from './plugins/code-block-base-kit';
import { BaseColumnKit } from './plugins/column-base-kit';
import { BaseCommentKit } from './plugins/comment-base-kit';
import { BaseDateKit } from './plugins/date-base-kit';
import { BaseFontKit } from './plugins/font-base-kit';
import { BaseLineHeightKit } from './plugins/line-height-base-kit';
import { BaseLinkKit } from './plugins/link-base-kit';
import { BaseListKit } from './plugins/list-base-kit';
import { MarkdownKit } from './plugins/markdown-kit';
import { BaseMathKit } from './plugins/math-base-kit';
import { BaseMediaKit } from './plugins/media-base-kit';
import { BaseMentionKit } from './plugins/mention-base-kit';
import { BaseSuggestionKit } from './plugins/suggestion-base-kit';
import { BaseTableKit } from './plugins/table-base-kit';
import { BaseTocKit } from './plugins/toc-base-kit';
import { BaseToggleKit } from './plugins/toggle-base-kit';
export const BaseEditorKit = [
...BaseBasicBlocksKit,
...BaseCodeBlockKit,
...BaseTableKit,
...BaseToggleKit,
...BaseTocKit,
...BaseMediaKit,
...BaseCalloutKit,
...BaseColumnKit,
...BaseMathKit,
...BaseDateKit,
...BaseLinkKit,
...BaseMentionKit,
...BaseBasicMarksKit,
...BaseFontKit,
...BaseListKit,
...BaseAlignKit,
...BaseLineHeightKit,
...BaseCommentKit,
...BaseSuggestionKit,
...MarkdownKit,
];
添加套件
import { createSlateEditor, serializeHtml } from 'platejs';
import { BaseEditorKit } from '@/components/editor/editor-base-kit';
const editor = createSlateEditor({
plugins: BaseEditorKit,
value: [
{ type: 'h1', children: [{ text: 'Hello World' }] },
{ type: 'p', children: [{ text: '此内容将被序列化为HTML。' }] },
],
});
// 序列化为HTML
const html = await serializeHtml(editor);示例
查看完整的服务端HTML生成示例:
import * as React from 'react';
import { cva } from 'class-variance-authority';
import fs from 'node:fs/promises';
import path from 'node:path';
import {
type Value,
createStaticEditor,
normalizeNodeId,
serializeHtml,
} from 'platejs';
import { BaseEditorKit } from '@/components/editor/editor-base-kit';
import {
EditorClient,
EditorViewClient,
ExportHtmlButton,
HtmlIframe,
} from '@/components/editor/slate-to-html';
import { alignValue } from '@/registry/examples/values/align-value';
import { basicBlocksValue } from '@/registry/examples/values/basic-blocks-value';
import { basicMarksValue } from '@/registry/examples/values/basic-marks-value';
import { columnValue } from '@/registry/examples/values/column-value';
import { dateValue } from '@/registry/examples/values/date-value';
import { discussionValue } from '@/registry/examples/values/discussion-value';
import { equationValue } from '@/registry/examples/values/equation-value';
import { fontValue } from '@/registry/examples/values/font-value';
import { indentValue } from '@/registry/examples/values/indent-value';
import { lineHeightValue } from '@/registry/examples/values/line-height-value';
import { linkValue } from '@/registry/examples/values/link-value';
import { listValue } from '@/registry/examples/values/list-value';
import { mediaValue } from '@/registry/examples/values/media-value';
import { mentionValue } from '@/registry/examples/values/mention-value';
import { tableValue } from '@/registry/examples/values/table-value';
import { tocPlaygroundValue } from '@/registry/examples/values/toc-value';
import { createHtmlDocument } from '@/lib/create-html-document';
import { EditorStatic } from '@/components/ui/editor-static';
const getCachedTailwindCss = React.cache(async () => {
const cssPath = path.join(process.cwd(), 'public', 'tailwind.css');
return await fs.readFile(cssPath, 'utf8');
});
export default async function SlateToHtmlBlock() {
const createValue = (): Value =>
normalizeNodeId([
...basicBlocksValue,
...basicMarksValue,
...tocPlaygroundValue,
...linkValue,
...tableValue,
...equationValue,
...columnValue,
...mentionValue,
...dateValue,
...fontValue,
...discussionValue,
...alignValue,
...lineHeightValue,
...indentValue,
...listValue,
...mediaValue,
]);
const editor = createStaticEditor({
plugins: BaseEditorKit,
value: createValue(),
});
const tailwindCss = await getCachedTailwindCss();
const katexCDN = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/katex.css" integrity="sha384-9PvLvaiSKCPkFKB1ZsEoTjgnJn+O3KvEwtsz37/XrkYft3DTk2gHdYvd9oWgW3tV" crossorigin="anonymous">`;
// const cookieStore = await cookies();
// const theme = cookieStore.get('theme')?.value;
const theme = 'light';
// Get the editor content HTML using EditorStatic
const editorHtml = await serializeHtml(editor, {
editorComponent: EditorStatic,
props: { style: { padding: '0 calc(50% - 350px)', paddingBottom: '' } },
});
// Create the full HTML document
const html = createHtmlDocument({
editorHtml,
katexCDN,
tailwindCss,
theme,
});
return (
<div className="grid grid-cols-3 px-4">
<div className="p-2">
<h3 className={headingVariants()}>Editor</h3>
<EditorClient value={createValue()} />
</div>
<div className="p-2">
<h3 className={headingVariants()}>EditorView</h3>
<EditorViewClient value={createValue()} />
</div>
<div className="relative p-2">
<h3 className={headingVariants()}>HTML Iframe</h3>
<ExportHtmlButton
className="absolute top-10 right-0"
html={html}
serverTheme={theme}
/>
<HtmlIframe
className="h-[7500px] w-full"
html={html}
serverTheme={theme}
/>
</div>
</div>
);
}
const headingVariants = cva(
'group mt-8 scroll-m-20 font-heading text-xl font-semibold tracking-tight'
);
Plate转HTML
将Plate编辑器内容(Plate节点)转换为HTML字符串。这通常在服务端完成。
关键服务端限制
在服务端环境(Node.js, RSC)中使用serializeHtml或其他Plate工具时,不得从任何platejs*包的/react子路径导入。始终使用基础导入(例如使用@platejs/basic-nodes而非@platejs/basic-nodes/react)。
这意味着服务端编辑器实例应使用platejs中的createSlateEditor,而非platejs/react中的usePlateEditor或createPlateEditor。
基础用法
提供服务端编辑器实例并在编辑器创建时配置Plate组件。
import { createSlateEditor, serializeHtml } from 'platejs'; // 基础导入
// 导入基础插件(不从/react路径导入)
import { BaseHeadingPlugin } from '@platejs/basic-nodes';
// 导入用于渲染的静态组件
import { ParagraphElementStatic } from '@/components/ui/paragraph-node-static';
import { HeadingElementStatic } from '@/components/ui/heading-node-static';
// 对于带样式的静态输出,可以使用像EditorStatic这样的包装器
import { EditorStatic } from '@/components/ui/editor-static';
// 将插件键映射到其静态渲染组件
const components = {
p: ParagraphElementStatic, // 'p'是段落的默认键
h1: HeadingElementStatic,
// ... 为所有元素和标记添加映射
};
// 创建带组件的服务端编辑器实例
const editor = createSlateEditor({
plugins: [
BaseHeadingPlugin, // 标题基础插件
// ... 添加与内容相关的所有其他基础插件
],
components,
});
async function getMyHtml() {
// 示例:在服务端编辑器上设置内容
editor.children = [
{ type: 'h1', children: [{text: '我的标题'}] },
{ type: 'p', children: [{text: '我的内容。'}] }
];
const html = await serializeHtml(editor, {
// 可选:使用像EditorStatic这样的自定义包装器进行样式设置
// editorComponent: EditorStatic,
// props: { variant: 'none', className: 'p-4 m-4 border' },
});
return html;
}序列化HTML的样式设置
serializeHtml仅返回编辑器内容本身的HTML。如果使用带样式的组件(如EditorStatic或具有特定类的自定义静态组件),必须确保最终显示HTML的上下文中包含必要的CSS。
这通常意味着将序列化的HTML包装在包含样式表的完整HTML文档中:
// ...(来自generate-html.ts的先前设置)
async function getFullHtmlDocument() {
const editorHtmlContent = await getMyHtml(); // 来自之前的示例
const fullHtml = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/path/to/your-global-styles.css" />
<link rel="stylesheet" href="/path/to/tailwind-or-component-styles.css" />
<title>序列化内容</title>
</head>
<body>
<div class="my-document-wrapper prose dark:prose-invert">
${editorHtmlContent}
</div>
</body>
</html>`;
return fullHtml;
}仅静态输出
序列化过程将Plate节点转换为静态HTML。交互功能(React事件处理程序、客户端钩子)或依赖浏览器API的组件在序列化输出中将无法工作。
使用静态组件
对于服务端序列化,必须使用组件的静态版本(无仅客户端代码,无React钩子如useEffect或useState)。
参考静态渲染指南获取为Plate元素和标记创建服务端安全静态组件的详细说明。
import React from 'react';
import type { SlateElementProps } from 'platejs';
// 示例静态段落组件
export function ParagraphElementStatic(props: SlateElementProps) {
return (
<SlateElement {...props} className={cn('m-0 px-0 py-1')}>
{props.children}
</SlateElement>
);
}HTML转Plate
HTML反序列化器允许将HTML内容(字符串或DOM元素)转换回Plate格式。这支持往返转换,在存在对应插件规则的情况下保留结构、格式和属性。
基础用法
在客户端Plate编辑器上下文中使用editor.api.html.deserialize。
import { PlateEditor, usePlateEditor } from 'platejs/react'; // 客户端专用的React导入
// 导入表示HTML内容所需的所有Plate插件
import { HeadingPlugin } from '@platejs/basic-nodes/react';
// ... 以及粗体、斜体、表格、列表等的插件
function MyHtmlImporter({ htmlString }: { htmlString: string }) {
const editor = usePlateEditor({
plugins: [
HeadingPlugin, // 用于<h1>, <h2>等
// ... 包含与预期解析的HTML对应的所有插件
],
});
const handleImport = () => {
const slateValue = editor.api.html.deserialize(htmlString);
editor.tf.setValue(slateValue);
};
// ... 渲染编辑器及触发handleImport的按钮 ...
return <button onClick={handleImport}>导入HTML</button>;
}客户端操作
使用editor.api.html.deserialize的HTML反序列化通常是客户端操作,因为它与配置了React组件和插件的实时Plate编辑器实例交互。
插件反序列化规则概览
每个Plate插件可以定义规则,说明在反序列化期间如何解释特定的HTML标签、样式和属性。下表是常见HTML结构及通常负责它们的Plate插件的摘要。
| HTML元素/样式 | Plate插件(典型) | 备注 |
|---|---|---|
<strong>, <b>, font-weight: 600,700,bold | BoldPlugin | 转换为bold: true标记。 |
<em>, <i>, font-style: italic | ItalicPlugin | 转换为italic: true标记。 |
<u>, text-decoration: underline | UnderlinePlugin | 转换为underline: true标记。 |
<s>, <del>, <strike>, text-decoration: line-through | StrikethroughPlugin | 转换为strikethrough: true标记。 |
<sub>, vertical-align: sub | SubscriptPlugin | 转换为subscript: true标记。 |
<sup>, vertical-align: super | SuperscriptPlugin | 转换为superscript: true标记。 |
<code> (不在<pre>中), font-family: Consolas | CodePlugin | 转换为code: true标记(内联代码)。 |
<kbd> | KbdPlugin | 转换为kbd: true标记。 |
<p> | ParagraphPlugin | 转换为段落元素。 |
<h1> - <h6> | HeadingPlugin | 转换为对应的标题元素(h1 - h6)。 |
<ul> | ListPlugin (经典) | 转换为无序列表(ul类型)。项目变为li。 |
<ol> | ListPlugin (经典) | 转换为有序列表(ol类型)。项目变为li。 |
<li> (在<ul>或<ol>中) | ListPlugin (经典) | 转换为列表项(li类型),带有lic(列表项内容)子项。 |
<li> (带aria-level缩进) | ListPlugin | 转换为带indent和listStyleType属性的段落。 |
<blockquote> | BlockquotePlugin | 转换为blockquote元素。 |
<pre> (通常内部有<code>) | CodeBlockPlugin | 转换为code_block元素。内容分割为code_line。 |
<hr> | HorizontalRulePlugin | 转换为水平线元素。 |
<a> | LinkPlugin | 转换为链接元素(a类型)带url属性。 |
<img> | ImagePlugin | 转换为图像元素(img类型)带url属性。 |
<iframe> | MediaEmbedPlugin | 转换为媒体嵌入元素,尝试解析URL。 |
<table> | TablePlugin | 转换为table元素。 |
<tr> | TablePlugin | 转换为tr(表格行)元素。 |
<td> | TablePlugin | 转换为td(表格单元格)元素。 |
<th> | TablePlugin | 转换为th(表格标题单元格)元素。 |
style="background-color: ..." | FontColorPlugin | 转换为backgroundColor标记。(插件名称可能看似相反) |
style="color: ..." | FontColorPlugin | 转换为color标记。 |
style="font-family: ..." | FontFamilyPlugin | 转换为fontFamily标记。 |
style="font-size: ..." | FontSizePlugin | 转换为fontSize标记。 |
style="font-weight: ..." (非粗体值) | FontWeightPlugin | 为非标准粗体值转换为fontWeight标记。 |
<mark> | HighlightPlugin | 转换为highlight: true标记。 |
style="text-align: ..." | TextAlignPlugin | 在块元素上设置align属性。 |
style="line-height: ..." | LineHeightPlugin | 在块元素上设置lineHeight属性。 |
插件配置
确切的Plate类型(例如ParagraphPlugin.key与'p')取决于插件的配置方式。表格显示典型关联。确保编辑器包含对应的Plate插件以使这些规则生效。
插件中的反序列化属性
插件可以在其parsers.html.deserializer配置中定义如何处理HTML反序列化:
parse: 函数({ editor, element, getOptions, ... }) => Partial<SlateNode>,接收HTML元素并返回部分Plate节点。这是主要转换逻辑所在。query: 可选函数({ element, getOptions }) => boolean,确定是否应考虑当前HTML元素的反序列化规则。rules: 规则对象数组,每个定义匹配HTML元素的条件:validNodeName: 字符串或字符串数组,用于匹配HTML标签名(例如'P',['STRONG', 'B'])。validAttribute: 对象或对象数组,指定必需的属性名和/或值(例如{ align: ['left', 'center'] })。validClassName: 字符串或字符串数组,用于匹配CSS类名。validStyle: 对象或对象数组,指定必需的CSS样式属性和/或值(例如{ fontWeight: ['600', '700', 'bold'] })。
isElement: 布尔值,true表示插件将HTML元素反序列化为Plate Element节点。isLeaf: 布尔值,true表示插件将HTML元素或样式反序列化为Text节点上的Plate Leaf(标记)。attributeNames: HTML属性名数组,其值应保留在结果Plate节点的node.attributes属性上。withoutChildren: 布尔值,如果true,HTML元素的子节点不会被convertHtmlAttributes处理。
自定义反序列化行为
可以扩展插件以修改其HTML解析逻辑。这对于支持非标准HTML属性或结构非常有用。
import { CodeBlockPlugin } from '@platejs/code-block/react';
import { CodeLinePlugin } from '@platejs/code-block'; // 如果需要基础
const MyCustomCodeBlockPlugin = CodeBlockPlugin.configure({
parsers: {
html: {
deserializer: {
// 继承大多数规则和属性,然后覆盖或添加
...CodeBlockPlugin.parsers.html.deserializer,
parse: ({ element, editor }) => { // 可能需要editor用于getType
const language = element.getAttribute('data-custom-lang') || element.className.match(/language-(?<lang>[^\s]+)/)?.groups?.lang;
const textContent = element.textContent || '';
const lines = textContent.split('\n');
return {
type: CodeBlockPlugin.key, // 或editor.getType(CodeBlockPlugin.key)
lang: language,
code: textContent, // 示例:存储完整代码字符串
children: lines.map((line) => ({
type: editor.getType(CodeLinePlugin.key),
children: [{ text: line }],
})),
};
},
rules: [
// 如果需要,继承现有规则
...(CodeBlockPlugin.parsers.html.deserializer.rules || []),
// 添加基于自定义属性的新规则
{ validAttribute: { 'data-custom-lang': true } },
],
},
},
},
});
// 然后在编辑器配置中使用MyCustomCodeBlockPlugin。此示例自定义CodeBlockPlugin以查找data-custom-lang属性或language-*类来确定代码语言。
高级反序列化示例(ListPlugin)
ListPlugin展示了一个更复杂的反序列化场景,它将HTML列表结构(<li>元素)转换为Plate中的缩进段落,使用aria-level确定缩进。
以下是其反序列化逻辑的概念性展示:
// 来自ListPlugin的简化概念
export const ListPluginConfig = {
// ... 其他配置 ...
parsers: {
html: {
deserializer: {
isElement: true,
// query: ({ element }) => hasListAncestor(element), // 示例条件
parse: ({ editor, element }) => ({
type: 'p', // 将<li>转换为<p>
indent: Number(element.getAttribute('aria-level') || '1'),
listStyleType: element.style.listStyleType || undefined,
// 子节点在此节点创建后由Plate的默认反序列化器处理
}),
rules: [
{ validNodeName: 'LI' }, // 仅适用于<li>元素
],
},
},
},
};这展示了插件如何完全将HTML结构重新解释为不同的Plate表示。
API 参考
serializeHtml(editor, options)
将 editor.children(或提供的 value)中的 Plate 节点转换为 HTML 字符串。此函数通常在服务端使用。
用于在静态渲染期间包装整个编辑器内容的 React 组件。默认为 PlateStatic。
该组件接收 editor、value 以及此处传递的任何 props。
传递给 editorComponent 的 props。P 默认为 PlateStaticProps。
要序列化的 Plate 节点。如果未提供,将使用 editor.children。
如果 stripClassNames 为 true,则保留的类名前缀数组。如果 stripClassNames 为 true,null 将保留所有类名。默认值:['slate-', 'line-clamp']。
如果为 true,则从输出 HTML 中删除所有类名,但保留 preserveClassNames 中列出的前缀的类名。默认值:true。
如果为 true,则从输出 HTML 中删除所有 data-* 属性。默认值:true。
api.html.deserialize(options)
将 HTML 字符串或 HTMLElement 解析为 Plate Value(Descendant 节点数组)。这通常在客户端与完全配置的 Plate 编辑器一起使用。
下一步
- 探索静态渲染指南以创建服务端安全的组件。
- 查看各个插件文档以了解特定的 HTML 序列化/反序列化功能和默认规则。
- 查看服务端 HTML 生成示例。