☰
mdBook Rust Edition 配置实战:用 `rust.edition` 与 `edition20xx` 标注精准控制代码块编译版本
2026/10/4 1:43:59 网站建设 项目流程
  • 开发工具
  • 文档

【免费下载链接】mdBook

Create book from markdown files. Like Gitbook but implemented in Rust

项目地址:https://gitcode.com/gh_mirrors/md/mdBook
点击查看免费下载

mdBook 允许通过book.toml中的[rust] edition全局指定 Rust 代码块的默认编译版本,也支持在 Markdown 代码围栏中使用edition2015、edition2018、edition2021、edition2024标注对单个代码块单独覆盖。本文基于仓库中的default_rust_edition渲染测试套件,从配置文件、Markdown 写法、HTML 渲染产物到源码实现,完整拆解这一机制的运作原理,读完即可在真实书籍项目中正确配置并验证 Rust 代码块的版本控制。

一、背景:为什么需要显式控制 Rust Edition

Rust 语言自 2015 年起先后发布了 2015、2018、2021 与 2024 四个 edition,不同 edition 下同一段代码的合法性与语义可能完全不同。mdBook 作为面向 Rust 生态的文档工具,会把 Markdown 中的 ```rust 代码块交给 Rust 编译器(Playground)执行或测试,因此必须明确每个代码块使用哪个 edition,否则会出现"文档里的代码明明能编译、测试时却报错"这类与写作无关的环境问题。

mdBook 的解决思路是"全局默认 + 单块覆盖"两级控制:

  • 全局:book.toml的[rust] edition设置整本书代码块的默认 edition;
  • 单块:在代码围栏的 info string 中追加edition2021之类的标注,覆盖全局默认值。

仓库中的default_rust_edition测试套件正是为验证这套机制而存在,对应测试用例位于 tests/testsuite/rendering.rs,通过BookTest::from_dir("rendering/default_rust_edition").check_all_main_files()将实际渲染结果与expected/目录中的基线 HTML 逐字节比对。

二、测试套件全景:文件结构与配置

测试套件目录tests/testsuite/rendering/default_rust_edition/由四部分组成:

文件/目录作用
book.toml书籍配置,全局 edition 设置为"2021"
src/SUMMARY.md章节导航清单
src/default-rust-edition.md被测的 Markdown 源码,包含三种 Rust 代码块
expected/default-rust-edition.html期望的渲染基线,用于比对

其中book.toml内容如下:

[book] title = "default_rust_edition" [rust] edition = "2021"

这里[rust]表即全局 Rust 配置,edition = "2021"声明:凡是没有显式标注 edition 的 ```rust 代码块,一律按 Rust 2021 edition 处理。

三、Markdown 侧的三种写法与覆盖规则

被测章节 src/default-rust-edition.md 精心编排了三种代码块,覆盖"无标注 / 显式标注 / 显式标注另一版本"三种场景:

let x = 2021;
let x = 2021;
let x = 2024;

三个代码块的 edition 解析结果分别是:

  1. 第一个代码块:info string 仅为rust,无任何 edition 标注,应回落到全局配置[rust] edition = "2021";
  2. 第二个代码块:info string 为rust,edition2021,显式声明 2021,与全局默认一致;
  3. 第三个代码块:info string 为rust,edition2024,显式覆盖为 2024,与全局默认不同——用于验证单块标注能否压过全局配置。

这种"标注与全局默认一致时保持等价、不一致时按标注执行"的设计,保证作者既可以为整本书锁定统一的 edition,又能在个别章节安全地使用新版本特性。

四、渲染产物:HTML 中如何体现 Edition

执行mdbook build后,default-rust-edition.md被渲染为 expected/default-rust-edition.html。观察其代码块部分:

<pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)] </span><span class="boring">fn main() { </span>let x = 2021; <span class="boring">}</span></code></pre> <pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)] </span><span class="boring">fn main() { </span>let x = 2021; <span class="boring">}</span></code></pre> <pre class="playground"><code class="language-rust edition2024"><span class="boring">#![allow(unused)] </span><span class="boring">fn main() { </span>let x = 2024; <span class="boring">}</span></code></pre>

三个关键事实:

  • 前两个代码块(无标注、标注edition2021)的 class 均为language-rust edition2021,证明全局配置被正确应用,且显式标注与全局一致时产物完全相同;
  • 第三个代码块 class 为language-rust edition2024,证明单块标注成功覆盖了全局配置;
  • 每个<pre>被加上playgroundclass,<code>内部被自动包裹#![allow(unused)]与fn main() { ... }(标记为boring),说明这些代码块默认被视为可运行、可进 Playground 的 Rust 代码——这也是 edition 标注发挥作用的前提。

五、源码实现:从配置解析到 class 注入

edition 机制在代码中贯穿三层:配置模型 → HTML 渲染器 → Playground 编译器调用。

5.1 配置模型:RustConfig与RustEdition

配置解析位于 crates/mdbook-core/src/config.rs。RustConfig结构体(#[serde(default, rename_all = "kebab-case")])只有一个字段:

pub struct RustConfig { /// Rust edition used in playground pub edition: Option<RustEdition>, }

RustEdition枚举支持四个版本,每个都通过#[serde(rename = "...")]绑定 TOML 中对应的字符串字面量:

  • 2024→E2024
  • 2021→E2021
  • 2018→E2018
  • 2015→E2015(官方指南中说明默认值为"2015")

edition字段是Option<RustEdition>:不写[rust]表或省略edition时值为None,渲染器据此不注入任何 edition class,代码块保持裸language-rust。在同文件的测试代码(config.rs)中,可以找到E2015、E2018、E2021与E2024四种字符串解析的断言,以及cfg.rust.edition == Some(RustEdition::E2024)这类反序列化验证。

5.2 HTML 渲染器:按需注入 edition class

真正决定 HTML 产物的是 crates/mdbook-html/src/html/tree.rs 中的update_code_blocks方法。核心逻辑分两步:

第一步,判断代码块是否为 Playground 可运行代码:class 含language-rust,且不包含ignore、noplayground、noplaypen,并且playground.runnable配置开启(或显式带mdbook-runnable标注)时,才会进入 edition 处理流程。

第二步,注入 edition class:

let add_edition = if class_set.iter().any(|cls| cls.starts_with("edition")) { None // 代码块已带 edition20xx 标注,不覆盖 } else { self.options.edition.map(|edition| match edition { RustEdition::E2015 => "edition2015", RustEdition::E2018 => "edition2018", RustEdition::E2021 => "edition2021", RustEdition::E2024 => "edition2024", _ => panic!("edition {edition:?} not covered"), }) }; if let Some(edition) = add_edition { code_el.insert_attr("class", format!("{class} {edition}").into()); }

这段逻辑清晰印证了前面观察到的行为:只要 class 中已存在以edition开头的标注(如edition2024),渲染器就不再追加全局 edition;否则才把[rust] edition映射为edition2015/edition2018/edition2021/edition2024追加到 class 中。这正是"全局默认 + 单块覆盖"两级规则的实现根基。

随后,若playground.editable未开启且代码块不可编辑,wrap_rust_main会把没有fn main的代码包进fn main() { ... }(对应 HTML 中的boring行);<pre>元素也被加上playgroundclass。

5.3 测试与 Playground:edition 如何传给编译器

edition 不仅影响 HTML class,还决定 Playground 实际调用编译器时的参数。在 crates/mdbook-driver/src/mdbook.rs 中,可以看到 Rust 测试/Playground 命令的构建过程:

if let Some(edition) = self.config.rust.edition { match edition { // ... RustEdition::E2015 => cmd.args(["--edition", "2015"]), RustEdition::E2018 => cmd.args(["--edition", "2018"]), RustEdition::E2021 => cmd.args(["--edition", "2021"]), RustEdition::E2024 => cmd.args(["--edition", "2024"]), _ => panic!("RustEdition {edition:?} not covered"), } }

即运行mdbook test编译书中 Rust 代码片段时,会依据全局 edition 为 rustc 追加--edition参数,从而保证文档测试与 HTML 标注所指版本一致。

六、官方指南与实战建议

mdBook 官方指南在 guide/src/format/configuration/general.md 的 "Rust options" 一节给出了通用写法:

[rust] edition = "2015" # the default edition for code blocks

并说明:edition是代码片段的默认 Rust edition,默认值为"2015";单个代码块可用edition2015、edition2018、edition2021或edition2024标注控制,例如:

```rust,edition2015 // This only works in 2015. let try = true;
同时,[guide/src/format/mdbook.md](https://link.gitcode.com/i/4cdc89922dbadefd91d61790f8c6a6bd) 的代码块属性一节也列出了这四种标注,并明确指向 `rust.edition` 作为全局设置入口。综合源码与文档,实战建议如下: 1. **新书推荐显式设置全局 edition**:在 `book.toml` 中写 `[rust] edition = "2021"`(或根据读者群体选择 2018/2024),避免默认 2015 导致新代码意外编译失败;本测试套件的 `book.toml` 即如此。 2. **需要单块新特性时用标注覆盖**:仅对个别代码块追加 `rust,edition2024` 等 info string,无需改变全书默认值,也不会影响其他代码块。 3. **标注优先级牢记"显式优先"**:只要代码块自身带 `edition20xx`,渲染器就不再追加全局 edition(见 [tree.rs](https://link.gitcode.com/i/2d5dfb26d3d29015b6a355562c3f5ccd) 的 `starts_with("edition")` 判断)。 4. **用测试套件做回归验证**:若修改了与 edition 相关的渲染逻辑,可参照 `default_rust_edition` 用例([tests/testsuite/rendering.rs](https://link.gitcode.com/i/1f2e28d2822d77045c8d303a60eee766))维护 `expected/*.html` 基线,保证全局默认与单块覆盖行为不被破坏。 ## 七、小结 `default_rust_edition` 测试套件完整覆盖了 mdBook edition 控制的两条核心路径:全局配置 `[rust] edition` 决定默认值,代码围栏的 `edition20xx` 标注实现单块覆盖。从 [config.rs](https://link.gitcode.com/i/5aa707a7d8ef7238e4416a78b0db08f3) 的配置模型,到 [tree.rs](https://link.gitcode.com/i/6dd823679a0191e2ac5936e644605610) 的 class 注入,再到 [mdbook.rs](https://link.gitcode.com/i/5ca0af02a17efd56a98b9d6793833cec) 中 `--edition` 参数的传递,每一环都有明确的实现与测试佐证。掌握这套机制后,你可以在自己的书籍中精准控制每个 Rust 代码块的编译版本,让文档示例与实际编译器行为始终一致。
  • 开发工具
  • 文档

【免费下载链接】mdBook

Create book from markdown files. Like Gitbook but implemented in Rust

项目地址:https://gitcode.com/gh_mirrors/md/mdBook
点击查看免费下载
上一篇:《awesome-blockchains》项目安装与配置指南
下一篇:infinite-canvas-tutorial:探索无限画布的魅力

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询