"后端"是 mdbook 在书籍渲染过程中调用的一个程序。该程序通过 stdin 接收书籍的 JSON 表示和配置信息。一旦后端接收到这些信息,它可以自由地执行任何操作。
有关使用后端的更多信息,请参阅"配置渲染器"章节。
社区已开发了多个后端。请参阅"第三方插件" Wiki 页面以获取可用后端列表。
设置
本页将引导您创建一个简单的字数统计程序形式的替代后端。虽然将使用 Rust 编写,但没有理由不能使用 Python 或 Ruby 等语言实现。
创建项目
首先创建一个新的二进制程序并添加 mdbook 作为依赖:
1$ cargo new --bin mdbook-wordcount 2$ cd mdbook-wordcount 3$ cargo add mdbook 4
基础框架
当我们的 mdbook-wordcount 插件被调用时,mdbook 将通过插件的 stdin 发送 RenderContext 的 JSON 版本。为方便起见,有一个 RenderContext::from_json() 构造函数可以加载 RenderContext。
这是我们后端加载书籍所需的所有样板代码:
1// src/main.rs 2extern crate mdbook; 3 4use std::io; 5use mdbook::renderer::RenderContext; 6 7fn main() { 8 let mut stdin = io::stdin(); 9 let ctx = RenderContext::from_json(&mut stdin).unwrap(); 10} 11
注意:RenderContext 包含一个 version 字段。这让后端能够确定它们是否与调用它们的 mdbook 版本兼容。建议后端使用 semver crate 检查此字段,并在可能存在兼容性问题时发出警告。
检查书籍
现在我们的后端有了书籍的副本,让我们统计每个章节的字数!
由于 RenderContext 包含一个 Book 字段(book),而 Book 有用于迭代 Book 中所有项目的 Book::iter() 方法,这一步变得和第一步一样简单。
1fn main() { 2 let mut stdin = io::stdin(); 3 let ctx = RenderContext::from_json(&mut stdin).unwrap(); 4 5 for item in ctx.book.iter() { 6 if let BookItem::Chapter(ref ch) = *item { 7 let num_words = count_words(ch); 8 println!("{}: {}", ch.name, num_words); 9 } 10 } 11} 12 13fn count_words(ch: &Chapter) -> usize { 14 ch.content.split_whitespace().count() 15} 16
启用后端
现在我们有了基础功能,我们想要实际使用它。首先安装程序:
1$ cargo install --path . 2
然后切换到您想要统计字数的特定书籍目录,并更新其 book.toml 文件:
1[book] 2title = "mdBook Documentation" 3description = "Create book from markdown files. Like Gitbook but implemented in Rust" 4authors = ["Mathieu David", "Michael-F-Bryan"] 5 6[output.html] 7 8[output.wordcount] 9
重要说明:如果要添加自定义后端,还需要确保添加 HTML 后端,即使其表保持为空。
现在您只需要像平常一样构建书籍,一切应该正常工作:
1$ mdbook build 2... 32018-01-16 07:31:15 [INFO] (mdbook::renderer): 调用 "mdbook-wordcount" 渲染器 4mdBook: 126 5Command Line Tool: 224 6init: 283 7build: 145 8... 9
自定义命令
如果我们不需要指定字数统计后端的完整名称/路径,是因为 mdbook 会尝试按照约定推断程序的名称。foo 后端的可执行文件通常称为 mdbook-foo,并在 book.toml 中有相关的 [output.foo] 条目。要明确告诉 mdbook 调用什么命令,可以使用 command 字段:
1[output.wordcount] 2command = "python /path/to/wordcount.py" 3
配置
假设您不想统计特定章节的字数,可以通过在 book.toml 配置文件中向 [output.foo] 表添加项来实现。
创建配置结构
首先将 serde 和 serde_derive 添加到您的 Cargo.toml:
1$ cargo add serde serde_derive 2
然后创建配置结构体:
1extern crate serde; 2#[macro_use] 3extern crate serde_derive; 4 5... 6 7#[derive(Debug, Default, Serialize, Deserialize)] 8#[serde(default, rename_all = "kebab-case")] 9pub struct WordcountConfig { 10 pub ignores: Vec<String>, 11} 12
使用配置
现在我们只需要从 RenderContext 反序列化 WordcountConfig,然后添加检查以确保跳过被忽略的章节:
1fn main() { 2 let mut stdin = io::stdin(); 3 let ctx = RenderContext::from_json(&mut stdin).unwrap(); 4 let cfg: WordcountConfig = ctx.config 5 .get_deserialized("output.wordcount") 6 .unwrap_or_default(); 7 8 for item in ctx.book.iter() { 9 if let BookItem::Chapter(ref ch) = *item { 10 if cfg.ignores.contains(&ch.name) { 11 continue; 12 } 13 14 let num_words = count_words(ch); 15 println!("{}: {}", ch.name, num_words); 16 } 17 } 18} 19
输出和错误处理
输出到文件
除了在构建书籍时将字数统计打印到终端外,将其输出到某个文件也是个好主意。mdbook 通过 RenderContext 中的 destination 字段告诉后端应该将任何生成的输出放置在何处。
1use std::fs::{self, File}; 2use std::io::{self, Write}; 3use mdbook::renderer::RenderContext; 4use mdbook::book::{BookItem, Chapter}; 5 6fn main() { 7 ... 8 9 let _ = fs::create_dir_all(&ctx.destination); 10 let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap(); 11 12 for item in ctx.book.iter() { 13 if let BookItem::Chapter(ref ch) = *item { 14 ... 15 16 let num_words = count_words(ch); 17 println!("{}: {}", ch.name, num_words); 18 writeln!(f, "{}: {}", ch.name, num_words).unwrap(); 19 } 20 } 21} 22
注意:不能保证目标目录存在或为空,因此最好使用 fs::create_dir_all() 创建它。
错误处理
如果在处理书籍时发生错误,mdbook 会将非零退出代码解释为渲染失败。
例如,如果我们想确保所有章节都有偶数个单词,在遇到奇数时出错:
1use std::process; 2... 3 4fn main() { 5 ... 6 7 for item in ctx.book.iter() { 8 if let BookItem::Chapter(ref ch) = *item { 9 ... 10 11 let num_words = count_words(ch); 12 println!("{}: {}", ch.name, num_words); 13 writeln!(f, "{}: {}", ch.name, num_words).unwrap(); 14 15 if cfg.deny_odds && num_words % 2 == 1 { 16 eprintln!("{} has an odd number of words!", ch.name); 17 process::exit(1); 18 } 19 } 20 } 21} 22 23#[derive(Debug, Default, Serialize, Deserialize)] 24#[serde(default, rename_all = "kebab-case")] 25pub struct WordcountConfig { 26 pub ignores: Vec<String>, 27 pub deny_odds: bool, 28} 29
现在,如果我们重新安装后端并构建一本书:
1$ cargo install --path . --force 2$ mdbook build /path/to/book 3... 42018-01-16 21:21:39 [INFO] (mdbook::renderer): 调用 "wordcount" 渲染器 5mdBook: 126 6Command Line Tool: 224 7init: 283 8init has an odd number of words! 92018-01-16 21:21:39 [ERROR] (mdbook::renderer): 渲染器以非零返回代码退出。 10
输出建议
鼓励插件遵循"静默规则",只在必要时生成输出(例如生成错误或警告)。所有环境变量都会传递给后端,允许您使用通常的 RUST_LOG 来控制日志详细程度。
总结
虽然这个示例是人为设计的,但希望它能足够展示如何为 mdbook 创建替代后端。本章开头提到的现有后端应该作为实际如何完成的良好示例,因此请随时浏览源代码或提出问题。
《【mdBook】7.2 替代后端》 是转载文章,点击查看原文。

