如何理解 ZenNotes 的 Markdown 编辑器:CodeMirror 6 实时预览与图表渲染实现原理
【免费下载链接】zennotesKeyboard-first local Markdown notes with Vim motions, diagrams, and MCP integration.项目地址: https://gitcode.com/gh_mirrors/zenn/zennotes
ZenNotes 是一款键盘优先的本地 Markdown 笔记应用,它基于 CodeMirror 6 构建了现代 Markdown 编辑器:光标离开某行时,标题的#、加粗的**等符号会自动淡出,实时预览出排版效果;再移回去,符号又回来供你编辑。Mermaid 流程图、KaTeX 数学公式也遵循同样的"所见即所得"逻辑。本文将用通俗的方式讲清楚这套实时预览与图表渲染的实现原理,适合想了解编辑器内部机制的新手。
为什么选 CodeMirror 6 作为编辑器内核
现代 Markdown 编辑器面临一个两难:用户想看到渲染后的效果(像富文本),但底层又必须是纯文本(方便 Git 版本管理、MCP 工具直接操作)。
ZenNotes 的架构文档 how-zennotes-works.md 提到一个关键设计:笔记就是磁盘上的普通文件,不藏在任何数据库里。这意味着编辑器必须做到——看起来是 WYSIWYG,存下来的却是标准 Markdown。
CodeMirror 6 恰好为这种需求而设计。它不像传统编辑器那样"一个巨型组件",而是拆成一组小积木:
| 模块 | 职责 | 类比 |
|---|---|---|
@codemirror/state | 纯文本状态与事务 | 乐谱 |
@codemirror/view | 屏幕渲染与装饰 | 舞台 |
@codemirror/language | 语法解析(Lezer) | 语法老师 |
@codemirror/lang-markdown | Markdown 语法定义 | 课文 |
你可以在 packages/app-core/package.json 里看到这套依赖,还包括mermaid、katex等图表渲染库。ZenNotes 的实时预览正是靠**插件(Extension)**机制,往这堆积木上叠加一个个功能:隐藏符号、渲染公式、画流程图……每个插件只管一件事,互不干扰。
实时预览的核心:一行插件让符号"隐身"
实时预览的入口在 cm-live-preview.ts。文件开头的注释把这个策略说得非常直白:
Obsidian 风格的所见即所得。当你离开某行时,
#、**、`等标记淡出,标题/加粗/链接干净地渲染出来;当光标回到该行,标记又会回来供你编辑。
它的工作原理可以拆成三步:
语法树识别符号位置。CodeMirror 的 Lezer 解析器会把
# Title解析成一棵语法树,其中HeaderMark(井号)、EmphasisMark(星号)、CodeMark(反引号)等节点就是"语法标记",全部记录在 cm-live-preview.ts 的常量表里。判断光标在哪。每次光标移动或选区变化,插件检查当前行是否被光标"占用"。
用 Decoration 装饰器打补丁。被光标占用的行原样显示;没被占用的行,用
Decoration.replace把符号"替换掉"(视觉上消失),或替换成渲染后的组件。
这种"装饰"不修改原文本——磁盘里的文件永远是干净的 Markdown,消失的只是屏幕上的符号。这是 CodeMirror 6 最优雅的地方:状态层只关心文本,展示层用装饰自由发挥。
上图中不同下划线样式区分了 wikilink 的解析状态:已有笔记的链接实线,未解析的虚线——这正是装饰器叠加在语法树之上的效果
图表渲染:Mermaid 流程图如何"懒加载"
在笔记里写一段 ```mermaid 代码块,光标离开它时,流程图直接画出来。这个功能在 cm-mermaid-render.ts 里实现,注释里提到了两个让它"便宜到可以跑在高频编辑器上"的关键优化:
1. 懒加载。Mermaid 是整个渲染端最重的依赖之一。插件只在文档里真的存在 mermaid 围栏代码块时才去import它——一张没有图表的笔记永远不会拖入这个最重的代码块。
2. SVG 缓存。渲染结果按(源码 + 主题完整身份)缓存。你在笔记别处敲一个键,触发整篇重绘时,已渲染的图直接从缓存取,零开销。
还有一个体验细节很值得新手学习:图表编辑到一半通常是语法不合法的(比如正在写箭头),此时如果每次按键都报错,屏幕会疯狂闪烁。ZenNotes 的做法是——渲染失败就保留该块上一次好的画面,直到你离开这个块、而它依然解析不了,错误提示才接管。区别"正在输入的图"和"写错的图",是体验好坏的分水岭。
数学公式走的是同样的思路,见 cm-math-render.ts:行内$…$渲染成行内公式,块级$$…$$渲染成居中展示公式;光标落在哪个公式上,哪个公式就还原成源码。它还支持按笔记标签选择 KaTeX 或 Typst 排版器。
多个插件如何协作:一次组合测试就够了
实时预览、表格、任务复选框、wikilink 渲染……十来个插件同时挂在一个文档上,会不会互相打架(比如两个装饰器重叠冲突)?
答案是靠一个组合测试保证:cm-wysiwyg-compose.test.ts 把所有 WYSIWYG 插件按EditorPane中的真实方式一起加载,用一份"富文档"(标题 + 加粗 + 标签 + wikilink + 引用 + 表格 + 任务 + 代码块)验证它们能同时共存、一次性全部渲染。每个插件另有单元测试,组合测试则专管"大家在一起"这件事。这种单插件单测 + 组合集成测试的分层思路,对任何插件化系统都适用。
ZenNotes 编辑器:wikilink 在编辑态渲染为可点击链接,右侧 Connections 面板实时汇总链接关系
键盘优先:编辑器不只是"看",还要"飞"
渲染效果只是半壁江山。ZenNotes 的另一半是 Vim 运动:在编辑态用gd可以直接跳转到 wikilink 指向的笔记或文件(上图中Other、diagram.png这类链接都可以跳),无需鼠标。
编辑器内的绘图功能同样基于这套嵌入渲染机制——用![[drawing.excalidraw]]把白板画嵌入 Markdown,切换不同画板时各画独立的会话保存、互不覆盖,这个数据丢失修复的完整背景见 RELEASE_NOTES.md。
小结:三句话记住实现原理
- 装饰器是灵魂:实时预览不改文本,只改显示——Lezer 语法树找出符号位置,Decoration 让光标离开的行"隐身"。
- 图表要省钱:懒加载最重的库 + 按(源码、主题)缓存渲染结果 + 编辑中途保留上次成功画面。
- 插件分层验证:每个 WYSIWYG 插件独立单测,再加一个组合测试确保它们在同一文档上共存。
这套"纯文本状态 + 展示层装饰"的架构,让 ZenNotes 既是键盘党飞起来的 Vim 编辑器,又是图表渲染的现代 Markdown 编辑器——而磁盘上那份文件,始终是干净的.md。
【免费下载链接】zennotesKeyboard-first local Markdown notes with Vim motions, diagrams, and MCP integration.项目地址: https://gitcode.com/gh_mirrors/zenn/zennotes
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考