AI 时代一群人在卷笔记软件,我想先把 Markdown 编辑器的底层做好

4 小时 18 分钟前
 1244943563

AI 时代,一群人在卷编辑器:AI 写作、AI 笔记、AI 知识库,一个比一个炫。

可打开底层看,大家用的还是那几套东西,遇到的也还是那几个老问题:内容存成 JSON ,来回转 Markdown 时把你的原文改掉;所见即所得和源码模式是两套编辑器,切一次丢一点;协同一上来,结构就开始出怪事。AI 生成得再快,落到这些框架里照样会丢格式、错位置。

我做 ProseMirror 相关的产品三年了,这些坑基本都踩过。这次我不想再做一个编辑器应用,我想先把编辑器框架这件事做对。这篇讲其中一条线:MSFE ( Markdown Source-First Editor ),一个源码优先的 Markdown 编辑器框架。

先放成品,打开就能用: https://msfe.quirekit.com

一、别人是怎么做的

先看看最近几年流行的笔记软件和编辑器,底层都是什么。开源的看了仓库源码,闭源的只采用官方文档和工程博客,官方没说的写“未公开”(截至 2026 年 10 月):

产品 编辑方式 编辑时改的是什么 编辑器底层 内容存在哪 开源
Notion 块编辑(富文本) 块数据,Markdown 只在导入导出时转换 自研 服务端数据库 否
Obsidian 实时预览:在源码里直接渲染;另有源码模式、阅读模式 Markdown 原文 CodeMirror 6 本地 Markdown 文件 否
飞书文档 块编辑(富文本) 块树,Markdown 只在导入导出时转换 自研 云端 否
思源笔记 块编辑(富文本),可以用 Markdown 语法输入 节点树,Markdown 只在导入导出时转换 自研 Protyle 每篇一个 .sy 文件,节点树 JSON 是
Typora 所见即所得,另有源码模式 未公开 自研,内核未公开 本地 Markdown 文件 否
AFFiNE 块编辑(富文本)+ 白板 块数据 BlockSuite Yjs 是
Logseq 大纲块编辑:正在编辑的块显示源码,离开后渲染 当前块的 Markdown 原文 每个块一个 textarea 文件版是 Markdown/Org 文件; DB 版( Beta )是 SQLite 是
AppFlowy 块编辑(富文本) 块数据 自研 Flutter 编辑器 yrs ( Rust 版 Yjs ),本地 RocksDB 是
Memos 短笔记,块编辑 块结构,进出都是 Markdown 自研 数据库里的 Markdown 文本 是
MarkText 所见即所得 + 源码模式,两套 所见即所得模式改的是渲染后的文档,保存时用 turndown 转回 Markdown 自研 Muya + CodeMirror 5 本地 Markdown 文件 是
妙言 MiaoYan 源码编辑 + 语法高亮 + 界面预览 Markdown 原文 系统原生文本框 + WebView 预览 本地 Markdown 文件 是
NoteGen 富文本 + 源码模式,两套 富文本模式改的是 Tiptap 文档树,源码模式改原文 Tiptap 3 + CodeMirror 6 本地 Markdown 文件 是
Blinko 短笔记,Vditor 编辑器(支持所见即所得、即时渲染、分屏预览) 取决于 Vditor 的模式 Vditor PostgreSQL 里的 Markdown 文本 是
Flowix 富文本 + 源码模式,两套 富文本模式改的是 Tiptap 文档树,保存时转成 Markdown ;源码模式改原文 Tiptap 3 + CodeMirror 6 本地 Markdown 文件,另有 SQLite 索引 是
Joplin (桌面) 富文本 + 源码,两套 富文本模式改的是 TinyMCE 文档,切换时互相转换 TinyMCE + CodeMirror SQLite 里的 Markdown 是

以上仅收集公开信息,方便对照技术路线,不评价产品好坏。每个产品的取舍都有它的场景。

按“编辑时改的是什么”,大致可以分成三类:

不少主打本地、离线的产品,内容存的是数据库或 JSON ,Markdown 主要用在导入导出上。

二、主要问题:转换只能按规定来,你的源码保不住

富文本派要读写 Markdown ,就得在 ProseMirror 的 node 、mark 和 Markdown 源码之间来回转换。问题是,这个转换只能“按规定转”。

拿最常用的几个实现看(都是看源码得出的):

prosemirror-markdown 1.13.4

Tiptap 官方的 @tiptap/markdown ( 3.30 )

社区的 tiptap-markdown 已经停止维护。它遇到表达不了的节点,会输出一个 [nodeName] 占位;表格一有合并单元格就降级成 HTML ,列对齐信息也丢了。

这些不是 bug ,是这个架构必然的结果:文档的权威是那棵 JSON 树,Markdown 只是它的一种“打印方式”。打印方式只有一种,所以你写的那份源码,早在第一次保存时就已经不在了。

对一个人写笔记,可能无所谓。可对这些场景很要命:

三、“Markdown 做不了复杂块”,其实大错特错

常听到一种说法:Markdown 表达不了复杂结构,像多维表格、数据库视图,所以只能存 JSON 。Logseq 的联合创始人在 2024 年解释为什么做数据库版本时,也提到 Markdown 的结构化数据支持有限,缺少持久 ID 、时间戳这些东西。

持久 ID 这类问题确实存在,后面会讲。可“做不了复杂块”是错的。

Markdown 本来就有一个非常好用的容器:代码围栏。在 MSFE 里,多维表格、流程图、思维导图、白板都存成一个代码围栏,里面放的是可以反复打开编辑的数据:

```quirekit-data-view
{ ...表格的字段、记录、视图... }
```

复杂块的问题不在 Markdown ,而在你有没有给它设计一个好的落点。

四、Markdown 真正的限制在哪

说 Markdown 无所不能也不诚实。它真正的限制在语法本身,我用 micromark 4.0.2 加 GFM 扩展逐条实测过:

1. 表格单元格里只能放行内内容。 想在单元格里放一个列表或者一段代码?表格当场被拆坏:

| A | B |
| --- | --- |
| - 一
- 二 | x |

解析结果:第一行变成 - 一 和一个空单元格,- 二 | x | 被挤出表格,成了表格外面的一个列表项。代码块放进单元格也一样,整张表都乱了。单元格里想分段,只能写 <br>。

2. 格式不能随便嵌套。

3. 中文标点会让加粗失效,这个最坑中文用户。

这是**“加粗”**后面的字
注意**(重要)**这里

这两行都不会加粗,渲染出来就是原样的星号。原因是 CommonMark 判断 ** 能不能开始或结束强调,要看它两边是不是标点和空白。中文引号、括号是标点,后面又紧跟着汉字,规则就判它不成立。英文写作几乎碰不到这个问题,中文写作却天天碰到。

4. 还有一些是语法层面根本没有的东西:

所以我的态度是:

五、两套编辑器,又重又不尊重源码

Markdown 派想做所见即所得,最常见的做法是:一套富文本编辑器,加一套源码编辑器,切换时互相转换。

Joplin 桌面端就是 TinyMCE 加 CodeMirror 。它的官方帮助页很诚实,直接列出了在富文本编辑器里编辑 Markdown 笔记会出的问题:大多数插件格式会丢;表格没有表头会被补一个空表头;同一个列表里不能混用复选框和普通列表项; Markdown 里的 HTML 可能丢;引用式链接会被改成行内链接。

MarkText 是自研的 Muya 加 CodeMirror 5 ,读入用 marked 转成 HTML ,写出用 turndown 再转回 Markdown 。

两套编辑器意味着:

六、协同:ProseMirror 的 Yjs 协同,坑比想象中多

富文本派做协同,基本都走 Yjs 加 y-prosemirror ( Tiptap 的协同也是这套)。我们做 Word 级文档时专门测过,也翻了上游的文档和 issue:

上游自己承认的问题( y-prosemirror 的 CAVEATS 和 issue ):

我们自己实测的( Yjs 13.6.33 + y-prosemirror 1.3.7 、Tiptap 的 y-tiptap 3.0.9 、Yjs 14 预发布版,三组结果一致):

两个人同时做的事 结果
A 把一段拆成两段,B 在原来的后半段插字 B 插的字跑到了前半段的末尾
A 拆段,B 同时把后半段加粗 加粗丢了
A 拆段,B 同时删掉后半段的字 删掉的字又回来了
A 把两段合成一段,B 同时在第二段插字 B 插的字没了

这些不是部署问题,换个服务器、多加几台机器都解决不了。根子在“树形文档怎么映射成 CRDT”这件事上。

MSFE 的协同为什么没有这类问题:MSFE 协同的是 Markdown 源码本身,一个 Yjs 文本。任何一段文本都是合法的 Markdown ,所以不存在“结构不合法要删节点”;结构是从文本重新解析出来的,所以也不存在“拆段合段没有表示”。两个人在同一个地方改,结果可能需要人看一眼,比如把同一行表格改成了不同的样子,但内容不会因为结构冲突凭空消失。撤销是协同撤销,只撤你自己的修改。

七、为什么一定要尊重源码

说到底,就一句话:你写下的那份 Markdown ,就是文档本身。

八、CodeMirror 也能做“富文本”

很多人以为 CodeMirror 只能做源码编辑器,要做所见即所得就得上 ProseMirror 。其实 CodeMirror 6 的装饰和 widget 足够做出“看起来就是富文本”的效果:标题变大、标记符号藏起来、表格画成表格、公式渲染成公式、代码围栏里的数据渲染成流程图和多维表格。可底下的文本一个字都没变。

MSFE 本身就提供三种可以编辑的呈现:文档视图(标记符号全部隐藏,看起来就是富文本)、实时预览(编辑到哪里,哪里露出 Markdown 标记)、源码视图。它们是同一个 CodeMirror 实例上的不同呈现,不是三套编辑器。切换时不做任何转换,选区和撤销历史都还在,改的永远是同一份 Markdown 。

QuireKit 的 MD Editor 就是在 MSFE 上面拼出来的成品:加上工具栏、斜杠菜单这些界面,再加一个只读的阅读视图,用来渲染同一份源码。

文档视图:标记全部隐藏,看起来就是富文本。

实时预览:光标在哪一行,哪一行就露出 Markdown 源码,比如这里的公式行变成了 LaTeX ,其他地方照常渲染。

源码视图:同一份内容,就是这份 Markdown 。

源码优先也有代价:每次输入都要更新语法树,文件一大就慢。

这个问题我其实不是第一次碰到。之前我做过一个开源项目 Incremark ,专门解决 AI 流式输出时 Markdown 的增量渲染:模型每吐出一段文字,不重新解析整篇。做 MSFE 时我发现,编辑器的增量解析和流式渲染其实是同一个问题,而且在解析器这一层能做得更彻底。于是我直接改了 Lezer 的增量解析,在它上面重写了新版 Incremark 。现在 MSFE 编辑、MD Editor 的阅读视图、AI 流式回复的显示,用的都是同一套解析。Incremark 的来龙去脉,以及我为什么放弃了第一版,后面单独写一篇细讲。

具体的优化是给 Lezer 的 Markdown 增量解析加了“复用证明”,解析器会记下哪些区间是原样复用的,编辑器据此直接跳过没变的部分。1 MiB 的文档连续输入 20 次:

工作量 优化前 优化后
语法树访问 107,760 1,380 (少了 98.7%)
语法树比较 153,940 660 (少了 99.6%)

证明不成立的地方,会退回完整比较,所以正确性不受影响。

九、为什么要封装成框架

上面这些,单独做一个编辑器应用也能做。我把它做成框架,是因为这些问题每个做编辑器的人都会碰到,没必要每人重新踩一遍。

MSFE 的写法是 Tiptap 风格的,装上什么扩展,就能编辑什么:

import { createMsfeEditor } from '@quirekit/msfe-core'
import { StarterKit } from '@quirekit/msfe-starter-kit'

const editor = createMsfeEditor({
  parent,
  documentId: 'note-1',
  markdown: '# Hello\n\n**Markdown** stays the source.\n',
  mode: 'live',
  extensions: [StarterKit],
})

editor.getMarkdown() // 永远是那份源码

这些都是 MIT 开源。AI 读写文档、改动以“待审阅”的形式显示,以及批注,是商业组件,开发和评估免费。

十、MD Editor 和周边生态

不想自己拼的,直接用 MD Editor:工具栏、四种视图、斜杠菜单、表格、公式、图表都配好了,几行代码就能放进 Vue 项目。

<script setup lang="ts">
import { MdEditor } from '@quirekit/md-editor/vue'
import '@quirekit/md-editor/styles.css'
</script>

<template>
  <MdEditor markdown="# Hello" label="Document" />
</template>

周边的编辑器也都是独立的包:公式编辑器( LaTeX 和 Word 公式互转)、流程图、思维导图、白板、Mermaid / PlantUML / Graphviz 文本绘图、多维表格,以及给 AI 边生成边显示用的流式 Markdown 渲染( Incremark )。所有界面都来自同一套 UI 组件库,亮色暗黑一键切换。

十一、专业场景:Word 和 PDF ,两套方案

Markdown 写完,总有人要 Word 、要 PDF 。这里我们给了两套方案。

开源方案(@quirekit/md-editor-basic-export,MIT ):用成熟的开源库来转。Markdown 转 Word 用 md-to-docx ,Word 转 Markdown 用 Mammoth 加 Turndown ,PDF 用 pdfmake ,全在浏览器里完成。MSFE 特有的内容会降级:流程图、思维导图、白板导出成预览图,其他嵌入块变成占位。每一处降级都会写进导出报告,不会悄悄丢掉。

商业方案:不走“Markdown 转 HTML 再转 Word”这条路,而是把 Markdown 转进和我们分页文档编辑器 Classic 同一套文档模型,再交给 Classic 的排版引擎输出 Word 、PDF 和 OFD:

顺便说一下 Classic

Classic 就是上面提到的那个分页文档编辑器,是 QuireKit 的另一条线:基于 ProseMirror ,自己排版、真正分页,画在 Canvas 或 SVG 上。它解决的是另一类问题,有机会单独写一篇。官网首屏就是它,可以直接上手试: https://quirekit.com

最后

QuireKit 不是某一个编辑器,而是一整套编辑器生态:两套 Tiptap 风格的编辑器框架( ProseMirror 线和这篇讲的 CodeMirror 线 MSFE ),上面是分页文档、块编辑器、Markdown 编辑器,以及一堆周边编辑器,共用一套 UI 组件库。开源部分全部 MIT ,闭源商用也可以。

目前还是 alpha ,API 还会变,欢迎来试、来挑刺:

觉得这个方向有意思的话,点个 Star 就是对我最大的支持。也很想听听:你们做 Markdown 编辑器时,被哪个坑卡得最久?

542 次点击
所在节点    程序员
4 条回复
Marthemis
36 分钟前
是不是可以理解成:处理 markdown 这类内容时,你的选择是 codemirror 的 MSFE (以源码为根)而不是 prosemirror (以内置 json schema 为根)。很有借鉴意义。
linkopeneyes
26 分钟前
Markdown 其实也有缺点,格式表达不够丰富即使都用 code 来表达也不够,我们之前就是用 markdown 来表达 ai 生成的内容,像字号字体字重,行间距,行首对齐,段间距缩进很多其实没法表达,因为很多时候你想做的只是富文本编辑器,使用的人只想要 word,所以只能最开始 markdown 生成,存还是只能存 html
tiptap 也有问题,做个编辑器历史记录没法存下来,分页做起来也复杂,超长内容还要做虚拟化,如果想要自己受众广导出还不能是 markdown 得是 word 或者是 pdf,还得一格式一个格式做过去,word 还支持磅字符那些的单位,还得自己一个一个支持,即使支持了也很难让网页和导出的 word 效果对齐
1244943563
17 分钟前
@Marthemis 是的,以前我总喜欢给 tiptap 做 markdown 适配,用 tiptap 结合 codemirror 面板做 md 编辑器,然后做分屏,但是后来因为 markdown 转换总是会改源码,我心里就很不舒服。所以还是想要尊重一下 markdown
1244943563
14 分钟前
@linkopeneyes 是的,我没有全部提到,文中只提到了一部分。我做 quirekit 的时候,其实提供了各种编辑器,word 最难啃,要自己做 view 排版引擎,分页,block editor 可以做块编辑器,处理复杂的场景。如果要追求不出本地,源码可靠,markdown 为底座,那就是 msfe 做底层。其实各有自己的差异,按照场景用不同的方案才是最好的。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1252802

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX