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
* 和 **,你写的 _斜体_、__粗体__ 存一次就变了。*,你习惯的 - 没了。1. 1. 1. 存完变成 1. 2. 3.。\。*、[、]、~ 一律加反斜杠转义。Tiptap 官方的 @tiptap/markdown ( 3.30 )
-;有序列表重新编号。社区的 tiptap-markdown 已经停止维护。它遇到表达不了的节点,会输出一个 [nodeName] 占位;表格一有合并单元格就降级成 HTML ,列对齐信息也丢了。
这些不是 bug ,是这个架构必然的结果:文档的权威是那棵 JSON 树,Markdown 只是它的一种“打印方式”。打印方式只有一种,所以你写的那份源码,早在第一次保存时就已经不在了。
对一个人写笔记,可能无所谓。可对这些场景很要命:
常听到一种说法:Markdown 表达不了复杂结构,像多维表格、数据库视图,所以只能存 JSON 。Logseq 的联合创始人在 2024 年解释为什么做数据库版本时,也提到 Markdown 的结构化数据支持有限,缺少持久 ID 、时间戳这些东西。
持久 ID 这类问题确实存在,后面会讲。可“做不了复杂块”是错的。
Markdown 本来就有一个非常好用的容器:代码围栏。在 MSFE 里,多维表格、流程图、思维导图、白板都存成一个代码围栏,里面放的是可以反复打开编辑的数据:
```quirekit-data-view
{ ...表格的字段、记录、视图... }
```
复杂块的问题不在 Markdown ,而在你有没有给它设计一个好的落点。
说 Markdown 无所不能也不诚实。它真正的限制在语法本身,我用 micromark 4.0.2 加 GFM 扩展逐条实测过:
1. 表格单元格里只能放行内内容。 想在单元格里放一个列表或者一段代码?表格当场被拆坏:
| A | B |
| --- | --- |
| - 一
- 二 | x |
解析结果:第一行变成 - 一 和一个空单元格,- 二 | x | 被挤出表格,成了表格外面的一个列表项。代码块放进单元格也一样,整张表都乱了。单元格里想分段,只能写 <br>。
2. 格式不能随便嵌套。
`**加粗**` 显示的就是带星号的原文。[外层 [内层](b) 文字](a),外层链接直接失效,变回普通文字。<u>**粗**</u> 还好,HTML 和 Markdown 都能生效;可一旦写成 HTML 块,比如 <div align="center"> 里面写 **居中加粗**,里面的 Markdown 就不再解析,直接显示成纯文本的星号。3. 中文标点会让加粗失效,这个最坑中文用户。
这是**“加粗”**后面的字
注意**(重要)**这里
这两行都不会加粗,渲染出来就是原样的星号。原因是 CommonMark 判断 ** 能不能开始或结束强调,要看它两边是不是标点和空白。中文引号、括号是标点,后面又紧跟着汉字,规则就判它不成立。英文写作几乎碰不到这个问题,中文写作却天天碰到。
4. 还有一些是语法层面根本没有的东西:
所以我的态度是:
Markdown 派想做所见即所得,最常见的做法是:一套富文本编辑器,加一套源码编辑器,切换时互相转换。
Joplin 桌面端就是 TinyMCE 加 CodeMirror 。它的官方帮助页很诚实,直接列出了在富文本编辑器里编辑 Markdown 笔记会出的问题:大多数插件格式会丢;表格没有表头会被补一个空表头;同一个列表里不能混用复选框和普通列表项; Markdown 里的 HTML 可能丢;引用式链接会被改成行内链接。
MarkText 是自研的 Muya 加 CodeMirror 5 ,读入用 marked 转成 HTML ,写出用 turndown 再转回 Markdown 。
两套编辑器意味着:
富文本派做协同,基本都走 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 只能做源码编辑器,要做所见即所得就得上 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:工具栏、四种视图、斜杠菜单、表格、公式、图表都配好了,几行代码就能放进 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 组件库,亮色暗黑一键切换。
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 就是上面提到的那个分页文档编辑器,是 QuireKit 的另一条线:基于 ProseMirror ,自己排版、真正分页,画在 Canvas 或 SVG 上。它解决的是另一类问题,有机会单独写一篇。官网首屏就是它,可以直接上手试: https://quirekit.com
QuireKit 不是某一个编辑器,而是一整套编辑器生态:两套 Tiptap 风格的编辑器框架( ProseMirror 线和这篇讲的 CodeMirror 线 MSFE ),上面是分页文档、块编辑器、Markdown 编辑器,以及一堆周边编辑器,共用一套 UI 组件库。开源部分全部 MIT ,闭源商用也可以。
目前还是 alpha ,API 还会变,欢迎来试、来挑刺:
觉得这个方向有意思的话,点个 Star 就是对我最大的支持。也很想听听:你们做 Markdown 编辑器时,被哪个坑卡得最久?
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.