Plate 如何用装饰高亮预览 Markdown 语法而不反序列化为节点?
2026/9/15 10:15:13 网站建设 项目流程

Plate 如何用装饰高亮预览 Markdown 语法而不反序列化为节点?

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

如果你希望编辑器把**加粗**## 标题这类 Markdown 语法原样保留为纯文本,同时在视觉上做出对应的强调(加粗、大字号标题、引用竖线等),又不想把它们解析成 Plate 的 heading/bold 节点,Plate 官方仓库里的 Preview Markdown 示例给出了一条现成的路径:用 Slate 的装饰(decoration)机制给文本节点算出"样式范围",再用自定义 leaf 渲染器给这些范围上色。官方示例文档对此的描述是:

This example previews Markdown syntax with Slate decorations. It does not deserialize Markdown into Plate nodes; it keeps the text as text and styles matching ranges with a custom leaf renderer.

也就是说,装饰只"标注范围 + 套样式",内容数据始终是一段普通文本。

装饰预览与反序列化是两条路

官方示例文档(preview-markdown.mdx)在结尾给出了明确的选型判断:

  • 本例的装饰预览:当编辑器需要让用户看到原始 Markdown 字符时使用("Use this pattern when the editor should keep raw Markdown characters visible")。
  • Markdown 插件的反序列化:当编辑器需要把 Markdown 文本转换成 Plate 节点时使用("Use Markdown when the editor should convert Markdown text into Plate nodes")。文档的 Related 一节指向 Markdown 章节(覆盖 Markdown 的反序列化与序列化)。

示例文档给出的运行时分工(Runtime Shape)如下:

SurfaceOwnerNotes
decoratePreviewRegistry example读取文本节点,对node.text分词,返回带 token 类型标志的 Slate 范围。
PreviewLeafRegistry example当被装饰的 leaf 带有bolditalictitlelisthrblockquotecode标志时套用 CSS 类。
preview-markdownpluginRegistry example本地createSlatePlugin({ decorate })插件。
BasicNodesKitRegistry kit提供编辑器常规的段落和标题插件。
prismjsDependency提供装饰函数使用的 Markdown 分词器。

准备条件

按示例源码(preview-markdown-demo.tsx)的依赖来看,需要准备:

  1. platejs:提供createSlatePluginDecorateRenderLeafPropsTTextTextApi
  2. platejs/react:提供PlateusePlateEditor
  3. prismjs,并且要额外加载其 Markdown 语言组件:
import Prism from 'prismjs'; import 'prismjs/components/prism-markdown.js';
  1. 一套基础节点插件。示例使用 registry 中的BasicNodesKit,文档说明它 "Supplies the editor's normal paragraph and heading plugins",用来提供常规的段落与标题行为。

注意装饰函数本身只依赖platejsprismjs;registry 专属的BasicNodesKitcn(类名合并工具)、Editor/EditorContainer组件在你的项目里可以换成等价实现,下文代码中会标注它们的来源。

第一步:写 decorate 函数,给每个文本节点算出样式范围

Plate 会把文档里的每个[node, path]条目传给插件的decorate属性——这是 Plate 插件的一个可选字段,官方 API 文档(plate-plugin.mdx)描述为 "Property used by Plate to decorate editor ranges.",类型是Decorate<WithAnyKey<C>>

decoratePreview的处理逻辑:

  1. 只处理文本节点:TextApi.isText(node)不成立时直接返回空数组;
  2. 用 Prism 的 Markdown 语法对node.text分词:Prism.tokenize(node.text, Prism.languages.markdown)
  3. 逐个 token 累加偏移量,对非字符串 token 生成一个 Slate 范围:anchor/focus落在该 token 的起止 offset 上,并附带一个以 token 类型命名的布尔标志(如{ title: true });
  4. getLength负责计算 token 占用的字符数:字符串 token 取lengthcontent是字符串的取content.length,嵌套content数组则递归求和——这保证了范围偏移与原文本一一对应。

完整实现(来自示例源码):

import * as React from 'react'; import { type Decorate, type RenderLeafProps, type TText, createSlatePlugin, TextApi, } from 'platejs'; import { Plate, usePlateEditor } from 'platejs/react'; import Prism from 'prismjs'; import { cn } from '@/lib/utils'; import { BasicNodesKit } from '@/registry/components/editor/plugins/basic-nodes-kit'; import { previewMdValue } from '@/registry/examples/values/preview-md-value'; import { Editor, EditorContainer } from '@/registry/ui/editor'; import 'prismjs/components/prism-markdown.js'; /** Decorate texts with markdown preview. */ const decoratePreview: Decorate = ({ entry: [node, path] }) => { const ranges: any[] = []; if (!TextApi.isText(node)) { return ranges; } const getLength = (token: any) => { if (typeof token === 'string') { return token.length; } if (typeof token.content === 'string') { return token.content.length; } return token.content.reduce((l: any, t: any) => l + getLength(t), 0); }; const tokens = Prism.tokenize(node.text, Prism.languages.markdown); let start = 0; for (const token of tokens) { const length = getLength(token); const end = start + length; if (typeof token !== 'string') { ranges.push({ anchor: { offset: start, path }, focus: { offset: end, path }, [token.type]: true, }); } start = end; } return ranges; };

其中cn来自 registry 的@/lib/utilsBasicNodesKitEditor/EditorContainer来自 registry 目录,换成你自己项目里的等价模块即可;decoratePreviewPreviewLeaf本身不依赖这些导入。

第二步:用自定义 renderLeaf 给带标志的 leaf 套样式

装饰范围落到渲染层后,每个 leaf 会带上对应的标志。PreviewLeaf接收RenderLeafProps,把标志映射成 Tailwind 类名:

function PreviewLeaf({ attributes, children, leaf, }: RenderLeafProps< { blockquote?: boolean; bold?: boolean; code?: boolean; hr?: boolean; italic?: boolean; list?: boolean; title?: boolean; } & TText >) { const { blockquote, bold, code, hr, italic, list, title } = leaf; return ( <span {...attributes} className={cn( bold && 'font-bold', italic && 'italic', title && 'mx-0 mt-5 mb-2.5 inline-block font-bold text-[20px]', list && 'pl-2.5 text-[20px] leading-[10px]', hr && 'block border-[#ddd] border-b-2 text-center', blockquote && 'inline-block border-[#ddd] border-l-2 pl-2.5 text-[#aaa] italic', code && 'bg-[#eee] p-[3px] font-mono' )} > {children} </span> ); }

注意attributes必须透传到<span>上,这是 Slate 装饰范围定位所依赖的属性集合。

第三步:注册插件并创建编辑器

decorate挂到本地插件上,与基础节点插件一起传给编辑器:

export default function PreviewMdDemo() { const editor = usePlateEditor( { plugins: [ ...BasicNodesKit, createSlatePlugin({ key: 'preview-markdown', decorate: decoratePreview, }), ], value: previewMdValue, }, [] ); return ( <Plate editor={editor}> <EditorContainer> <Editor renderLeaf={PreviewLeaf} /> </EditorContainer> </Plate> ); }

初始值previewMdValue(见 preview-md-value.tsx)就是包含 Markdown 字符的普通 Plate 段落内容,没有预解析成任何 Markdown 节点:

/** @jsxRuntime classic */ /** @jsx jsx */ import { jsx } from '@platejs/test-utils'; export const previewMdValue: any = ( <fragment> <hh2>👀 Preview Markdown</hh2> <hp> Slate is flexible enough to add **decorations** that can format text based on its content. For example, this editor has **Markdown** preview decorations on it, to make it _dead_ simple to make an editor with built-in `Markdown` previewing. </hp> <hp>- List item.</hp> <hp>&gt; Blockquote paragraph.</hp> <hp>&gt; &gt; Nested blockquote.</hp> <hp>&gt; - Quoted list item.</hp> <hp>---</hp> <hp>## Try it out!</hp> <hp>Try it out for yourself!</hp> </fragment> );

这个 value 在仓库中是用@platejs/test-utils的 jsx 编写的,如果你的项目没有该依赖,等价做法是直接写出对应的 Plate 节点数组,只要段落文本里含有##>----等 Markdown 字符即可触发装饰。

验证结果

示例文档提供了 Demo 预览页与完整源码(<ComponentPreview name="preview-markdown-demo" /><ComponentSource name="preview-markdown-demo" />),运行示例后可以按文档描述的两点判断:

  1. 文本保持为文本:编辑器中的## Try it out!---> Blockquote paragraph.等段落显示的是原始 Markdown 字符,内容数据没有被转换成 heading/hr/blockquote 节点;
  2. 匹配范围带样式:这些字符范围呈现出PreviewLeaf中定义的样式——## Try it out!是 20px 加粗的标题样式,---是居中带下边框的分隔线样式,> ...段落是左侧竖线、灰色斜体的引用样式。

由于样式完全来自PreviewLeaf的类名映射,如果某个范围没变样式,先检查decorate返回的范围 offset 是否与原文本对齐(getLength计算有误会导致偏移错位),再检查 leaf 是否处理了对应的 token 标志。

边界与限制

  • decorate只对文本节点生效,非文本条目直接返回空范围。
  • 文档列出的 token 类型有 "title, bold, italic, blockquote, list, horizontal rule, and code";PreviewLeaf处理的标志即titlebolditaliclisthrblockquotecode。分词类型由 Prism 的 Markdown 语法决定,leaf 没处理的标志不会有任何样式。
  • 如果目标不再是"保留原始字符",而是要把 Markdown 真正转换成 Plate 节点(并支持序列化回 Markdown),应改用文档 Markdown 章节描述的反序列化/序列化方案,本例的装饰路径不适用于该目标。

延伸阅读

  • Preview Markdown 示例文档:Demo、Source、Runtime Shape 与选型判断的原始出处。
  • 示例源码:decoratePreviewPreviewLeaf与插件装配的完整实现。
  • Plate Plugin API:decorate字段的类型定义。
  • Slate Text API:文本装饰与被装饰的 leaf 相关说明。

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询