编辑器换代方案(Vditor → 自研)
状态:方案,未实施。 本文只写「打算怎么做、为什么、要动哪些文件」。 起因见 §1;结论一句话:换掉 Vditor,但不自研文本引擎 —— 用 CodeMirror 6 当内核,自己写集成层与装饰层,预览复用本站已有的正文渲染管线。
1. 起因与问题定性
1.1 用户反馈
- 不适应 Typora 的工作流 —— 具体是「单栏就地渲染、源码隐去」;
另有一条补充症状:粘贴代码时编辑器会自作主张补上
```围栏。 - 在编辑器里用不上站内的云剪贴板 / 图床 / 音频床 / 收藏夹;
[@…]语法只能手打,且不能预览。 - Vditor 的 markdown 渲染与正文渲染有格式差别。
1.2 逐条定性
三条的性质完全不同,混在一起谈会得出「配一配就好了」的错误结论。
| # | 症状 | 性质 | 能不能靠配置解决 |
|---|---|---|---|
| ① | 单栏就地渲染、源码隐去 | 内核能力 | 否 |
| ①′ | 粘贴自动补 ``` |
内核行为(同上一条的同一个根) | 否 |
| ② | [@…] 只能手打 |
基建缺口(我们自己的东西没接上) | 是(但得写代码) |
| ②′ | 编辑器内不能预览 [@…] |
结构性 | 否 |
| ③ | 预览与正文渲染不一致 | 结构性 | 否 |
关于 ①:这条不是「缺一个模式」。
src/app/components/BlogForm.tsx 早就在用 mode: 'ir' —— 那正是 Vditor 照 Typora 做的
即时渲染模式。所以用户说的不是「我要单栏,现在是双栏」,而是
「Vditor 的 ir 做得不像 Typora」。它的实现是 contenteditable 加一整套「输入即改结构」
的启发式,粘贴时替用户做决定(自动补围栏就是这一类的产物)。这是内核实现层面的问题,
Vditor 只给了开关,没给「换掉这套启发式」的口子。
关于 ② 的前半:纯缺口,且缺口比想象的窄。
插入基建已经有了,只是只接在讨论 / 评论上(src/app/components/RichComposer.tsx),
没接博客与剪贴板:
src/app/components/textarea-insert.ts—— 往光标处插入一段文本,含三个踩过的坑 (程序化赋值不触发 React onChange、必须等提交后再量高、先 focus 再 setSelectionRange)src/app/components/StickerPicker.tsx、UserPicker.tsx、ImagePickerModal.tsxsrc/lib/image-client.ts的uploadImageFile()—— 完整的浏览器侧图床上传 (XHR 绕微信 X5、弱网重试一次、总体积闸门),与编辑器无关
真正缺的是五个取数面板:收藏夹、图床、音频、投票、剪贴板。
现在它们连个入口都没有 —— 收藏夹页面(src/app/favorite/FavoriteMenu.tsx)只是显示一句
「写成 [@xxxxxx]」让用户自己复制 ID。
关于 ②′ 与 ③:结构性,根因是同一个 —— 两套渲染器。
| 渲染器 | |
|---|---|
| 编辑器预览 | Vditor 自带的 lute |
| 正文终稿 | src/app/components/MarkdownRenderer.tsx(marked + DOMPurify + ContentRefProcessor) |
两套渲染器 → 必然漂移,且 [@…] 的展开(要发请求取剪贴板正文、投票数据、收藏夹卡片)
在 Vditor 的预览里没有地方挂。所以
docs/guide/内容引用语法指南.md 的「八、注意事项」里白纸黑字写着「编辑器中不可预览」。
Vditor 有一个渲染口子,但它够不到要害 —— 这是「高摩擦的非零解」,不是干净替换。 查过 vditor 4.0.0 的类型定义(见 §2.2 的来源说明):
- 够得到的:
IPreviewOptions.renderers?: ILuteRender有约 80 个按节点覆盖 HTML 输出的 回调(renderHeading/renderTable*/renderCodeBlock*/renderMath*/renderLink…), 挂在Vditor.md2html()/Vditor.preview()这两个静态方法上。另外实例上还有preview.transform?(html)与preview.parse?(element)两个后钩子。 - 够不到的:
renderers只在IPreviewOptions上,不在IOptions上 ——new Vditor(...)的实时编辑器够不着它。而编辑器内联那块 IR 编辑面本身由 Vditor 内部产出,没有任何「换皮」的口子,只有上面那两个后钩子。
也就是说:想让预览等价于站点渲染,得用约 80 个回调在 Lute 的节点模型上重实现一遍我们的 渲染器,而且换掉的只是另开一个预览面板——用户盯着的那个就地渲染的编辑区,还是它自己那套。 投入产出比明确为负,结论不变:换掉 Vditor。
1.3 现状盘点
全站只有两处实例化,但为「跟 Vditor 相处」写的代码量已经超过编辑器本身的业务逻辑:
| 位置 | 用途 |
|---|---|
src/app/components/BlogForm.tsx |
博客发布 / 编辑 |
src/app/clipboard/upload/UploadForm.tsx |
云剪贴板新建 / 编辑 |
围绕它们的自研代码:
src/lib/vditor-upload.ts(195 行)—— 把「Vditor 的上传协议」翻译成 「本站/api/images协议」(fieldName: 'file'、format回调、去重后缀插在扩展名前…)src/lib/vditor-theme.ts(89 行)—— 三条主题轨道;其中syncHljsTheme是为了绕开「Vditor 拼.css而 npm 包里只有.min.css」, 于是抢先占住#vditorHljsStyle这个 idscripts/copy-vditor-assets.mjs(190 行)+postinstall一环 —— 把资源拷进public/static/vditor/,否则图标 / 高亮 / 导出全 404public/static/js/core/base.js的if (input.closest('.vditor')) return;—— 2026-10 线上事故的产物(否则工具栏里凭空多出「选择文件 / 未选择文件 / ×」三件套)src/styles-scss/pages/_clipboard.scss里靠#clipboard-editor把特异性抬到 0-1-1 的一段 —— 不这么做就会被后加载的vditor/dist/index.css的 1px 描边 + 3px 圆角盖掉
测试四个文件、文档六处都在钉这些细节(清单见 §6)。这是一笔持续付的税。
2. 结论与选型判据
2.1 一句话判据
新内核不能自带 markdown 渲染器。
③ 的根因是「预览渲染器 ≠ 正文渲染器」。任何自带渲染器的方案都会把同一个问题原样搬过来 —— 换掉 Vditor 只是换了一个名字不同的 lute。
由此推出三条判据:
- 要的是文本引擎,不是渲染器。
- 输入法必须可靠。 中文站点,contenteditable 系内核的 composition 处理是长期雷区; 用户反馈里那条「粘贴被改写」就是这一类的近亲。
- 能挂行内装饰。 只有能把「一段区间换成 widget」的内核,才谈得上「源码隐去 + 就地渲染」。
2.2 候选
版本 / 日期 / 许可来自 npm registry 与 GitHub API,查证日期 2026-10-06。
| 候选 | 最新版(日期) | 内核 | 自带渲染器 | 能复用自己的渲染 | 行内装饰 | 判断 |
|---|---|---|---|---|---|---|
| Vditor | 4.0.0(2026-08-30) | 自研 contenteditable(ir/sv/wysiwyg 三态),引擎是 Lute(Go→WASM) | 是 | 只能逐节点覆盖,够不到内联编辑面(§1.2) | 否 | 正是要换掉的 |
| CodeMirror 6 | state 6.7.6 / view 6.43.13(2026-09-22) | 纯文本 + 装饰 API | 否 | 是 | 是(Decoration / WidgetType) |
选定 |
| Milkdown | @milkdown/core 7.22.2(2026-09-23) | ProseMirror + remark | 是 | 否,且 markdown 是 PM 文档的派生(往返有损) | 是 | 否 |
| ByteMD | 1.22.0(2025-02-12,此后无新版) | CodeMirror 5 + remark 渲染 | 是 | 只能部分 | 否 | 否(已基本停更) |
| ProseMirror 自建 | prosemirror-markdown 1.13.8(2026-09-21) | ProseMirror | 否 | 要自己写(正文管线是字符串进字符串出,接不进 PM 文档模型) | 是 | 代价高于 CM6 |
| Lexical | 0.52.0(2026-09-28) | Meta,富文本框架 | 否 | 同上;markdown 只是导入导出层,不是内核 | 是 | 否(仍 0.x,IME issue 多) |
| HyperMD | 0.3.11(2018-10-07) | CodeMirror 5 | 否 | — | 是 | 已死,勿选 |
| contenteditable 从零自研 | — | 无 | 否 | 是 | 自己造 | §2.3 |
两条关于「看起来该排除、其实不能靠翻 GitHub 排除」的事实,写下来免得下次误判:
- ⚠️ CodeMirror 与 ProseMirror 的 GitHub 主仓是
archived状态,但项目没死。 作者 Marijn Haverbeke 把开发搬到了自有 forgecode.haverbeke.berlin, npm 上的@codemirror/*一直在发版(state/view在 2026-09-22 同日发版)。 只看 GitHub 的最后提交日期会得出「CM6 已停更」的错误结论。 - Vditor 已经出到 4.0.0,而本站装的是
^3.10.7。也就是说「升级 Vditor 大版本」 在纸面上也是一条备选 —— 但它一条痛点都打不掉(§1.2 的三条分别来自 内核行为、我们的基建缺口、两套渲染器),所以不单列。
CodeMirror 6 的两个支撑事实:
- GFM 开箱即用。
@codemirror/lang-markdown的markdownLanguage= CommonMark + GFM(表格 / 任务列表 / 删除线) + 上下标 + emoji (由@lezer/markdown的扩展集提供)。中文用户常用的语法没有缺口。 - 有直接可抄的参考实现。
@atomic-editor/editor(MIT,React,0.6.2)做的就是 这件事:非光标行收起标记、图片/表格/任务列表行内渲染、虚拟化只渲染视口、- [ ]变可点复选框。形态与本方案 §3.3 / §3.4 几乎一致,装饰与原子区间的写法 可以直接读它的源码。另可参考@retronav/ixora(同思路,但 2023 后停更) 与 Yandex 的@gravity-ui/markdown-editor(PM + CM6 混用,证明路线可行)。
体积(bundlephobia,gzip):@codemirror/lang-markdown 约 175KB(含 lezer 语言依赖)、
view 约 79KB、state 约 16KB;CM6 可 tree-shake,实际载荷更小。
对照:Vditor 主 JS 约 70KB,但 npm 包 unpackedSize 约 23.6MB(全量资源,
现在靠 scripts/copy-vditor-assets.mjs 往 public/static/vditor/ 拷)。
2.2.1 「只给 Vditor 补面板、不换内核」能补到什么程度
这是本次没有选的一条路,但它不是「什么都做不了」,记下边界免得日后翻案时靠猜:
- ✅ 能补:
IHintExtend可以注册一个触发前缀(@//),返回候选列表 (html展示 +value文本),且hint可以返回 Promise(异步取数)。 也就是说「手打[@…]」这条用 Vditor 也解得掉 —— 面板 + hint 下拉即可。 - ✅ 能补一半:另开一个预览面板,用
Vditor.md2html(md, { renderers })逐节点接管输出。 - ❌ 补不掉:编辑器就地渲染那块换不了皮(③ 对用户最直观的那一半), 粘贴时自作主张的行为改不掉(①′),装饰层也没有(① 的「源码隐去」做不细)。
所以判据不是「Vditor 能不能补」,而是「补完还剩几条痛点」。答案是三条里还剩两条半。
2.3 明确不做的:从零写文本引擎
不要写 contenteditable WYSIWYG。 选区与 Range、输入法 composition、撤销栈、 粘贴净化、跨浏览器差异 —— 每一项都是数月量级,而且Vditor 让我们痛的正是这一层。
要写的是集成层:工具条、取数面板、装饰规则、上传、主题、草稿。 这恰恰是现在被 Vditor 挡在外面的那一层 —— 我们不是「自己造一个 Vditor」, 是「把 Vditor 挡着的那一段拿回来自己写」。
CodeMirror 6 提供文本引擎(文档模型、选区、撤销、输入法、视口渲染), 我们只提供扩展。这个分界是本文档所有取舍的基础。
3. 架构
3.1 分层
MarkdownEditor(新,博客与剪贴板共用)
├── EditorShell —— 工具条 / 取数面板挂载 / 状态条 ← 我们写
├── CodeMirror 6 实例 —— 文本引擎、快捷键、撤销栈 ← 现成
├── 装饰扩展 —— 隐藏标记 / widget ← 我们写
└── 取数面板群 —— 复用 3 个 + 新写 5 个 ← 混合
3.2 复用清单
这是本方案最省的部分 —— ② 的前半几乎不用写新逻辑:
| 需求 | 现成的东西 |
|---|---|
| 图床上传(选图 / 拖拽 / 粘贴 / 多选 / 重试 / 微信 X5 绕法 / 体积闸门) | src/lib/image-client.ts |
| 从图床选已有图 | src/app/components/ImagePickerModal.tsx |
| 表情 | src/app/components/StickerPicker.tsx |
| 用户名片 | src/app/components/UserPicker.tsx |
| 光标插入语义(含三个坑) | src/app/components/textarea-insert.ts |
| hljs 亮 / 暗双主题 | MarkdownRenderer.tsx 的 useHljsThemeStyles(见 §3.6) |
| 正文渲染 / 引用展开 / 数学 / 高亮 | src/app/components/MarkdownRenderer.tsx |
新写的只有五个取数面板(收藏夹 / 图床 / 音频 / 投票 / 剪贴板)。
它们形状相同:列一页 + 搜索 + 点一下回 token,可照 ImagePickerModal.tsx 的骨架做。
3.3 装饰层
分两类,这个划分决定了实现的复杂度:
同步 widget —— 文档一改就能画,不需要任何请求(raw 路由直接给字节):
[@10位图床ID]→<img src="/api/images/<id>/raw">[@音频/<ID>]→<audio controls>(注意上限 3 个)[@合集/表情]→ 行内<img>$$…$$/$…$→ 公式
异步 widget —— 要先取数:
[@8位剪贴板ID](回正文)、[@9位投票ID](投票数据)、[@6位收藏夹ID](卡片)、[@用户/用户名](名片)
CodeMirror 的装饰必须在 ViewPlugin 里同步算出来,所以异步那条走标准模式:
异步解析器(防抖后跑,复用 §3.5 的解析口)
│ dispatch(StateEffect)
▼
StateField<Map<tokenKey, ResolvedRef>> ← 已解析结果的驻留地
│ 读 (doc, field)
▼
ViewPlugin → DecorationSet
纪律:widget 用的解析结果与终稿渲染用的是同一个来源。 否则我们就造出了第三套 渲染 —— 正是这次要消灭的东西。这一条要写成守卫(§7)。
3.4 「源码隐去」到底隐什么
照 Typora 的口径:光标所在的行显示源码,其余行隐去标记。
所以装饰规则必须知道光标位置 —— 这是 ViewPlugin 里读 selection 的事,
不是「整篇一律隐藏」。
@atomic-editor/editor(§2.2)做的正是这个行为,它的 README 原话是
「raw syntax appears only on the line your cursor is on」。先读它的实现,
再写我们的 —— 这一段不必从零想。
落到本站的语法,要隐的是:# 标题符、** / * / ~~、[]() 的括号与 URL、
![]()、>、列表符、``` 围栏(但围栏里的内容是代码,不隐)。
[@…] token 属于「整段换成 widget」那一类,不适用「光标行显示源码」——
光标进到 widget 里时退回显示 token 原文即可。
3.5 前置重构:把 ContentRefProcessor 抽出来
ContentRefProcessor 现在是 src/app/components/MarkdownRenderer.tsx 里的私有类
(第 60 行起)。编辑器要在每个 token 上用它,必须:
- 抽到
src/lib/content-ref-processor.ts,MarkdownRenderer改为 import; - 暴露「按 token 解析」的口子(现在只有「整篇
preprocess」这一个入口); - 缓存要能跨渲染复用 —— 现在每次
new ContentRefProcessor(...)都是新实例, 缓存随实例一起丢。「每敲一个字重渲染一次」的编辑器撞上这个,会对每个引用 反复发请求。
这是 M0 的先决条件,不是可选项。
3.6 主题
MarkdownRenderer.tsx 里的 useHljsThemeStyles 已经解决得比 vditor-theme.ts 更好:
插两份 <style id="hljs-theme-light|dark">,靠 media='not all' 切换哪份生效 ——
不换 href、不用抢 id、天然没有「离开编辑页要摘掉全局 <link>」那个问题
(那个问题现在由一条 e2e 用例如实描述着)。
所以:把 useHljsThemeStyles 抽成公用 hook,删掉 syncHljsTheme /
removeHljsTheme,以及 BlogForm 里「必须在 new Vditor 之前调用」那个时序陷阱。
编辑器外壳跟随 <html data-theme>,颜色一律取 docs/frontend-styles.md 的令牌。
3.7 草稿与自动保存
- 博客:Vditor 的
cache: { enable: true, id: 'blog-upload-editor' }提供 「新建时保留草稿」。自研要自己写一个(localStorage,键名保持同形)。 编辑态本来就是cache: { enable: false }。 - 剪贴板:自动保存是
UploadForm自己实现的(每分钟,偏好存在localStorage.clipboard_autosave_enabled),与 Vditor 无关,不动。
4. 分期
每期都能独立发布、独立回滚。
| 期 | 内容 | 打掉哪条反馈 |
|---|---|---|
| M0 | 抽 ContentRefProcessor、抽 hljs 主题 hook、立「禁止第二个渲染器」的守卫 |
— |
| M1 | MarkdownEditor 内核替换:CM6 + markdown 语言包 + 工具条 + 快捷键 + 上传/拖拽/粘贴 + 草稿 + 主题;装饰层先只做隐藏标记 |
①(含 ①′:粘贴不再被改写) |
| M2 | 五个取数面板接上工具条 | ②「手打」 |
| M3 | widget:先同步那批(图床图 / 音频 / 公式),再异步那批(剪贴板 / 投票 / 收藏夹 / 名片) | ②′「不能预览」、③ |
| M4 | 表格(可选,见 §6) | — |
M1 就该换掉博客与剪贴板两处 —— 留一个用 Vditor 会让我们同时维护两套,
而 vditor-* 那两个 lib 又必须为留下的那一处继续存在。宁可一次换完。
5. 迁移清单
5.1 新增
docs/editor-plan.md(本文)src/app/components/MarkdownEditor.tsx—— 外壳与状态src/app/components/markdown-editor/—— 工具条、五个取数面板、草稿src/lib/md-editor/—— 装饰规则、widget、异步解析器接线、快捷键src/lib/content-ref-processor.ts(从MarkdownRenderer.tsx抽出)src/lib/use-hljs-theme.ts(同上)- 依赖:
@codemirror/state/view/commands/language/search/lang-markdown+@lezer/highlight(最终清单以实际 import 为准,宁少勿多) - 测试:装饰规则与 token 插入的单测、§7 那几条新守卫、改写后的 e2e
5.2 修改
src/app/components/BlogForm.tsx、src/app/clipboard/upload/UploadForm.tsxsrc/app/components/MarkdownRenderer.tsx(改用抽出的模块)src/styles-scss/pages/_clipboard.scss(删#clipboard-editor那段特异性 hack; 补编辑器外壳样式)public/static/js/core/base.js(删input.closest('.vditor')早退及其注释)package.json(删依赖vditor与prepare:vditor/vditor:check, 以及postinstall里的对应一段).gitignore(删/public/static/vditor/那条)src/lib/image-client.ts(第 83 行提到 Vditor 路径固定compress=1的注释)- 文档:
docs/architecture.md§5 的表两行、docs/deploy.md、docs/frontend-styles.md、docs/legacy-constraints.md、docs/guide/图床使用指南.md、docs/guide/内容引用语法指南.md的「编辑器中不可预览」一条要删掉 docs/README.md(索引表加一行)与src/lib/docs-catalog.ts(登记本文)
5.3 删除
src/lib/vditor-upload.ts、src/lib/vditor-theme.tsscripts/copy-vditor-assets.mjstests/e2e/vditor-theme.spec.ts、tests/e2e/vditor-upload.spec.ts、tests/unit/vditor-upload.test.tstests/unit/base-js-filepick.test.ts里 Vditor 相关的两条用例tests/e2e/global-setup.ts里「vditor 资源不存在就自愈重跑」那一段
6. 会丢的东西
| 能力 | 判断 |
|---|---|
| 表格编辑 | 唯一真会疼的。 见下 |
| 导出(HTML / PDF) | 低频。真想要就单独立项 |
| 大纲面板 | 可以和「文章目录」共用逻辑,不难但也不急 |
| 全屏 | 不值一提 |
| 字数统计 | 自己写很便宜 |
表格建议在 M4 单独立项,且不追求 Typora 那种可视化表格(它是整个方案里 唯一的深水区)。退路有两种:源码 + 对齐高亮;或者做一个「表格编辑弹窗」, 覆盖插入 / 增删行列 / 对齐这些常见操作,改完写回源码。
7. 测试与守卫
7.1 要新立的守卫
静默错是这个仓库最在意的一类故障,编辑器换代至少引入三种新的静默错:
- 只有一个渲染器。 静态扫
src/lib/md-editor/与MarkdownEditor: 不许 importmarked再自己拼 HTML;widget 一律走共用的解析口。 形状照tests/unit/vditor-upload.test.ts里那条「全仓扫new Vditor(」的契约测试。 - token 正则同源。 编辑器插入的 token 必须通过渲染侧的正则校验,
且正则里的字符集是白名单。理由与本仓表情包那条纪律完全同源:
宽一个
\s*就是向同名用户凭空发通知。 - 插入器与解析器不漂移。 对每个面板产出的样例 token 跑一次真实解析 —— 防的是「面板改了 token 形状、渲染侧不认」,症状是「插进去的东西渲染不出来」。
7.2 e2e
- 粘贴含
```的文本不应被改写 —— 这是本次反馈的回归钉子,必须有一条 - 工具条五个面板各插一次,正文渲染出对应元素
- 主题跟随(亮 / 暗切换,编辑器与正文一致)
- 图床上传:拖拽 / 粘贴 / 多选,插入的结果能被正文渲染
- 不污染文章页:离开编辑页后,文章页的代码高亮主题照旧
(这条现在由
vditor-theme.spec.ts的一条用例守着,改写时要保留意图而不是实现)
⚠️ 改写旧 e2e 时注意:那四个文件钉的多是Vditor 的实现细节
(.vditor--dark 类名、#vditorContentTheme 的 href、#vditorHljsStyle)。
新用例要钉行为(主题确实变了、上传确实插进来了),别换成新实现细节 ——
否则下一次换内核还要再废一遍。
8. 风险
- 输入法 composition。 CM6 本身成熟,但装饰层必须在 composition 期间暂停 —— 否则用户正在拼的字会被 redraw 打断。这是已知的、必须专门处理的一类 bug, 不是「但愿不要碰到」。
- 表格。 唯一的真难点,见 §6。
- 依赖体积。 CM6 是一串包(gzip 量级:
lang-markdown约 175KB 含 lezer 语言依赖、view约 79KB、state约 16KB;可 tree-shake)。 而本仓的纪律是npm ci(严格按 lockfile),加依赖必须显式装、 把package-lock.json一起提交,并留意postinstall那三条copy-*-assets的连带影响。净账是正的:Vditor 那条线要拷 23.6MB 资源进public/。 - 两处编辑器同时换。 缓解:先换博客(反馈的源头),剪贴板紧随;
两者共用
MarkdownEditor与EditorShell。 - 旧测试全废。 四个文件都要改写或删除,见 §7.2。
- 异步 widget 的请求量。 「每敲一个字重渲染」撞上「每个引用发一次请求」
会把限频打爆(
docs/architecture.md§6.5 的RULES)。 缓解:§3.5 的第 3 条(缓存跨渲染复用)+ 解析防抖 + 结果驻留在StateField里。
9. 未决问题(需要站长定)
- 编辑器里插入图床图,插哪个形状?
[@10位ID](本站语法:渲染器认、有条数上限、与其它引用一致) 还是(标准 markdown:离开本站也能看)。 现在 Vditor 的上传按钮插的是后者。两者都能被正文渲染器正确处理。 - 表格做到哪一步(不做 / 源码 + 对齐 / 弹窗编辑)。
- 「光标行显示源码」是否照搬 Typora。 有人反而嫌它在光标移动时「闪」。
- 导出功能是否保留。
- 装饰层的边界:是否连
>引用块、任务列表也做可视化(工作量大头在细节, 不在机制)。
10. 为什么这件事值得做(一句话总结)
现在为「跟 Vditor 相处」而写的代码 —— 上传适配、主题三轨、资源拷贝、
base.js 的绕行、SCSS 的特异性 hack —— 比编辑器本身的业务逻辑还多,
而且每一项都是为了让第三方内核接受我们的规则。
换成 CM6 之后,我们写的东西第一次全是自己的规则:预览就是终稿渲染器,
[@…] 的解析只有一份,主题跟着既有的那一套走。
这不是「自己造一个编辑器」,是把被 Vditor 挡在外面的那一层拿回来。