我写了个 Markdown 编辑器,中间把内核整个砸了重来
我用过很多 Markdown 编辑器,没有一个是我想一直用下去的。
这话说出来有点不讲道理,因为那些工具本身都不差。Typora 的手感是真好,但它不开源,Windows 上字体渲染一般。VS Code 装插件能写,可左边文件树右边终端,写东西的时候像个程序员在干活。在线的那些更好看,但把稿子放在别人的服务器上,我不太愿意。
所以十月下旬我动手做了一个。10 月 23 号第一版跑起来,叫 mditor。
第一版就写错了
第一版的内核是 CodeMirror 6 加装饰器。思路很简单,文档就是纯文本,光标离开某一行时,把 **加粗** 两边的星号藏起来,把字变粗,看起来就是渲染过的样子。Typora 早期也走过这条路。
想法成立,实现也在推进,单测能过百。
然后项目文档里留下了这一版真机验证的结论,四条,全是毛病。光标进入某行时整行退化成小号源码,排版剧烈跳变。表格、列表、引用、图片都没正确渲染。没有所见即所得和源码的双模式切换。第三行写得很干脆,CodeMirror 装饰器方案以纯文本行为模型,上限是 Obsidian 的 Live Preview。
表格它只能画成一堆竖线。列表没缩进没圆点。引用块就是一行灰字,没有容器感。
问题不在某个 bug 上。装饰器方案的底层模型是「文本行」,它知道第 37 行有哪些字符,不知道第 37 行到第 40 行是一个表格。要做成 Typora 那样,得给每一种块结构手写一套替换组件,还要处理光标怎么跨过这些组件、怎么在表格单元格之间跳、粘贴进来一段 HTML 怎么拆。
这条路走到头也到不了我要的地方。
把内核砸了
我停了三天,把方案推翻,换成 Milkdown,底层是 ProseMirror 加 remark。
这次换掉的是文档模型。ProseMirror 的文档是一棵语义树,表格是一个 table 节点,下面挂着 row,row 下面挂 cell,cell 里面才是段落。光标在单元格之间移动是原生行为,不用我自己算。
源码模式保留,因为有时候你需要精确控制那一个空格。这部分用 CodeMirror 6,⌘/ 一键切。
两套引擎共存,共享同一份文档内容。切过去切回来,内容不能变,光标位置要合理,撤销栈不能乱。这部分调了两天才顺手。
这次改完再上手,一行字的字形不再随光标移动而变。表格能直接在预览里点进去改。引用块有左边一条竖线和内边距,看起来是个容器了。

我在项目文档里给这一版写了句话,把编辑器内核从「CodeMirror 装饰器」换成「Milkdown 所见即所得 + CodeMirror 源码模式」。听起来是一次技术选型调整,实际是三天白干加一次重写。
一条把自己逼死的规矩
项目里有一条硬规矩,从第一版就立着,打开一个 Markdown 文件,再原样保存,磁盘上那堆字节必须一模一样。
听起来是应该的。做起来不。
富文本编辑器保存时,是把内部结构重新序列化成 Markdown。这一路上每个环节都可能改你的东西。列表符号 * 被规范化成 -。表格的对齐冒号被重排。你辛辛苦苦手写的缩进被吃掉两个空格。行尾的空格、空行、frontmatter 里那个古怪的 YAML 写法,全可能在这一步走样。
用户不会立刻发现。等到他某天 git diff,看见自己没碰过的二十行报了改动。
我没打算接受这件事。于是有了一条门禁,四十来份「打开再保存、逐字节比对」的回归测试。语料是我自己攒的,各种刁钻写法都有,嵌套列表混着任务项、表格里塞行内代码、引用块套列表、带 CRLF 的文件、只有 BOM 没有内容的空文件。测试里还有一条断言,语料数量少于三十份就直接失败,防止有人图省事把刁钻的样本删掉。
任何一次提交,这些用例里有一条字节不一致,就不许合。
这条规矩很贵。它逼着我去定制 remark 的序列化管线,一个一个节点地对,而不是拿默认配置糊过去。它也逼着后面所有新功能都得考虑这件事,加一个语法扩展,得顺手保证它序列化回去还是原样。
但它换来的是另一件事,你随时可以把这个工具装上去试,你的稿子不会有任何风险。这一条比任何功能都重要。
那些不写代码的人不会在意的地方
界面这块我花的时间比预想的多得多。
窗口不要标题栏,macOS 的红绿灯按钮浮在内容上,左边的侧边栏得留出避让空间,不然按钮压在文件名上。侧边栏宽度能拖,从 180 到 480 像素,双击把手回到 240,这个宽度要记住,下次打开还是你上次设的。
标签页能拖着换顺序,鼠标停在标签上,如果是已保存的文件,浮出完整路径,如果是没保存的草稿,从正文第一行抓个标题出来当提示。
有四套排版主题,macOS 清爽浅色、GitHub 文档风、暗黑护眼、米黄羊皮纸。换主题就是换一组 CSS 变量,编辑器里的字号行距跟着变,代码块和表格都得跟着对。
这些东西单独拿出来都不算技术难点。但它们加起来决定了一个工具是「能用」还是「想用」。我在这上面花的功夫,大概和写编辑器内核差不多。
三个我没想到会做进去的功能
做到第三版,我发现自己其实不只是在做一个编辑器。
第一个是公众号排版。 起因很实际,我写完东西要发公众号,微信后台不认外部 CSS,从 Markdown 直接粘过去,字体大小乱、代码块没底色、表格变形。原来用的办法是扔进第三方在线排版网站,每次都得把稿子贴上去。
我做成了本地的一步。选中一个主题,点复制,剪贴板里是内联了样式的 HTML,直接去微信后台 ⌘V,字体、行距、代码块、引用、表格全在。外链微信不让跳,会自动转成文末的参考文献列表。代码块加了 Mac 窗口那三个圆点。
同一个功能对知乎也能用。
第二个是全文检索。 ⌘⇧F 在整个工作区里搜,支持正则和全字匹配,结果按文件分组,点进去直接跳到匹配那一行并高亮。⌘P 是模糊打开,敲几个字母就能定位到文件。
我一开始觉得这是锦上添花。实际用下来,它是让我愿意把整个笔记库都放进去的原因。
第三个是导出。 PDF 走 Chromium 的打印管线,但我改了分页逻辑,标题和代码块不允许被从中间劈开,页眉页脚做了出版级的样式。Word 是真导出 .docx,不是 HTML 改后缀,标题层级、表格、引用、列表都映射成原生的 Word 结构。还有自包含的 HTML,样式和图片全内嵌,单文件拿走就能看。
一个说实话的部分
这个项目我给自己上了一条规矩,任何特性必须先有失败的测试,再有实现。不许没测试就合代码。
到现在,八十多个测试套件、三百多项用例,加 Playwright 跑的真机端到端测试。因为跑得太频繁,速度快慢也成了体验的一部分,现在整套单测是秒级跑完。
我不觉得这个数字本身值得炫耀。测试多不等于质量好,测试写得烂一样能全绿。但它确实抓到了一些很难靠手点发现的问题。
举个例子。我在侧边栏加了一个全工作区搜索,加完之后跑单测,卡死了。CPU 占满,四十多分钟不出结果。
查了很久才明白。组件内部有个衍生数组,folders ?? [],外部没传 folders 的时候,每次渲染都生成一个新的空数组。这个数组被塞进了 useEffect 的依赖。于是渲染、生成新数组、触发 effect、effect 里调用 setState、再渲染,无限循环。
人手动点界面永远发现不了这个,因为条件很窄。但测试环境每次都满足。
还有些更细的。Electron 的自定义协议如果声明了 standard,local-file:///Users/... 里的 Users 会被当成主机名转成小写 users,路径直接丢了根目录。这类问题网上一搜一大把,但你得先遇上才知道要搜。
为什么最后长出了授权
做到第六版,功能已经够我自己用了。第七版我做了件很多开源作者会犹豫的事,加了买断制的授权。
我犹豫的原因很简单,这东西装了就能用,凭什么让人付钱。
最后定下来的线是这样画的。打字、光标手感、大纲、多标签、打开文件、保存,永久免费,没有天数限制,没有功能阉割。要付费的是三样,公众号一键排版、导出无水印的 PDF 和 Word、以及自定义的排版主题脚本。
15 天全功能试用,不注册、不绑卡。到期之后上面那三样会拦一下,其他照常用。买断价,一个授权绑三台设备,可以解绑换机。整套验证是离线的,客户端里只有公钥,私钥不打包进去,断网也能激活。
写代码的时候我把这部分当成一个安全工程来做。机器指纹取的是硬件标识,加盐哈希成 MD-XXXX-XXXX-XXXX-XXXX 这样的格式,不碰任何个人信息。试用期记录带 HMAC 签名,手改 JSON 会直接失效。还盯着系统时间,往回拨超过一小时就算篡改,试用立即作废。
我知道这些都能被绕过。这一段的意义是让愿意付钱的人付得心安,跟破解的人较劲没有尽头。
我也把底线写进了文档,你的本地文件,打开和保存这两件事,任何时候都不会上锁。工具不该拿你的稿子当人质。
现在
从 10 月 23 号开工到现在,两周,一百三十多次提交。
对外发布的最新版是 v0.6.0,带公众号排版和那套主题系统。macOS 的 dmg、Windows 的安装包、Linux 的 AppImage 和 deb 都在 Release 页。
写这篇文章的时候 v0.7.0 已经在本地跑通了,就是上面讲的授权那套,还没推上去。等测试和打包流程走完再发,按现在的节奏不会太久。
MIT 许可,源码在 https://github.com/monkeychen/mditor 。
它现在离「我最喜欢的编辑器」还有点距离,我知道缺什么。多窗口没做,协同没做,插件系统我一直在犹豫要不要碰。
但有一件事它已经做到了,打开它,写两个小时,中间不会因为工具本身分心。这对我来说就是及格线。剩下的都是加分。
如果你也是那种一天要在编辑器里待四五个小时的人,可以试试。用之前建议先拿一份自己的稿子,打开,保存,git diff 看一眼。这是我对它最放心的地方,也应该是你对一个陌生工具的第一道检验。