Skip to content
目录

Markdown 引擎与序列化策略 ​

1. 设计定位 ​

Tiptap 的 Markdown 不是“单独编辑器模式”,而是通过扩展系统把 markdown 解析/序列化能力嵌入现有 Editor。

关键文件:

  • packages/markdown/src/Extension.ts
  • packages/markdown/src/MarkdownManager.ts

2. 接入方式 ​

Markdown 扩展会:

  • 扩展 EditorOptions.contentType
  • 扩展 setContent/insertContent/insertContentAt 的 contentType
  • 在 onBeforeCreate 注入 editor.markdown 与 editor.getMarkdown()

当 contentType: 'markdown' 时,初始字符串会先 parse 成 JSON 再进入 core。

源码片段(packages/markdown/src/Extension.ts):

ts
if (assumedType === 'markdown') {
  const json = this.editor.markdown.parse(this.editor.options.content as string)
  this.editor.options.content = json
}

3. 为什么选择“扩展注册式”而不是硬编码语法表 ​

MarkdownManager 会扫描扩展能力:

  • markdownTokenName
  • parseMarkdown
  • renderMarkdown
  • markdownTokenizer
  • markdownOptions

这意味着 Markdown 支持范围由“启用的扩展集合”决定,天然与编辑器 schema 对齐。

4. parse 流程(简化) ​

  1. 用 marked lexer 把 markdown 转 token。
  2. parseTokens 遍历 token,按扩展注册处理。
  3. 无 handler 时走 fallback(paragraph/heading/text/html)。
  4. 产出 Tiptap JSON(doc + content)。

其中 list 解析有一段针对 task list 混排的拆分逻辑,避免 task/bullet 混合 token 造成结构失真。

5. serialize 流程(简化) ​

  1. 遍历 JSON nodes。
  2. 每个 node 交给对应扩展 renderMarkdown。
  3. 文本节点需处理 mark 边界的开闭顺序。
  4. 处理重叠 marks 时,必要时使用 htmlReopen(HTML 标签重开)避免 markdown 歧义。

这是 Markdown 序列化里最难的一段:保证“可读”与“可逆”同时成立。

6. HTML token 的特殊处理 ​

parseHTMLToken 会区分环境:

  • 浏览器环境:用 generateJSON(html, extensions) 按扩展 parseHTML 规则解析。
  • 服务端环境:降级为纯文本/段落,避免无 DOM 时报错。

这是对 SSR 兼容性的工程化处理。

7. 典型问题证据 ​

  • Issue: #7107
    hard break(双空格换行)在 contentType: markdown 下解析异常,后续已修复。
  • Changelog 多次记录 markdown parse/serialize 边界修复,说明该模块长期演进中。

8. 高频追问 ​

问:为什么 Markdown 不直接存字符串? ​

答:编辑时仍以 ProseMirror 文档树为真相源,markdown 只是输入/输出格式之一;否则很多编辑行为(selection、node transform、协同 merge)会非常脆弱。

问:如何支持自定义 markdown 语法? ​

答:在扩展中实现 markdownTokenizer + parseMarkdown + renderMarkdown,并与 node/mark schema 保持一致即可。

MIT License.