- 开发工具
- 文档
【免费下载链接】mdBook
Create book from markdown files. Like Gitbook but implemented in Rust
mdBook(用 Rust 实现的 Gitbook 式文档工具)的init命令不仅能为全新项目生成脚手架,还有一个常被忽略但非常实用的能力:当目标目录中已经存在SUMMARY.md时,init会解析该文件,并按照其中的章节路径自动补齐缺失的 Markdown 源文件。本文以仓库中真实的测试用例 init_from_summary 为切入点,结合 CLI 实现、BookBuilder源码与create_missing配置项,讲解这一机制的完整工作流程,以及如何用「先写大纲、后生成文件」的方式快速搭建一本书的骨架。
一、测试夹具:一个只含 SUMMARY.md 的最小书目录
在仓库的测试套件中,init_from_summary 目录是一个极其精简的测试书,它的src目录下只有一个文件 SUMMARY.md,内容如下:
# Summary [intro](https://link.gitcode.com/i/c2802e7a6bd5d4009aec456f5bd9f023) - First chapter outro注意:这里没有intro.md、first.md、outro.md这三个实际章节文件——它们全部依赖mdbook init根据SUMMARY.md自动生成。这恰好构造了「大纲先行、内容后补」的典型场景。
从SUMMARY.md的语法角度看,这个文件涵盖了三种最基础的章节类型(详见官方文档 SUMMARY.md 规范):
- 前缀章节(Prefix Chapter):
[intro](https://link.gitcode.com/i/c2802e7a6bd5d4009aec456f5bd9f023)以无-列表前缀的形式出现,位于编号章节之前,不会参与章节编号,适合前言、导读; - 编号章节(Numbered Chapter):
- First chapter以-列表项形式出现,是全书主体内容,支持嵌套(子章节通过缩进表示); - 后缀章节(Suffix Chapter):
outro位于编号章节之后,同样不参与编号,适合结语、附录。
二、测试断言:init 生成了哪些文件
对应的集成测试位于 tests/testsuite/init.rs,它把上述目录复制到临时目录后执行init,并逐文件断言生成结果:
#[test] fn init_from_summary() { BookTest::from_dir("init/init_from_summary") .run("init", |_| {}) .check_file( "src/intro.md", str![[r#" # intro "#]], ) .check_file( "src/first.md", str![[r#" # First chapter "#]], ) .check_file( "src/outro.md", str![[r#" # outro "#]], ); }由断言可以提炼出两个关键事实:
- 按
SUMMARY.md中的路径逐一补齐缺失文件:intro.md、first.md、outro.md被创建在src目录下,路径与链接中的相对路径一一对应; - 新文件的内容由链接文本派生:每个新生成的文件都会写入一行
# {章节标题},标题取SUMMARY.md链接中[...]部分显示的文本。例如First chapter生成# First chapter,而[intro](https://link.gitcode.com/i/c2802e7a6bd5d4009aec456f5bd9f023)生成# intro——链接文本原样成为文件的一级标题。
这就是init命令的「从 SUMMARY.md 生成章节」特性的直接证据:测试夹具中的src目录在复制时只有SUMMARY.md,运行init后三个章节文件全部就位。
三、底层原理:从 CLI 到 BookBuilder 的完整调用链
3.1 CLI 入口:src/cmd/init.rs
mdbook init的 CLI 实现在 src/cmd/init.rs,其核心流程是:
let mut builder = MDBook::init(&book_dir); // ...处理 --theme、--ignore、--title 等参数... builder.with_config(config); builder.build()?;MDBook::init()返回一个BookBuilder,随后build()一次性完成目录创建、桩文件生成、book.toml写入与书籍加载。命令行支持的参数包括:
| 参数 | 作用 |
|---|---|
[dir] | 指定书籍根目录,省略时默认为当前目录 |
--theme | 将默认主题复制到theme目录,便于定制(已存在时交互确认是否覆盖) |
--force | 跳过所有确认提示(.gitignore询问与标题询问) |
--title <title> | 直接指定书籍标题,省略时进入交互式标题输入 |
--ignore <ignore> | 创建 VCS 忽略文件,取值none或git |
3.2 关键分支:BookBuilder::create_stub_files
决定「生成桩文件还是读取已有 SUMMARY.md」的逻辑在 crates/mdbook-driver/src/init.rs 的create_stub_files():
let summary = src_dir.join("SUMMARY.md"); if !summary.exists() { // 全新目录:写入默认的桩 SUMMARY.md 与 chapter_1.md fs::write(summary, "# Summary\n\n- [Chapter 1](https://link.gitcode.com/i/87649f43582062dd46d12f876e8cd66d)\n")?; fs::write(src_dir.join("chapter_1.md"), "# Chapter 1\n")?; } else { trace!("Existing summary found, no need to create stub files."); }也就是说,init面对两种场景采取不同策略:
- 没有
SUMMARY.md:按basic_init测试(tests/testsuite/init.rs)所见,生成默认的# Summary大纲和chapter_1.md示例章节; - 已有
SUMMARY.md:不写任何桩文件,直接进入后续的加载阶段——缺失章节由加载流程补齐。
3.3 真正的生成者:load_book与create_missing
build()的最后一步是MDBook::load(&self.root)(见 crates/mdbook-driver/src/init.rs),随后在 crates/mdbook-driver/src/mdbook.rs 中经load_with_config调用load_book(src_dir, &config.build)。真正负责按大纲补文件的函数位于 crates/mdbook-driver/src/load.rs:
pub(crate) fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> { let summary_md = src_dir.join("SUMMARY.md"); let summary_content = fs::read_to_string(&summary_md)?; let summary = parse_summary(&summary_content) .with_context(|| format!("Summary parsing failed for file={summary_md:?}"))?; if cfg.create_missing { create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?; } load_book_from_disk(&summary, src_dir) }create_missing遍历SUMMARY.md解析出的三类章节(prefix_chapters、numbered_chapters、suffix_chapters),对每个带路径的链接检查文件是否存在,不存在则创建,内容为# {escape_html(&link.name)}\n(详见 crates/mdbook-driver/src/load.rs):
if let Some(ref location) = link.location { let filename = src_dir.join(location); if !filename.exists() { // 父目录不存在时先递归创建目录 if let Some(parent) = filename.parent() { if !parent.exists() { fs::create_dir_all(parent)?; } } debug!("Creating missing file {}", filename.display()); let title = escape_html(&link.name); fs::write(&filename, format!("# {title}\n"))?; } items.extend(&link.nested_items); }值得注意的实现细节:该函数使用while let Some(next) = items.pop()配合items.extend(&link.nested_items)做深度优先遍历,因此嵌套子章节(SUMMARY.md中缩进的子链接)同样会被处理;并且文件缺失时其父目录也会被一并创建,支持SUMMARY.md中出现类似- sub的多级路径。
四、开关背后的配置:create-missing
上述补文件行为并非无条件生效,它由book.toml中[build]段的create-missing选项控制。配置结构定义在 crates/mdbook-core/src/config.rs:
pub struct BuildConfig { /// 构建产物输出目录(相对书籍根目录) pub build_dir: PathBuf, /// SUMMARY.md 中指定但尚不存在的 markdown 文件是否自动创建 pub create_missing: bool, pub use_default_preprocessors: bool, pub extra_watch_dirs: Vec<PathBuf>, } impl Default for BuildConfig { fn default() -> BuildConfig { BuildConfig { build_dir: PathBuf::from("book"), create_missing: true, use_default_preprocessors: true, extra_watch_dirs: Vec::new(), } } }默认值create_missing = true,这意味着不只是init,任何一次常规的mdbook build/mdbook watch在加载书籍时,都会自动补齐SUMMARY.md中缺失的章节文件。这对「先规划全书大纲,再逐章填充内容」的工作流非常友好:大纲中新建的章节链接,在构建时会被自动创建为带# 标题的空文档(官方init文档 guide/src/cli/init.md 的 Tip 一节也专门说明了这一行为)。
若希望禁止自动补文件(例如希望构建时因缺文件直接报错),可以在book.toml中显式设置:
[build] create-missing = false关闭后,SUMMARY.md中任何指向不存在文件的链接都会在加载阶段报错(对应cant_load_a_nonexistent_chapter等单元测试所验证的行为,见 crates/mdbook-driver/src/load.rs)。
五、完整的实战流程:先写大纲,再生成骨架
结合上文机制,一个典型的「大纲驱动」初始化流程如下:
创建书目录并手写大纲:新建目录(如
my-book/),在其中创建src/SUMMARY.md,先定义好全书结构——前缀章节、编号章节(含嵌套)、后缀章节均可,暂不创建任何章节源文件。运行 init 生成骨架:
mdbook init my-book --force由于
SUMMARY.md已存在,BookBuilder跳过默认桩文件;加载阶段create_missing按大纲自动补齐所有缺失的.md文件,每个文件包含由链接文本派生的一级标题。--force可跳过.gitignore与标题的交互询问;若想直接指定标题,可改用mdbook init my-book --title="My Book"。核对生成结果:此时
src/下应同时出现SUMMARY.md与大纲中的全部章节文件(形如测试断言展示的# intro、# First chapter、# outro),并额外生成book.toml与book/构建目录。逐章填充内容并构建:编辑各章节正文,执行
mdbook build即可输出 HTML;由于create-missing默认开启,后续在大纲中新增的章节链接也会在下次构建时自动补出空文件。
六、从 API 层面复现:BookBuilder 编程式用法
除了 CLI,这一机制也可以通过mdbook-driver的编程接口复现(示例见 crates/mdbook-driver/src/lib.rs):
use mdbook_driver::MDBook; use mdbook_driver::config::Config; let root_dir = "/path/to/book/root"; // 在已有 SUMMARY.md 的目录上运行初始化 MDBook::init(root_dir) .create_gitignore(true) .with_config(Config::default()) .build() .expect("Book generation failed");BookBuilder::build()(crates/mdbook-driver/src/init.rs)的文档注释明确列出了完整职责:创建目录结构、生成桩文件(或在已有SUMMARY.md时跳过)、创建.gitignore、可选复制主题、写出book.toml,最后加载书籍。测试 init_api 验证了 API 形式会生成book.toml、src/SUMMARY.md、src/chapter_1.md与book目录;而init_from_summary测试则验证了「已有大纲」这一分支的 API/CLI 共通行为。
七、小结
mdbook init的init_from_summary特性,本质上是「解析SUMMARY.md→ 按链接路径补齐缺失章节文件」的自动化流程,贯穿 CLI 入口(src/cmd/init.rs)、BookBuilder(crates/mdbook-driver/src/init.rs)与书籍加载器(crates/mdbook-driver/src/load.rs)三层实现,并由[build] create-missing(默认true)配置统一控制。掌握了这一机制,你就可以把 mdBook 当作一个「大纲即骨架」的文档生成器:先专注于用SUMMARY.md规划书籍结构,剩下的空章节文件交给init与每次构建自动补齐,让写作从结构设计开始,而不是从零散的建文件开始。
- 开发工具
- 文档
【免费下载链接】mdBook
Create book from markdown files. Like Gitbook but implemented in Rust
相关推荐
mdBook `init` 命令完全指南:初始化书籍骨架、从 SUMMARY 生成章节与主题定制
mdBook init 命令完全指南:初始化书籍骨架、从 SUMMARY 生成章节与主题定制 mdbook init 是 mdBook(以 Rust 实现的 M
开发工具文档ReMe Tool Memory论文拆解:基于ReMe的经验驱动Agent工具使用方法
ReMe Tool Memory论文拆解:基于ReMe的经验驱动Agent工具使用方法 本文将拆解一篇基于 ReMe 智能体记忆管理框架 的最新工作 ExpG
人工智能Agent 记忆知识库RAGMCP 服务mdBook build 命令完全指南:从 SUMMARY.md 解析到 HTML 渲染输出
mdBook build 命令完全指南:从 SUMMARY.md 解析到 HTML 渲染输出 本文以 mdBook 的 build 命令为核心,讲解如何将 Ma
开发工具文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考