Lexical Markdown 集成指南:@lexical/markdown 的导入导出、快捷键与 Transformers 深度解析
【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical
@lexical/markdown是 Lexical 官方提供的 Markdown 辅助包,为富文本编辑器带来完整的 Markdown 导入(import)、导出(export)与输入快捷键(shortcuts)能力。本文以 packages/lexical-markdown/README.md 为主线,结合仓库源码深入讲解其核心 API、内置 Transformers 的构成与顺序规则、自定义 Transformer 的接口契约,以及导入导出管线的底层实现,帮助你在自己的 Lexical 应用中落地"复制粘贴 Markdown / 以 Markdown 初始化 / 输入时实时排版"三类典型场景。
一、包定位与能力概览
@lexical/markdown包的定位非常纯粹:它不包含任何编辑器 UI,只提供三类能力:
- Import(导入):把 Markdown 字符串解析成 Lexical 节点树,写入编辑器状态;
- Export(导出):把编辑器状态序列化回 Markdown 字符串(整篇或仅选中内容);
- Shortcuts(快捷键):用户在编辑器内输入
#、-、**等 Markdown 标记时,实时转换为对应富文本节点。
从 package.json 可以看到,它依赖lexical以及@lexical/code-core、@lexical/link、@lexical/list、@lexical/rich-text、@lexical/selection、@lexical/text、@lexical/utils、@lexical/internal等包,说明 Markdown 转换最终映射到的是 Lexical 的 Code、Link、List、Heading、Quote 等标准节点体系。
二、导入与导出:四个核心函数
包的公共入口在 packages/lexical-markdown/src/index.ts,导出了四个核心函数,覆盖"整篇转换"与"选中内容转换"两个维度。
2.1 导出:$convertToMarkdownString
将当前编辑器状态(或指定的ElementNode子树)导出为 Markdown 字符串:
import { $convertToMarkdownString, TRANSFORMERS, } from '@lexical/markdown'; editor.update(() => { const markdown = $convertToMarkdownString(TRANSFORMERS); // 得到整篇文档的 Markdown 文本 });函数签名(来自 index.ts):
function $convertToMarkdownString( transformers: Transformer[] = TRANSFORMERS, node?: ElementNode, // 不传则导出整篇根节点 shouldPreserveNewLines: boolean = false, ): string其中shouldPreserveNewLines为true时,转换过程会保留源码中的换行结构;同时源码注释指出,保留换行时还会对* _ ~等特殊字符做转义,避免破坏 Markdown 语义(见 MarkdownExport.ts 中exportTextFormat的转义逻辑)。
2.2 导入:$convertFromMarkdownString
将 Markdown 字符串解析为节点并写入编辑器根节点,操作完成后选区移到文档开头:
editor.update(() => { $convertFromMarkdownString(markdown, TRANSFORMERS); });函数签名(来自 index.ts):
function $convertFromMarkdownString( markdown: string, transformers: Transformer[] = TRANSFORMERS, node?: ElementNode, // 不传则写入 $getRoot() shouldPreserveNewLines = false, shouldMergeAdjacentLines = false, // CommonMark 相邻非空行合并规则 ): void两个布尔参数的行为差异值得注意(源码注释有明确说明):
shouldPreserveNewLines = true:保留 Markdown 源文本中的换行,例如空段落、硬换行等不会被吞掉;shouldPreserveNewLines = false时,shouldMergeAdjacentLines才生效:相邻的非空行会按 CommonMark 规范(spec.commonmark.org 0.24 第 177 例)合并为同一段落。
合并与保留的具体逻辑在normalizeMarkdown中实现(见 MarkdownTransformers.ts):它逐行扫描,能识别代码围栏(围栏内的行一律原样保留)、标题、引用、列表、表格分隔行等块级结构,在这些结构之间以及空行处不合并,其余相邻文本行用空格拼接。
2.3 生成节点但不落树:$generateNodesFromMarkdownString
该函数解析 Markdown 后返回LexicalNode[]数组,不修改文档树、不触碰选区,返回的节点可以借助selection.insertNodes()插入到任意位置(源码注释原文说明)。其实现是先把节点导入到一个ArtificialNode__DO_NOT_USE临时容器,再取出子节点,非常适合"把 Markdown 片段插入到光标处"这类需求(index.ts)。
2.4 选中内容导出:$convertSelectionToMarkdownString
只把当前选区内容导出为 Markdown:
function $convertSelectionToMarkdownString( transformers: Transformer[] = TRANSFORMERS, selection: BaseSelection | null, shouldPreserveNewLines: boolean = false, ): string当selection为空或是折叠选区(collapse)时直接返回空字符串(index.ts)。其底层由createSelectionMarkdownExport实现(MarkdownExport.ts),采用类似 HTML 导出中extractWithChild的递归结构,正确处理链接等行内元素上的部分选区;例如选中一行列表项时,未选中的兄弟项会被跳过。
三、用 Markdown 初始化编辑器状态
导入 API 最常见的实战场景之一,是用 Markdown 字符串初始化编辑器的初始内容。README 给出了配合 React<RichTextPlugin>的标准写法:
<LexicalComposer initialConfig={{ editorState: () => $convertFromMarkdownString(markdown, TRANSFORMERS), }} > <RichTextPlugin /> </LexicalComposer>editorState是惰性求值的函数,在编辑器首次渲染时执行一次,即可把 Markdown 渲染为富文本内容。这在"从数据库/文件恢复内容""预览 Markdown 文件"等场景中非常实用。
四、输入快捷键:边打字边转 Markdown
4.1 React 场景:<MarkdownShortcutPlugin>
如果使用 React,直接挂载官方提供的插件组件即可:
import { TRANSFORMERS } from '@lexical/markdown'; import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'; <LexicalComposer> <MarkdownShortcutPlugin transformers={TRANSFORMERS} /> </LexicalComposer>此后用户输入#生成一级标题、##生成二级标题、-生成无序列表、1.生成有序列表、>生成引用、```生成代码块、**包裹生成加粗、text生成链接等。
4.2 非 React 场景:registerMarkdownShortcuts
不用 React 时,可以通过registerMarkdownShortcuts手动注册快捷键监听:
import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown'; const editor = createEditor(/* ... */); registerMarkdownShortcuts(editor, TRANSFORMERS);该函数实现在 MarkdownShortcuts.ts,核心机制值得展开:
- 通过
registerCommand注册文本变化(TEXT_INSERT_COMMAND)监听,在每次输入后判断光标前的文本是否命中 Transformer 的正则; - 对于块级(element)Transformer,要求锚点字符是段首文本节点、且光标前一字符为空格(防止在单词中间误触发),命中后执行
splitText分割并调用replace完成节点替换(MarkdownShortcuts.ts); - 文本匹配(text-match)Transformer(如链接)则通过
trigger字符(如))在敲入该字符的瞬间触发匹配; - 部分块级 Transformer 还支持
triggerOnEnter: true,即行尾直接按回车也能触发(无需尾随空格),内置的HEADING、QUOTE、UNORDERED_LIST、ORDERED_LIST、CHECK_LIST均开启了该选项(见 MarkdownTransformers.ts 的HEADING定义)。
五、Transformers:一切转换的灵魂
Markdown 的一切功能都建立在transformers 配置数组之上。它是一个对象数组,定义了在导入、导出或输入过程中如何处理特定文本或节点。README 原文强调:"Transformers are explicitly passed to markdown API allowing application-specific subset of markdown or custom transformers"——即 transformers 由调用方显式传入,你可以自由裁剪内置集合,也可以编写自定义 Transformer。
5.1 三种 Transformer 类型
| 类型 | 作用对象 | 典型代表 | 源码类型定义位置 |
|---|---|---|---|
| Element transformer | 顶层块级元素(列表、标题、引用、表格、代码块) | HEADING、QUOTE、UNORDERED_LIST、ORDERED_LIST | MarkdownTransformers.ts |
| Text format transformer | 应用TextFormatType定义的文本范围格式 | BOLD_STAR、ITALIC_STAR、INLINE_CODE、STRIKETHROUGH | MarkdownTransformers.ts |
| Text match transformer | 匹配叶子文本节点的内容并替换为节点 | LINK | MarkdownTransformers.ts |
三种类型对应源码中的联合类型Transformer = ElementTransformer | MultilineElementTransformer | TextFormatTransformer | TextMatchTransformer(MarkdownTransformers.ts)。
5.2 内置 Transformers 清单
README 列出了包内提供的全部内置 Transformer:
Element transformers(块级)
UNORDERED_LIST // 无序列表:- * + 开头 CODE // 代码块:``` 围栏 HEADING // 标题:# ~ ###### ORDERED_LIST // 有序列表:1. 2. ... QUOTE // 引用:> 开头Text format transformers(文本格式)
BOLD_ITALIC_STAR // ***text*** BOLD_ITALIC_UNDERSCORE // ___text___ BOLD_STAR // **text** BOLD_UNDERSCORE // __text__ INLINE_CODE // `code` ITALIC_STAR // *text* ITALIC_UNDERSCORE // _text_ STRIKETHROUGH // ~~text~~Text match transformers(文本匹配)
LINK // text此外,在 index.ts 的导出中还可以看到两个 README 清单之外的内置项:CHECK_LIST(任务清单,匹配- [ ]/- [x],属于 element 类型)和HIGHLIGHT(高亮,==text==格式,属于 text format 类型),以及工具函数isTableRowDivider和normalizeMarkdown。这说明当前仓库版本的内置能力比 README 示例清单更完整,使用前以实际导出的常量为准。
5.3 常用打包集合
包内置了五组常用打包:
| 常量 | 内容 |
|---|---|
TRANSFORMERS | 全部内置 transformers |
ELEMENT_TRANSFORMERS | 全部内置 element transformers(HEADING、QUOTE、UNORDERED_LIST、ORDERED_LIST) |
MULTILINE_ELEMENT_TRANSFORMERS | 全部内置多行 element transformers(CODE) |
TEXT_FORMAT_TRANSFORMERS | 全部内置 text format transformers |
TEXT_MATCH_TRANSFORMERS | 全部内置 text match transformers(LINK) |
TRANSFORMERS的组装方式见 MarkdownTransformers.ts:按 element → multiline-element → text-format → text-match 的顺序拼接。
5.4 顺序即语义:内置数组的排列规则
阅读源码可以发现两个与顺序强相关的规则(源码注释明确写出):
- code 优先:
TEXT_FORMAT_TRANSFORMERS中INLINE_CODE排在最前(MarkdownTransformers.ts),因为反引号内的内容不应被其他格式转换,防止**等标记在行内代码里被误处理; - 长标记优先:
BOLD_ITALIC_STAR(***)排在BOLD_STAR(**)之前、BOLD_STAR排在ITALIC_STAR(*)之前,保证***text***被识别为粗斜体而不是粗体嵌套斜体。
同样的规则也体现在导出端:createMarkdownExport会过滤掉多格式 Transformer(如***),只用单格式 Transformer(**+*)分别导出,并把包含code格式的 Transformer 排序到末尾,避免**Bold Code**这种错误输出(MarkdownExport.ts)。
六、编写自定义 Transformer:接口契约与源码级拆解
README 提示可查看MarkdownTransformers.js了解实现范例(当前仓库对应源码为 packages/lexical-markdown/src/MarkdownTransformers.ts)。下面按类型给出接口字段与实现要点。
6.1 ElementTransformer
type ElementTransformer = { type: 'element'; dependencies: Klass<LexicalNode>[]; // 依赖的节点类,用于节点注册 regExp: RegExp; // 匹配行首标记 replace(parentNode, children, match, isImport): boolean | void; export(node, traverseChildren, selection?): string | null; triggerOnEnter?: boolean; // 是否支持回车触发 };export返回null表示放弃导出(Lexical 会继续尝试下一个 transformer);返回字符串表示该节点由本 transformer 序列化;replace返回false表示放弃转换(即使正则已匹配),isImport参数用于区分是导入操作还是输入快捷键操作——例如HEADING.replace在非导入且父节点为不可替换块(QuoteNode)时返回false(MarkdownTransformers.ts),防止引用块被块级快捷键意外吞掉(源码注释引用了 issue #7407);- 块级创建的通用模式是
createBlockNode(MarkdownTransformers.ts):创建节点 →append(...children)→parentNode.replace(node),非导入时把选区移到新节点开头。
6.2 MultilineElementTransformer
代码块CODE是唯一的内置多行 transformer,比普通 element 多出regExpStart/regExpEnd(结束围栏可标记为optional,未闭合时匹配到文档末尾)以及可选的handleImportAfterStartMatch手工接管导入流程。其实现细节非常丰富,值得关注的几点:
- 围栏长度自适应:导出时若代码内容里出现更长的反引号串,会动态加长围栏,保证围栏不与内容冲突(MarkdownTransformers.ts);
- 围栏缩进剥离:按 CommonMark 规范,起始围栏缩进 N 个空格,内容每行最多剥掉 N 个空格(
stripFenceIndent); - info string 元数据:
```js title="x"中语言之后的title="x"被存入codeMetaState,往返转换不丢失(源码注释专门解释了这一设计); - 单行代码块:
```code```单行形态也有专门的正则CODE_SINGLE_LINE_REGEX处理(导入前的 normalize 阶段识别)。
6.3 TextFormatTransformer
结构最简单,只有三个字段:
type TextFormatTransformer = Readonly<{ type: 'text-format'; format: readonly TextFormatType[]; // 如 ['bold']、['bold', 'italic'] tag: string; // 如 '**'、'*'、'`' intraword?: boolean; // 是否允许出现在单词内部 }>;intraword的作用:ITALIC_UNDERSCORE(_)和BOLD_UNDERSCORE(__)设置为false,即foo_bar中不会被当成斜体(避免与文件名等场景冲突);而星号版本ITALIC_STAR未设置该字段,行为更宽松。README 指出这些格式最终映射到TextFormatType(bold、italic、underline、strikethrough、code、subscript、superscript),内置 transformer 覆盖了其中常用子集。
6.4 TextMatchTransformer
LINK是唯一的实现范例,字段最丰富:importRegExp(导入时匹配)、regExp(快捷键匹配)、trigger(触发字符,链接为')')、replace与export。链接导出的细节可以体现这个包的严谨程度:
- 目的地含空白时改用
<...>尖括号形式;空 URL 也走尖括号形式; - 圆括号
()、反斜杠、行尾换行等特殊字符分别做转义或转成字符引用( / ); - 标题支持三种引号拼写(双引号 / 单引号 / 圆括号)并在往返时保留(MarkdownTransformers.ts)。
七、导入管线:从字符串到节点树
导入的完整流程(入口$convertFromMarkdownString→$importMarkdownNodes,见 MarkdownImport.ts)大致为:
- 按类型索引:
transformersByType把传入数组按 element / multiline-element / text-format / text-match 分组; - 预处理:
normalizeMarkdown规范化换行、合并相邻行(受shouldPreserveNewLines/shouldMergeAdjacentLines控制),并构造 text-format 索引(createTextFormatTransformersIndex为每个 tag 生成完整匹配正则,单字符 tag 与多字符 tag 的正则策略不同,且特意避免使用 Safari 16.4 以下不支持的负向后行断言,见 MarkdownImport.ts); - 逐行解析:先尝试
$importMultiline处理多行元素(命中即返回并跳过被消费的行),否则走$importBlocks按 element → text-format → text-match 顺序处理单行; - 清理:非保留换行模式下移除空段落(
isEmptyParagraph),并把文本节点中的制表符\t拆分为TabNode($normalizeMarkdownTextNode,逐个构建节点以避免长制表符串时的调用栈溢出问题)。
另外,导入解析列表时还实现了列对齐感知的嵌套列表:通过withListIndentColumns在单次导入过程中跨行记录每个列表层级的内容起始列,使1. a的三空格子列表与- a的两空格子列表都能正确嵌套、同列兄弟保持平级(MarkdownTransformers.ts),这是对 CommonMark 列表规则的忠实实现。
八、导出管线:从节点树到 Markdown
导出端($convertToMarkdownString→createMarkdownExport,见 MarkdownExport.ts)的关键机制:
- 顶层节点依次尝试 element / multiline transformers 的
export,命中即用其结果,否则回退到通用子节点导出;DecoratorNode导出其getTextContent(); - 行内格式跨节点闭合:
exportTextFormat用unclosedTags数组跟踪尚未闭合的格式标记,遇到文本兄弟节点时复用打开的标签,避免输出**a****b**这种可合并而未合并的碎片;同时引入unclosableTags,防止链接内部出现**text text**这种把闭合标记关进链接里的非法 Markdown(MarkdownExport.ts); - 空白与 flanking 规则:按 CommonMark 要求,格式标记必须紧贴非空白字符,因此
" foo "会导出为**   foo   **,用字符引用保住首尾空白(MarkdownExport.ts); - 相邻非空块之间用
\n\n分隔,空段落渲染为独立换行。
九、仓库内可运行的完整示例
本仓库自带多个可直接运行参考的 Markdown 集成示例:
- dev-examples/dom-import/src/MarkdownShortcutsExtension.ts:在 DOM 导入示例中接入 Markdown 快捷键;
- examples/markdown-editor/src/extensions:完整的 Markdown 编辑器示例,含多个 extension;
- examples/markdown-editor/src/tests:配套的转换测试。
单元测试方面,packages/lexical-markdown/src/tests/unit/LexicalMarkdown.test.ts 与 MarkdownTransformers.test.ts 覆盖了导入导出的往返一致性,另有若干针对边角场景的专项测试:CodeBlockFenceIndent.test.ts(围栏缩进)、CodeBlockLeadingBlankLine.test.ts(代码块前导空行)、EscapedBackslashHardLineBreak.test.ts(转义反斜杠与硬换行)、Issue5366Repro.test.ts(历史 issue 回归)。阅读这些测试是快速理解转换行为边界的最佳途径。
十、常见实践模式总结
| 需求 | 推荐方案 |
|---|---|
| 从 Markdown 初始化编辑器 | initialConfig.editorState = () => $convertFromMarkdownString(md, TRANSFORMERS) |
| 保存时导出整篇 Markdown | editor.update(() => $convertToMarkdownString(TRANSFORMERS)) |
| 复制选中内容为 Markdown | $convertSelectionToMarkdownString(TRANSFORMERS, selection) |
| 把 Markdown 片段插入光标处 | $generateNodesFromMarkdownString(md, TRANSFORMERS)+selection.insertNodes(...) |
| React 中输入即转换 | <MarkdownShortcutPlugin transformers={TRANSFORMERS} /> |
| 非 React 输入即转换 | registerMarkdownShortcuts(editor, TRANSFORMERS) |
| 只支持部分语法 | 传自定义 transformers 数组,如[HEADING, BOLD_STAR, LINK] |
| 保留源码换行 | 各转换函数传shouldPreserveNewLines: true |
需要提醒的是:所有转换 API 都必须在editor.update()回调或editorState.read()等合适的 Lexical 更新上下文内调用(以$前缀开头的函数是 Lexical 的"内部更新环境"专用 API);transformers 中的dependencies数组列出的节点类(如HeadingNode、ListNode、CodeNode、LinkNode)需要预先在编辑器nodes配置中注册,否则导入会失败。
总而言之,@lexical/markdown以"显式传入的 transformers 数组"为统一抽象,把导入、导出、快捷键三条路径串成一套可裁剪、可扩展的机制。理解内置 transformers 的顺序规则、三类接口契约与导入导出管线,你就能按需组合出符合业务语法的 Markdown 富文本编辑器。
【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考