- 开发工具
- 文档
【免费下载链接】mdBook
Create book from markdown files. Like Gitbook but implemented in Rust
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 解析结果分别是:
- 第一个代码块:info string 仅为
rust,无任何 edition 标注,应回落到全局配置[rust] edition = "2021"; - 第二个代码块:info string 为
rust,edition2021,显式声明 2021,与全局默认一致; - 第三个代码块: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→E20242021→E20212018→E20182015→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
相关推荐
Rust 版本(Editions)机制全解:Edition 2015 / 2018 / 2021 / 2024 与 Cargo.toml 配置实战
Rust 版本(Editions)机制全解:Edition 2015 / 2018 / 2021 / 2024 与 Cargo.toml 配置实战 导读 Rus
教程文档mdBook Rust Playground 实战指南:代码块可运行与可编辑机制的完整解析
mdBook Rust Playground 实战指南:代码块可运行与可编辑机制的完整解析 导读 本文以 mdBook 官方 GUI 测试夹具 tests/gu
开发工具文档mdBook 可编辑代码块(Editable Playground)完整指南:启用配置、Ace 编辑器定制与源码原理剖析
mdBook 可编辑代码块(Editable Playground)完整指南:启用配置、Ace 编辑器定制与源码原理剖析 导读 :mdBook 不仅能把 Mar
开发工具文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考