Sätteri 与 unified:Markdown 处理的两条路线
前几天看到一个消息,Astro 7 正式发布,默认换上了 Rust 写的 Sätteri 作为 Markdown 处理器。构建速度更快,插件更轻量,听起来一切都很美好。我的站点正好用 Astro,就顺手升级了。
结果 build 直接报错。原来是 Sätteri 不再内置 unified 的那套流水线,而我在 astro.config.ts 里配的 remark-math 和 rehype-katex 都是基于 unified 生态的插件,现在不兼容了。
我一开始还以为是配置问题,花了不少时间查文档。后来发现,Sätteri 的 AST 跟 MDAST 完全不是一回事,没有现成的 math 插件可以用。想继续用数学公式,要么自己写一个 Sätteri 插件,要么装回 @astrojs/markdown-remark 继续走 unified 的老路。
unified 的流水线
先说说 unified 是怎么工作的。Markdown 文本先进 remark-parse,出来的是一棵 MDAST 语法树。然后 remark 插件在这棵树上做变换,接着 remark-rehype 把树转成 HAST,rehype 插件继续改,最后 stringify 成 HTML。
这条流水线能跑通,全靠规范。MDAST 规定了 Markdown 节点长什么样,HAST 规定了 HTML 节点长什么样,unist 又给了共同的底层接口。三层规范叠在一起,插件之间才能互相协作。
remark-math 在 MDAST 阶段把 $...$ 识别成 math 节点,rehype-katex 在 HAST 阶段把这些节点转成 KaTeX 渲染后的 HTML。两个插件并不直接通信,但通过共享的 AST 规范间接配合。这种生态的丰富程度,是经过十年积累才有的——几百个插件,几乎你想得到的需求都有人做过。
但层数多了,AST 要被反复遍历。规范卡得太死,想做的事情如果不符合节点定义,就得绕路或者自定义类型。代价确实存在,只是以前没人太在意,因为生态太舒服了。
Sätteri 的扁平化
Sätteri 的做法完全不同。解析完 Markdown 直接得到一棵树,插件拿到树随便改。没有 MDAST,没有 HAST,没有 remark 和 rehype 的边界。
Rust 本身比 JavaScript 快一个数量级,省掉中间层的多次转换后,构建时间确实能缩不少。插件写起来也更直接,不需要 visitor 模式,拿到树,找到节点,改了就行。
我在 Astro 7 里第一次体验这种架构时,构建速度的提升是能感觉到的。但当我试图把 remark-math 和 rehype-katex 加回去的时候,问题就来了——Sätteri 不认识这些插件,因为它们操作的是 MDAST 和 HAST,不是 Sätteri 自己的 AST。
想继续用数学公式,理论上可以写一个 Sätteri 的 math 插件。但 Sätteri 的 AST 是它自己定的,没有社区共识。你写的插件,别的插件可能不认识。unified 里那种插件链上各自处理、最终拼出完整页面的模式,在这里很难复现。
也就是说,我不是在换一个更快的处理器,而是在换一个全新的、几乎没有插件的生态。这跟当年从 Webpack 切到 Vite 还不一样,Vite 至少兼容 Rollup 插件。Sätteri 这边,你基本得从零开始。
那我现在怎么办?
降级回 v6。