Files
mnote/design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md
T
lix-2026 d148e1ccc2 feat: 收口本地文件夹入口并推进 page aggregate rust-first
- 为 Rust Web 主入口补齐本地文件夹/云空间切换、最近目录与路径回填体验\n- 对齐 local markdown media 与 inline marks 的 Rust shell / TipTap converter 语义\n- 让 documents/page 优先消费 Rust page aggregate snapshot,并保留 TS fallback\n- 补强 tree live、local markdown 与主入口 smoke,并同步设计稿状态
2026-05-09 19:05:06 +08:00

11 KiB
Raw Blame History

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。

  • codestrongemsa 在正文和表格单元格中真实渲染。

  • 保存后刷新不丢语义。

回归边界

  • 不破坏 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 真正渲染出来。

11. 详细 checklist

11.1 选型与边界冻结

  • 确认 Rust 侧 Markdown AST 解析器选型。

  • 明确是否需要 GitHub Flavored Markdown 全集,还是只保留当前本地链路所需的 GFM 子集。

  • 冻结 frontmatter 处理方式:解析器只解析正文,还是 frontmatter 也由同一模块统一处理。

  • 冻结 AST 输出到中间 IR 的边界,不直接在 AST 层拼 PageAggregate

  • 明确 table、task list、inline mark、fenced code、attachment link 的优先级。

  • 明确不支持语法的降级策略,要求可解释且可测试。

11.2 AST -> 中间 IR

  • 建立 MarkdownAstDocument / 等价中间结构。

  • 建立 block 级节点映射:paragraph / heading / list / task / quote / code / divider / table / media。

  • 建立 inline 级节点映射:text / code / bold / italic / strike / underline / link。

  • 保证 task list 的 checked 状态进入正确的 task item 节点,而不是外层 list 节点。

  • 保证表格单元格内的 inline mark 不丢失。

  • 保证空段落、空表格单元格、空引用块的处理规则固定。

  • 统一处理标题优先级:frontmatter title > H1 > 文件名。

11.3 中间 IR -> PageAggregate / block document

  • 把 AST 到 PageAggregate.body.content 的映射集中到单一模块。

  • 消除 local_folder_source.rs 里散落的手写语法分支。

  • 让 block document 输出保持与 web_shell.rs 兼容。

  • 保留 block id 稳定性,避免刷新后整篇文档重建导致的局部状态抖动。

  • 保留 frontmatter 原文和页面设置写回边界。

  • 对 unsupported 节点采用明确降级,而不是静默丢语义。

11.4 Inline mark 双向转换

  • code 解析为 inline code mark。

  • bold 解析为 strong mark。

  • italic 解析为 emphasis mark。

  • strike 解析为 strike mark。

  • link 解析为带 href 的 link mark。

  • 保存回写时按同一映射反向输出 Markdown。

  • 确保 table cell 内的 inline mark 在读写两端都保留。

  • 确保 legacy block -> Tiptap -> legacy block 不再丢 marks。

11.5 Task list 双向转换

  • - [x] / - [X] 进入 checked task item。

  • - [ ] 进入未勾选 task item。

  • task item 文本内容走 inline IR,不走纯文本拼接。

  • Tiptap 的 taskList / taskItem 与 legacy todo 对应关系固定。

  • 保存回写时 task item 重新输出为标准 Markdown checkbox 语法。

  • 浏览器 smoke 断言 checked / unchecked 两种状态都出现。

11.6 Table 处理

  • 统一 pipe table 解析规则。

  • 单元格内容通过 inline IR 输出,不再只保留纯文本。

  • 表头 / 普通单元格类型在 AST 映射中保持稳定。

  • 保存回写时保留表格分隔行和列数对齐。

  • 列对齐、空单元格、带 mark 单元格的降级策略要写入测试。

11.7 保存回写

  • 保存链路只依赖 block document / editor document,不依赖 Markdown 手写规则。

  • 前端编辑器保存路径仍能写回 .md

  • task list、inline mark、table 的回写结果可再次被 AST 解析器读回。

  • 保留 frontmatter title / mnote_id / 页面设置写回逻辑。

  • 保存失败时保留编辑器状态并给出可解释错误。

11.8 回归测试

  • local_markdown 测试组覆盖基础块、task list、inline marks、table、附件链接。

  • 新增 AST 解析器单测,验证解析结果和当前手写 parser 预期一致或更强。

  • 新增保存回写单测,验证 round-trip 不丢 task / marks。

  • 新增 web shell 单测,验证 legacy <-> Tiptap 转换不丢 marks。

  • 新增浏览器 smoke,验证 checkbox、code、strong、em、strike、link、table cell marks。

  • 确认本地 .md 读写 smoke 不影响 file tree / page tree / page aggregate 路由。

11.9 清理与迁移收尾11

  • 把手写 parser 标记为过渡实现。11

  • 移除 local_folder_source.rs 中不再需要的临时解析函数。

  • 移除 web_shell.rs 中只为补丁存在的临时适配分支。

  • 保留回归测试和兼容层,不删除验证资产。

  • 迁移完成后把设计稿状态从 process 移到 done