本文是对 Introducing arborium, a tree-sitter distribution 的整理与翻译。
内容结构概览
- 问题起点:docs.rs 上非 Rust 代码块通常没有彩色语法高亮,体验很差。
- 为什么不容易修:历史文档不可变,不能重建所有历史版本;还要考虑语言数量、工具选择、体积、安全、平台兼容。
- rustdoc / docs.rs 背景:Rust crate 文档由 rustdoc 生成,发布到 crates.io 后由 docs.rs 构建和托管。
- tree-sitter 的价值:它是高质量、快速、鲁棒、依赖少的解析器生成和增量解析工具,适合语法高亮。
- LSP 为什么不适合离线高亮:语义高亮更准,但需要加载源码、依赖和 sysroot,成本太高。
- tree-sitter 原始生态的麻烦:要找 grammar、修旧 grammar、重新生成 C parser、找 highlight query 和 injection query。
- 作者的动机:过去 6 年为自己网站维护了一批 grammar,后来决定把这件事做成公共基础设施。
- arborium 是什么:一个 tree-sitter distribution,整理了 96 种语言的 grammar、queries、license 和 feature flags。
- 依赖联动:例如启用 Svelte 时,会自动带上 HTML、CSS、JavaScript、SCSS 等相关高亮依赖。
- 高层 API :
Highlighter::new()+highlight_to_html("rust", "..."),让使用方不用自己组装 tree-sitter。 - HTML 输出策略 :默认使用短标签如
<a-k>keyword</a-k>,也支持传统<span class="code-keyword">...。 - 终端输出支持:除了 HTML,也能输出带 ANSI escape 的终端高亮文本。
- WASM 支持难点 :目标是
wasm32-unknown-unknown,需要提供一组 libc 符号和 allocator。 - 方案一:在文档里引入前端脚本:今天就能用,docs.rs 无需改动,但 bundle 大且安全风险严重。
- 方案二:集成进 rustdoc:技术上可行,作者做了 PR,但 96 种语言会让 rustdoc 二进制从约 22MB 增到约 171MB。
- 方案三:docs.rs 后端后处理 :用
arborium-rustdoc扫描 rustdoc HTML、给 code block 上色、补 CSS。 - 后处理方案的优势:不需要第三方 JS,不增加用户下载 grammar bundle,体积增量很小,还可以 sandbox。
- 构建工程挑战:96 种语言、双平台发布、provenance、CI、WASM import 检查、xtask、缓存等都很折腾。
- 最终愿景:把 tree-sitter 高质量语法高亮能力打包成可长期维护的公共基础设施。
Rust 文档体验一直很强。
你在代码里写 /// 或 //!,rustdoc 就能生成漂亮的 HTML 文档。发布 crate 到 crates.io 后,docs.rs 会自动构建文档,给每个 crate 一个可以访问、可以链接、可以搜索的页面。对于 Rust 生态来说,这是一套非常重要的基础设施。
但这套基础设施里有一个长期看着不太舒服的小问题:代码块高亮。
Rust 代码块当然能高亮。但如果你的文档里有 Bash、TOML、YAML、JSON、C、C++、JavaScript、TypeScript、HTML、CSS、SQL、Dockerfile、Nix、Svelte、Python、Go 等其他语言,很多时候它们只是白字黑底。你明明写了语言标记,浏览器里看起来却像没上色一样。
这件事看起来简单:不就是加个语法高亮吗?
但原文的重点就是:这件事一点都不简单。
因为 docs.rs 不是个人博客。它托管了大量 crate 的文档,而且历史版本几乎是不可变的。一旦某个 crate 的某个版本构建完成,它生成的 HTML、CSS、JS 会被放进巨大的存储桶里。你不能说"我们现在想给代码块上色了,那就把所有 crate 的所有历史版本全部重新 rustdoc 一遍"。这在工程上不可行。
同时,语法高亮本身也有很多选择题:
text
用哪套高亮引擎?
支持哪些语言?
能不能信任它?
输出质量够不够?
需不需要动态链接?
rustdoc 支持的平台上都能编译吗?
高亮后的 HTML 会变大多少?
体积增加能不能接受?
谁来维护这些语言 grammar?
谁来负责许可证和归属声明?
这篇文章介绍的 arborium,就是作者对这些问题的回答。
简单说,arborium 是一个 tree-sitter distribution。它把许多 tree-sitter grammar、highlight query、injection query、许可证信息、构建规则、WASM 兼容性和 Rust API 包装起来,让开发者不用自己一点点拼装语法高亮基础设施。
更野一点说:它试图把"高质量、多语言、可复用、可部署"的语法高亮,变成 Rust 文档生态可以长期依赖的一块公共基础设施。
一、问题起点:为什么 docs.rs 上很多代码块没有颜色
故事从一次和 docs.rs 团队的讨论开始。
作者看到自己发布的 crate 文档里有很多不同语言的代码块,但它们看起来基本都是白字黑底。这让人很难受。文档里的代码块如果没有高亮,阅读体验会差很多。尤其是当一个 crate 文档需要展示配置文件、shell 命令、HTML、CSS、JavaScript、TOML、YAML 或其他语言时,黑白文本非常不友好。
于是作者开了一个 GitHub issue,想了解为什么 docs.rs / rustdoc 不能直接支持更多语言的语法高亮。
讨论很短,但很有成果。结果是:不是大家不知道它有用,而是背后有现实原因。
首先,docs.rs 的历史文档不是可以随便重写的。一个 crate 版本构建完成后,它生成的 HTML、CSS、JS 会被放进存储桶。后续如果 rustdoc 改了高亮逻辑,也不会自动重建所有历史版本。docs.rs 可能会重新构建最新版本,但不会把所有历史版本全部扫一遍。
所以,想要给"已经存在的所有历史文档"加颜色,不能只靠改 rustdoc。你没法承受全量重建成本。
其次,语法高亮不是一个开关。
你要先决定用什么高亮器。正则?syntect?tree-sitter?LSP?每个选择都有代价。
你还要决定支持多少语言。支持 Rust 之外的 5 种语言?20 种?100 种?谁来定?按下载量?按投票?按作者心情?
你还要考虑输出体积。高亮后的 HTML 一定比原始代码块大。每个 token 都包一层 span 或自定义标签,所有文档加起来,存储和传输体积都会增加。
你还要考虑安全。语法高亮如果在浏览器里运行,就意味着加载 JavaScript,甚至 WebAssembly。docs.rs 是公共基础设施,让任意 crate 作者向文档页面注入第三方 JS,本质上是安全风险。
所以,"为什么不直接上色"这个问题,答案不是"没人想做",而是"做起来要同时解决一堆工程、生态、体积、安全和维护问题"。
二、tree-sitter 为什么适合做这件事
作者选择的核心工具是 tree-sitter。
tree-sitter 是一个解析器生成工具,也是一套增量解析库。它可以为源文件构建具体语法树,并在源文件修改时高效更新语法树。它的目标是通用、快速、鲁棒、依赖少。
对代码编辑器来说,tree-sitter 已经非常重要。很多编辑器用它做语法解析、高亮、代码折叠、结构化选择等功能。它不像简单正则高亮那样脆弱,也不像完整语言服务器那样重。
作者认为,如果只看语法高亮质量,tree-sitter 是非常好的选择。只有 LSP 语义高亮能比它更强。
但 LSP 不适合这里。
LSP,也就是 Language Server Protocol,是 rust-analyzer 和编辑器之间说话的协议。LSP 能做语义高亮,因为它理解项目、依赖、类型、模块、符号等上下文。但要做到这些,它需要加载源码、依赖、sysroot,甚至整个项目图。对一个编辑器来说可以接受,对离线生成一堆文档页面来说,成本太高。
rustdoc / docs.rs 只是要给代码块上色。为了一个小代码块启动语言服务器、加载依赖、分析整个 crate,显然不现实。
tree-sitter 的定位正好在中间:比正则强,比 LSP 轻。它能用语法树判断什么是关键字、函数、字符串、数字、注释,也能通过 query 机制表达高亮规则。对于文档语法高亮来说,这是一个很合适的平衡点。
三、tree-sitter 不是拿来就能用
虽然 tree-sitter 很好,但原始生态并不是"引一个 crate 就完事"。
如果你想高亮某种语言,需要几个东西。
第一,需要 grammar。也就是该语言的 tree-sitter 语法定义。Rust、C++ 这类主流语言通常有高质量 grammar,但冷门语言就不一定。有些 grammar 很旧,是针对旧版本 tree-sitter 写的,需要重新生成。有些 grammar 里还有会让编译时间爆炸的规则,需要清理。
第二,需要运行 tree-sitter CLI 重新生成 parser。tree-sitter grammar 通常有 grammar.js,有时还有 scanner.cc 或 scanner.c。重新生成后,会得到一大堆 C 代码。最终每种语言的 parser 往往就是一堆生成出来的 C 文件。
第三,需要 highlight query。grammar 只会告诉你代码的语法树长什么样。它能告诉你这里是某个 node,那里是某个 expression,但它不会自动告诉你"这个 node 应该是 keyword"、"这个 node 是 function"、"这个 node 是 string"。highlight query 就是把语法树 node 映射到高亮类别的规则。
第四,需要 injection query。很多语言里会嵌套其他语言。比如 Svelte 组件通常包含 HTML,又嵌入 <script> 和 <style>,里面可能是 JavaScript、TypeScript、CSS、SCSS。Markdown 里有 fenced code block,也可能嵌入任意语言。injection query 用来告诉高亮器:这里有另一个语言,要切换 grammar 继续高亮。
第五,需要把这些依赖组织起来。tree-sitter-highlight 确实提供了 callback 系统处理 injection,但具体依赖怎么带、怎么调度、怎么让 Svelte 自动带上 HTML/CSS/JS,都要你自己实现。
所以作者之前为了自己网站,维护了一个私有 grammar 集合。最开始只有 18 种语言:Bash、C、Clojure、Dockerfile、Go、HTML、INI、Java、JavaScript、Meson、Nix、Python、Rust、TOML、TypeScript、x86asm、YAML、Zig 等。
后来他意识到:自己不止一个项目需要高亮,别人也会遇到同样问题。既然已经踩坑 6 年,不如把这些东西整理成公共分发。
于是 arborium 出现了。
四、arborium 是什么
arborium 是一个 tree-sitter distribution。
它不是单个 parser,也不是一个简单的 wrapper。它更像一个经过整理、修复、打包、带许可证和统一 API 的 tree-sitter 语言发行版。
作者根据投票需求整理了 96 种语言。对每一种语言,他都做了这些事:
text
寻找可用且质量较好的 grammar
把 grammar vendored 进项目
修复或清理旧 grammar
重新生成 parser
确认 highlight query 能工作
确认 injection query 能工作
确认许可证和 attribution 信息齐全
把语言接入 arborium 的 feature flag
arborium 的目标不是让你自己到处找 grammar、修 grammar、修 query、处理 injection、配置 theme、处理 HTML 输出,而是给你一个现成入口。
最简单的使用方式大概是:
rust
use arborium::Highlighter;
let mut highlighter = Highlighter::new();
let html = highlighter.highlight_to_html("rust", "fn main() {}")?;
当然,这个高层 API 牺牲了一部分 tree-sitter 的增量解析能力。tree-sitter 的强项之一是编辑器场景中的增量更新:文件改一点,语法树只更新一部分。但文档高亮通常不是这个场景。文档构建时,一段代码块就是一次性输入,一次性输出 HTML。所以简单 API 很有意义。
如果你需要更复杂的 API,arborium 也提供更底层的接口。但对很多使用者来说,highlight_to_html 就够了。
五、语言 feature 和依赖联动
arborium 把每种语言做成 cargo feature。这样使用者可以按需启用语言,避免把所有 96 种语言都编进来。
但它不只是"一个语言一个 feature"这么简单。
有些语言需要依赖其他语言才能完整高亮。比如 Svelte。Svelte 组件本身不是单一语言,它通常包含 HTML、JavaScript、CSS,有时还有 TypeScript、SCSS 等。如果你启用 Svelte,高亮器必须知道如何处理嵌入的脚本和样式。
所以 arborium 会做依赖联动。启用 svelte 时,它会自动带上 HTML、CSS、JavaScript、SCSS 等相关 grammar crate。这样用户不用手动猜"我高亮 Svelte 还要启用哪些语言"。
这很重要。因为 tree-sitter injection 的麻烦点就在这里:你不能只拿到外层 grammar,还要知道嵌套语言怎么解析、怎么高亮、怎么加载。
arborium 把这些复杂性收进 distribution 里。
六、HTML 输出:短标签还是传统 span
语法高亮最终要输出 HTML。最传统的方式是:
html
<span class="code-keyword">keyword</span>
这很直观,也和很多高亮器一致。缺点是体积较大。每个 token 都有很长的标签和 class 名,大量代码块叠起来,HTML 体积会明显增加。
arborium 默认选择更短的现代标签,例如:
html
<a-k>keyword</a-k>
这类标签短得多。只要配套 CSS 写好,浏览器可以正常渲染。它不是最传统的写法,但更紧凑。
如果你坚持传统风格,arborium 也支持长写法:
html
<span class="code-keyword">keyword</span>
作者开玩笑说,如果你复古,并且保证 Brotli 压缩能弥补体积,那也可以用这种方式。
这个设计背后其实是 docs.rs 场景的现实:高亮 HTML 体积不能无限膨胀。Rust 文档里代码块非常多,如果每个 token 都膨胀很多,长期存储和传输成本都会上升。
所以 arborium 在 API 层就提供输出风格选择:你可以选紧凑,也可以选传统。
七、不只是 HTML,也能输出终端高亮
arborium 不只面向网页。
如果你是终端用户,也可以让它输出带 ANSI escape code 的高亮文本。甚至可以配置背景色、margin、padding、border,让代码块在终端里也有漂亮的样式。
这说明 arborium 并不只是 docs.rs 的专用工具。它更像一个通用语法高亮基础设施:HTML 场景能用,终端场景也能用;文档生成能用,网站构建能用,CLI 工具也能用。
核心价值仍然是:你不需要自己去找 96 种 grammar,也不需要手动拼 tree-sitter-highlight 的 callback。
八、WASM 支持:wasm32-unknown-unknown 的麻烦
arborium 还有一个重要目标:能够编译到 wasm32-unknown-unknown,在浏览器中运行。
这很关键,因为作者的第一个落地方案,就是让文档页面加载一个前端脚本和 WebAssembly bundle,在浏览器里完成高亮。
但这件事并不简单。
tree-sitter grammar 最终会生成 C 代码。Rust crate 调用 C 编译器,把这些 parser 编译进来。要编译到 WebAssembly,就需要目标平台能提供 C 代码需要的一些 libc 符号。
tree-sitter 自带的 WASM playground 通常使用 wasm32-wasi。WASI 提供一套系统接口,类似"WebAssembly 的系统调用层"。但作者需要的是 wasm32-unknown-unknown,这是一个更裸的目标。它没有完整系统环境。你需要自己提供足够多的 libc 符号,让生成的 C parser 能满意。
arborium 为此准备了一个小型 sysroot,提供 assert.h、ctype.h、endian.h、inttypes.h 等头文件,以及一些函数实现。大多数函数很简单,比如 isupper、islower。比较麻烦的是 malloc、free 这类内存分配函数,arborium 使用 dlmalloc 来提供。
最终效果是:这些 Rust crate 可以通过 Rust toolchain 和 C toolchain 编译到 wasm32-unknown-unknown,然后在浏览器里运行。
这一步很工程化,也很关键。没有它,前端脚本方案就无法成立。
九、方案一:直接在文档里引入脚本
第一个落地方案最直接:如果你发布 crate,想让 docs.rs 上的文档支持多语言高亮,可以在仓库里放一个 HTML 文件,然后在 Cargo.toml 里加 metadata,让 docs.rs 构建时把它带进去。
这个 HTML 文件加载 arborium 的脚本和 WASM,在页面加载后扫描代码块,给它们上色。
作者还额外做了一些适配:检测页面是否运行在 docs.rs 上,并根据当前 docs.rs 主题切换对应高亮主题。docs.rs 有 light、dark、Ayu 等主题,arborium 会尽量保持风格一致。
这个方案的优点非常明显:
text
今天就能用
不需要 docs.rs 团队改任何东西
不需要改 rustdoc
不需要改构建管线
crate 作者可以自己选择接入
这类前端扩展在 rustdoc 里已经有人做过,比如集成 KaTeX 渲染 LaTeX 公式、渲染图表等。rustdoc 页面允许 crate 文档带一些前端资源,因此这是一条现实可行的 escape hatch。
但作者马上说:这个方案也是最糟糕的。
原因有两个。
第一,体积。它需要 JavaScript 和 WebAssembly。为了给几个小代码块高亮,用户可能要下载几百 KB 的 grammar bundle。对网页性能来说不理想。
第二,也是最重要的:安全。
让任意 crate 作者在 docs.rs 页面主上下文里注入第三方 JavaScript,是一个安全灾难。今天 docs.rs 页面上没什么可偷,顶多是用户选择的主题。但未来不一定。更重要的是,这是一种坏实践。
想象一下,如果所有人都用 arborium 的 NPM 包来给 docs.rs 高亮。几年后作者突然变坏,发布一个恶意版本。只要页面加载这个包,就可能影响大量用户。即使让大家 pin 固定版本,也会阻止他们获得安全更新和 bug 修复。
理想状态是:docs.rs 页面上的 JavaScript 都应该由 docs.rs 团队自己分发。这样世界只需要担心 docs.rs 团队变坏,而不是每个 crate 作者或第三方包作者变坏。
所以,前端脚本方案能用,但不应该是长期答案。
十、方案二:把 arborium 集成进 rustdoc
第二个方案是把 arborium 直接集成到 rustdoc。
arborium 本质上是一堆 Rust crate,加上一堆 tree-sitter 生成的 C 代码。它没有动态链接,没有 plugin folder,没有异步加载,也没有运行时乱七八糟的外部依赖。理论上,把它放进 rustdoc 是可行的。
作者甚至做了一个 rustdoc PR,让 rustdoc 能高亮其他语言。代码 diff 看起来不大,大概是 +537 -11。但这只是表面。真正被拉进来的,是数百万行 tree-sitter 生成的 C parser 代码。
这马上带来一个问题:到底要打包哪些 grammar?
如果把 96 种语言都编进 rustdoc,二进制体积会明显膨胀。作者展示了一个对比:带 96 种语言的自定义 rustdoc 大约 171MB,而 main branch 的 rustdoc 大约 22MB。
这不是小差距。
这也是为什么"集成进 rustdoc"虽然技术上可行,但政治和工程上都不一定好过。rustdoc 是 Rust 工具链的一部分,每个安装 Rust 的人都会拿到它。为了文档里偶尔出现的多语言代码块,把 rustdoc 变大这么多,肯定会引发讨论。
也许可以只内置少数常见语言?那谁来决定?也许可以 feature-gate?但 rustdoc 是随 toolchain 分发,不是每个 crate 自己 cargo feature。也许可以做外部插件?那又回到分发、安全、平台兼容问题。
所以,方案二很干净,但代价很重。
十一、方案三:只在 docs.rs 后端处理
于是作者提出第三个方案:不要让浏览器处理,也不要让 rustdoc 自带所有 grammar,而是在 docs.rs 后端构建完成后做一次 HTML 后处理。
这个工具叫 arborium-rustdoc。
它专门处理 rustdoc 生成的 HTML:
text
扫描 HTML 文件
找到代码块
识别语言
用 arborium 高亮
把原代码块替换成带高亮标记的 HTML
给主 CSS 文件底部补上样式
它不是浏览器脚本,也不是 rustdoc 内置功能,而是一个构建后处理器。
作者测试了 facet monorepo 的所有依赖文档。约 900MB 的文档目录,处理后只增加了 24KB。这个结果非常重要,因为它直接回应了"高亮后 HTML 会不会变大很多"的担忧。
后处理方案有几个优点:
text
不需要第三方 JS
不需要用户下载 WASM grammar bundle
不需要把 96 种语言塞进 rustdoc 二进制
可以在 docs.rs 后端统一执行
可以 sandbox
可以支持所有语言
对最终页面体积影响很小
作者最后也明确说,如果是 docs.rs,他现实中会选择 arborium-rustdoc 作为后处理步骤。它快,可以支持所有语言,也没有前两个方案的安全和 bundle size 问题。
这也是整篇文章里最像"工程上可落地"的答案。
十二、CI 和构建工程:真正痛苦的地方
文章最后的 post-mortem 讲了项目中最难的部分:CI。
如果只是构建一个小 Rust 包,GitHub Actions 还算能忍。但 arborium 不是一个小包。它要处理 96 种语言,支持多个包,构建 WebAssembly,发布到两个平台,还要带 provenance。
作者提到这是 2x96 builds + supporting packages 的规模。任何一个 CI 失败都很惩罚。为了让构建可维护,他把大量逻辑从 YAML 里拿出来,放进 cargo-xtask。
xtask 是 Rust 项目里常见的一种模式:在仓库里放一个小工具 crate,用 Rust 写构建、生成、检查、发布等工程脚本。相比在 GitHub Actions YAML 里塞复杂 bash,Rust 代码更容易测试、组织和维护。
arborium 的 xtask 不只是显示进度条和 nerd font 图标。它还会检查每个生成 artifact 是否真的能在浏览器里加载。具体做法是用 walrus 解析 WebAssembly bundle,检查它的 imports,而不是简单地把 wasm-objdump -x 输出 grep 一下。
它还用 blake3 hash 避免重复计算输入。作者开玩笑说一方面是因为名字听起来酷,另一方面是因为两周里发生了太多疯狂事情,他已经记不清一半了。
这段说明,arborium 不只是"收集 grammar"这么简单。真正难的是把它做成一个可发布、可检查、可重复构建、可跨平台运行的 distribution。
十三、arborium 的意义:把 6 年私人经验公共化
这篇文章最有价值的地方,是它展示了"基础设施化"的过程。
一开始,作者只是为自己网站解决语法高亮。他用了 tree-sitter,找 grammar,修 grammar,写 query,处理 injections。后来他维护了 18 种语言。再后来,他发现自己多个项目都需要这些东西。最后,他决定把这套私有工具变成公共组件。
这就是 arborium:
text
不是一个简单库
不是一个 NPM 小脚本
不是一个 tree-sitter wrapper
而是一个把 grammar、queries、license、feature、WASM、HTML 输出、终端输出、rustdoc 后处理都打包起来的 distribution
它的目标不是展示一个技术 demo,而是让别人不必再重复踩同样的坑。
作者最后说,他希望 arborium 能撑未来 20 年。他以 Apache-2.0 + MIT 许可把它捐给公共领域,希望准确的语法高亮能在 Web 上开花,就像代码编辑器这些年突然变得更擅长语法高亮一样。
他相信 tree-sitter 可以第二次改变世界。第一次是在编辑器里,第二次则是为那些没有时间、没有经验、没有兴趣自己拼装所有零件的人,提供一个现成的分发包。
十四、为什么这件事值得 Rust 生态关注
Rust 生态非常重视文档。很多 crate 的 docs.rs 页面就是事实上的官方文档。一个 crate 好不好用,文档读起来清不清楚,非常关键。
而现代文档里,代码块不再只有 Rust。你可能要展示:
text
Cargo.toml
YAML 配置
JSON 示例
Shell 命令
Dockerfile
GitHub Actions workflow
HTML/CSS/JS
SQL
Nix
Svelte
Python
Go
C/C++
如果这些代码块都没有高亮,阅读体验会明显下降。尤其是配置文件和嵌入语言,语法高亮能帮助读者快速分辨 key、value、string、comment、keyword。
但文档高亮不是"好看"这么简单。它还影响认知负担。好的高亮能让读者更快找到结构,减少看错代码的概率。对长文档、教程、API reference 来说,这很重要。
arborium 的意义在于,它把这件本来每个人都要自己拼的东西,变成一个公共包。
十五、三种方案的取舍
可以把文章里的三种方案放在一起看。
方案一:前端脚本
优点:
text
今天就能用
crate 作者自己接入
docs.rs 团队零改动
不需要等 rustdoc 或 docs.rs 改造
缺点:
text
需要 JavaScript
需要 WebAssembly
用户可能下载大 grammar bundle
安全风险严重
不适合作为长期公共基础设施
它适合作为逃生通道,不适合作为最终方案。
方案二:集成进 rustdoc
优点:
text
技术上干净
生成文档时直接高亮
不需要运行时脚本
不需要 docs.rs 后处理
缺点:
text
rustdoc 二进制会显著变大
必须决定内置哪些语言
把大量 tree-sitter 生成 C 代码带进 Rust 工具链
讨论成本和维护成本都高
它是理想主义方案,但代价很大。
方案三:docs.rs 后端后处理
优点:
text
不需要第三方 JS
不增加浏览器端 grammar bundle
不把所有 grammar 塞进 rustdoc
可以支持所有语言
可以 sandbox
处理后页面体积增量很小
适合 docs.rs 集中执行
缺点:
text
需要 docs.rs 构建流程增加一步
需要维护后处理工具
需要保证 HTML patch 稳定
作者现实中最看好第三种。
这也很符合工程直觉:如果 rustdoc 本体不适合变得巨大,浏览器端又有安全和体积问题,那么后端构建后处理就是一个很合理的折中。
十六、这篇文章真正想讲什么
表面上,文章是在介绍 arborium。更深层,它讲的是"把一个生态里的反复痛点做成可复用基础设施"。
tree-sitter 本身已经很好,但它不是完整产品。一个 grammar crate 通常只导出一个 language 函数,可能还附带 highlights query 和 injections query。它离"拿来高亮任意代码块"还有很远。
你需要选 grammar,修 grammar,重生成 C parser,处理 query,处理 injection,处理主题,处理 HTML 输出,处理终端输出,处理 WASM,处理 licenses,处理 feature flags,处理 CI,处理发布,处理 docs.rs 集成路径。
arborium 的价值就在于,把这些杂活收进一个 distribution。
这也是很多基础设施项目的本质:它们不是发明某个全新理论,而是把一堆别人都不想重复做的脏活、累活、边角问题、平台差异、许可证和构建细节整理干净,提供一个稳定接口。
这类工作不总是最炫,但非常重要。
十七、总结
这篇文章介绍了 arborium,一个面向 tree-sitter 的 Rust 分发项目。它的起点是 docs.rs / rustdoc 里的多语言代码块高亮问题。Rust 文档生成体验很好,但非 Rust 语言的代码块经常没有彩色高亮。想修这个问题并不容易:docs.rs 上历史文档几乎不可变,不能重建所有 crate 的所有版本;高亮器选择、语言支持范围、输出体积、平台兼容、安全边界和维护成本也都需要考虑。
作者选择 tree-sitter 作为基础。tree-sitter 比正则高亮更准确,又比 LSP 语义高亮更轻。LSP 需要加载源码、依赖和 sysroot,不适合离线批量文档高亮;tree-sitter 则能用 grammar 生成语法树,再通过 highlight query 和 injection query 映射到高亮类别和嵌套语言。但 tree-sitter 原始生态并不是拿来就能用:每种语言都要找 grammar、修旧 grammar、重新生成 C parser、找 highlight query、处理 injection callback、处理嵌套语言依赖。
arborium 正是为了解决这些重复劳动。作者整理了 96 种语言的 grammar,根据投票需求寻找可用实现,vendored 进项目,修复和重新生成 parser,确保 highlight query 和 injection query 正常,补全许可证和 attribution,并把每种语言集成进 cargo feature。对于 Svelte 这类嵌套语言,启用 Svelte 会自动带上 HTML、CSS、JavaScript、SCSS 等相关 grammar。使用者可以通过主 crate 的简单 API 调用高亮,比如创建 Highlighter 后调用 highlight_to_html("rust", "fn main() {}")。
arborium 不只支持 HTML。它支持不同主题,默认使用紧凑 HTML 标签如 <a-k>keyword</a-k>,也支持传统 <span class="code-keyword">keyword</span> 输出。它还能输出带 ANSI escape code 的终端高亮文本。更重要的是,它能编译到 wasm32-unknown-unknown,让浏览器端运行成为可能。为此,作者还提供了一套足够让 tree-sitter C parser 满意的 libc 符号和 allocator,其中 malloc、free 等由 dlmalloc 提供。
文章提出三种落地方式。第一种是今天就能用的前端脚本:crate 作者在文档里加入 HTML 和 metadata,让 docs.rs 带上脚本和 WASM,在浏览器中扫描代码块并高亮。它的优点是无需 docs.rs 团队改动,马上可用;缺点是需要下载 JavaScript 和 WebAssembly bundle,体积可能很大,而且允许第三方脚本进入 docs.rs 页面主上下文,是严重安全风险。因此它适合作为 escape hatch,不适合作为长期方案。
第二种是把 arborium 集成进 rustdoc。作者已经做了一个 PR,技术上能让 rustdoc 直接高亮其他语言。但问题是体积:集成 96 种语言后,rustdoc 二进制从约 22MB 增到约 171MB。虽然代码 diff 看起来不大,背后却拉入了数百万行 tree-sitter 生成的 C parser。于是"应该内置哪些 grammar"变成一个很难的政策和工程问题。
第三种是 docs.rs 后端后处理。作者做了 arborium-rustdoc,专门扫描 rustdoc 生成的 HTML,找到代码块,进行高亮,并在 CSS 文件底部追加样式。这个方案不需要第三方 JS,不需要浏览器下载 grammar bundle,也不需要把 96 种语言塞进 rustdoc。作者测试 facet monorepo 依赖文档,约 900MB 的文档目录处理后只增加 24KB。最终他认为,如果为 docs.rs 现实落地,后处理方案最合理:快、支持所有语言、安全边界清楚,还可以 sandbox。
文章最后还讲了构建工程。arborium 最难的部分不是写一个高亮函数,而是 CI 和发布:96 种语言、双平台、WebAssembly、provenance、支持包、GitHub Actions、artifact 检查、缓存。作者把大量逻辑移出 YAML,写进 cargo-xtask,并用 walrus 解析 WebAssembly bundle 检查 imports,用 blake3 hash 避免重复计算。这说明 arborium 是一个工程化 distribution,而不只是一个 demo。
最终,arborium 的意义在于:把作者过去 6 年为自己网站和项目积累的 tree-sitter 语法高亮经验,变成公共基础设施。它希望让那些没有时间、经验或兴趣自己拼装 grammar、query、injection、WASM 和主题系统的人,也能获得高质量语法高亮。对 Rust 文档生态来说,这可能是一个很现实的补足:让 docs.rs 上不只是 Rust 代码块好看,也让配置、脚本、前端、系统语言和各种嵌入代码都变得可读。