design rust web gfm ast markdown parser migration

This commit is contained in:
lix-2026
2026-05-08 00:44:19 +08:00
parent c620b9e40c
commit ffab095d86
@@ -0,0 +1,203 @@
# 3-13 [process] Rust Web 本地 Markdown GFM AST 解析器迁移方案 v1
> 更新时间:2026-05-08
>
> 关联:
> - `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/local_folder_source.rs`
> - `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/web_shell.rs`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-21-local-folder-convex-unified-tree-source-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-22-local-folder-convex-unified-tree-execution-checklist-v1.md`
## 1. 结论
当前本地 Markdown 渲染链路是 `mnote-web` 自己写的轻量 parser,不是 VSCode 解析器,也不是成熟 GFM AST pipeline。它已经被补到能覆盖一部分常见语法,但本质上仍是“自己造轮子”,后续维护成本会持续上升。
本方案建议把本地 `.md` 读取侧迁移到 Rust 侧成熟 GFM AST 解析器,再把 AST 映射到现有 `PageAggregate` / Tiptap bridge。当前手写 parser 只保留为过渡层,不再作为长期语义来源。
## 2. 当前问题
本地链路现在分成两段:
1. `local_folder_source.rs` 负责读取 `.md`、拆 frontmatter、把正文转成 block。
2. `web_shell.rs` 负责把 legacy block 转成 Tiptap,再把 Tiptap 保存回 legacy block。
问题不在 Tiptap 本身,而在前面的 Markdown 语义输入过窄:
- task list、inline code、bold、italic、strike、link 之前会被吞成纯文本。
- table 只做了最低限度兼容。
- 解析逻辑是手写规则,扩展语法时会不断叠条件分支。
- 读写两端各自维护一份近似规则,容易出现回写不一致。
## 3. 目标
- 用 Rust 侧成熟 GFM AST 解析器替换手写 Markdown parser。
- 保留当前已验证的本地文件夹读写能力。
- 让 Markdown 语法来源尽量统一,减少“读得懂、写不回”的差异。
- 继续支持当前已暴露的语法面:paragraph、heading、task list、list、quote、fenced code、divider、pipe table、attachment link、inline code、bold、italic、strike、link。
- 不改变 `PageAggregate`、树 projection、权限、路由边界。
## 4. 非目标
- 不在这一步追求“所有 Markdown 语法无损互转”。
- 不把 VSCode 源码直接搬进来。
- 不把 Next/Wolai 导入链路里的 `@blocknote/core markdownToBlocks` 作为本地 source 真相。
- 不把 tree/domain、Convex 或 editor island 的所有语义一起重写。
## 5. 方案比较
### 方案 A:继续扩写手写 parser
优点:
- 改动小,能快速补洞。
缺点:
- 语法越来越多时会失控。
- 读写规则难以保证一致。
- 这条路本质上还是重复造轮子。
### 方案 BRust GFM AST + 语义映射层
优点:
- 语法基础来自成熟实现,降低长期维护成本。
- AST 天然适合做 block / inline 双向映射。
- 更容易补齐 task list、table、inline mark 等常见 GFM 特性。
缺点:
- 需要补一层 AST 到 `PageAggregate` 的适配。
- 初期需要做迁移和回归测试。
### 方案 C:复用前端/导入侧解析器
优点:
- 可以少写部分逻辑。
缺点:
- 本地文件夹主链是 Rust 服务,不适合把语义真相放回 JS 侧。
- 导入链路和本地打开链路不是同一职责。
### 推荐
选方案 B。它最符合当前架构:语义在 Rust 侧收口,前端只消费稳定 projection。
## 6. 架构设计
### 6.1 解析层
新增一个本地 Markdown 解析模块,职责只做三件事:
- 读取 Markdown 文本并构建 GFM AST。
- 归一化 frontmatter、正文、块级节点、行内节点。
- 输出可供 `PageAggregate` 使用的中间表示。
建议把手写字符串规则替换成“AST -> 中间 IR -> block”的两段式转换,而不是直接从 AST 拼最终 JSON。
### 6.2 中间 IR
建议引入一个轻量中间层,表达 block / inline 的稳定语义:
- blockparagraph、heading、task_item、list_item、quote、code_block、divider、table、media。
- inlinetext + markscode、bold、italic、strike、underline、link)。
这样做的目的不是再造一套编辑器模型,而是把 Markdown 语义和 Tiptap/legacy block 解耦。
### 6.3 输出层
输出层仍然保留两条现有通路:
- 读入时:Markdown AST -> block document -> `PageAggregate.body.content`
- 保存时:editor document -> block document -> Markdown
`web_shell.rs` 里的 legacy / Tiptap 转换仍然存在,但它应该只负责编辑器适配,不再承担 Markdown 语法识别。
### 6.4 语法优先级
第一批必须稳定支持:
- task list
- inline code
- bold / italic / strike
- link
- pipe table
- fenced code
第二批再扩:
- nested list
- setext heading
- autolink
- footnote
- raw HTML
- block quote 多段落
## 7. 迁移步骤
### Step 1:引入 AST 解析器
先把当前自写 parser 包进一个兼容接口,替换为 Rust GFM AST 实现。
要求:
- 读入结果和现在的 `PageAggregate` 输出保持兼容。
- 已有 smoke 和 Rust 单测先不改大结构。
### Step 2:统一 block / inline 映射
把 AST 到 block document 的映射集中到一个明确模块,不再散在 `local_folder_source.rs` 大文件里。
要求:
- task list 变成真正的 todo / task item 语义。
- inline mark 进入统一的 marks 表达。
- table cell 里的 mark 也能保留。
### Step 3:统一回写
把 block document -> Markdown 的输出改成与 AST 语义对齐的反向映射。
要求:
- task list 回写为 `- [x]` / `- [ ]`
- 行内 mark 回写为标准 Markdown。
- 不支持的结构明确降级,不静默丢内容。
### Step 4:清理临时补丁
当 AST 迁移完成后,删除当前手写 parser 的临时补丁代码,保留测试和兼容层。
## 8. 测试计划
### Rust 单测
至少覆盖:
- task list 读写往返。
- inline code / bold / italic / strike / link 往返。
- table 单元格内 inline mark。
- frontmatter title 保留。
- 未支持语法的降级策略。
### 浏览器 smoke
至少覆盖:
- 本地 `.md` 打开后 checkbox 真正出现在 ProseMirror DOM。
- `code``strong``em``s``a` 在正文和表格单元格中真实渲染。
- 保存后刷新不丢语义。
### 回归边界
- 不破坏 `file_tree` / `page_tree`
- 不改变 local workspace 的只读边界。
- 不影响 Convex workspace 链路。
## 9. 风险
- GFM AST 到 block 的映射会比手写 parser 更清晰,但初期要补一轮语义归一化。
- 复杂 Markdown 结构不可能天然无损,必须明确哪些语法是支持、哪些是降级。
- 如果 AST 解析器选型过于底层,后续表格、任务列表和 inline mark 的兼容代码会变多,所以要优先选能直接拿到 AST 的成熟实现。
## 10. 完成判定
这个方案进入实现阶段的条件:
- 解析器选型明确。
- AST -> block / inline 映射接口冻结。
- 当前手写 parser 的临时补丁有对应回归测试。
- 浏览器 smoke 能稳定证明 task list 和 inline mark 真正渲染出来。