备用后端

“后端”只是一个,mdbook在书籍渲染过程中调用的程序。该程序会拿到传递到stdin的书籍和配置信息的 JSON 表达式。一旦后端收到这些信息,就可以自由地做任何想做的事情.

GitHub 上已有几个备用后端,可以作为你实践,如何实现这一功能的粗略示例.

此页面将引导您,创建自己的单词计数程序的简单形式的备用后端。虽然它将用 Rust 编写,但没有理由不能用 Python 或 Ruby 之类,来完成它.

目录

设置好

首先,您需要创建一个新的二进制程序,并添加mdbook作为依赖.

  1. $ cargo new --bin mdbook-wordcount
  2. $ cd mdbook-wordcount
  3. $ cargo add mdbook

捋一捋,当我们的mdbook-wordcount插件被调用,mdbook将通过我们的插件的stdin,发送它RenderContext的 JSON 版本。为方便起见,有一个RenderContext::from_json()构造函数,加载一个RenderContext.

这是我们后端加载本书,所需的所有样板.

  1. // src/main.rs
  2. extern crate mdbook;
  3. use std::io;
  4. use mdbook::renderer::RenderContext;
  5. fn main() {
  6. let mut stdin = io::stdin();
  7. let ctx = RenderContext::from_json(&mut stdin).unwrap();
  8. }

注意: RenderContext包含一个version字段。这使得后端在被调用时确定它们是否与mdbook版本兼容。这个version直接来自mdbookCargo.toml中的相应字段.

建议后端使用semver,如果可能存在兼容性问题,请检查此字段,并发出警告.

检查 Book

现在我们的后端有一本书的副本,让我们计算每章中有多少单词!

因为RenderContext包含一个Book字段(book),和一个BookBook::iter(),用于迭代其Book中所有项的方法,这一步就和第一步一样简单.

  1. fn main() {
  2. let mut stdin = io::stdin();
  3. let ctx = RenderContext::from_json(&mut stdin).unwrap();
  4. for item in ctx.book.iter() {
  5. if let BookItem::Chapter(ref ch) = *item {
  6. let num_words = count_words(ch);
  7. println!("{}: {}", ch.name, num_words);
  8. }
  9. }
  10. }
  11. fn count_words(ch: &Chapter) -> usize {
  12. ch.content.split_whitespace().count()
  13. }

启用吧,我的 Backend

现在我们的基本部分已经运行了,我们希望实际使用它。那首先,当然是安装程序.

  1. $ cargo install --path .

然后cd在特定的书目录中,若你想要数字计数,那更新它的book.toml文件.

  1. [book]
  2. title = "mdBook Documentation"
  3. description = "Create book from markdown files. Like Gitbook but implemented in Rust"
  4. authors = ["Mathieu David", "Michael-F-Bryan"]
  5. + [output.html]
  6. + [output.wordcount]

mdbook将一本书加载到内存中时,它会尝试检查你的book.toml,并查找所有output.*表格来尝试找出要使用的后端。如果没有提供,它将回退到,使用默认的 HTML 渲染器.

值得注意的是,这表示如果你想添加自己的自定义后端,你还需要确保添加 HTML 后端,即使只是空表格。

现在你只需要像平常一样构建你的书,一切都应该干得好.

  1. $ mdbook build
  2. ...
  3. 2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
  4. mdBook: 126
  5. Command Line Tool: 224
  6. init: 283
  7. build: 145
  8. watch: 146
  9. serve: 292
  10. test: 139
  11. Format: 30
  12. SUMMARY.md: 259
  13. Configuration: 784
  14. Theme: 304
  15. index.hbs: 447
  16. Syntax highlighting: 314
  17. MathJax Support: 153
  18. Rust code specific features: 148
  19. For Developers: 788
  20. Alternative Backends: 710
  21. Contributors: 85

我们之所以不需要指定我们wordcount后端的全名/路径,是因为mdbook会尽力的推断程序的名称,这些都是因为规范化,如下: 可执行文件foo后端通常被称为mdbook-foo,还有相关联的[output.foo]会进入book.toml。而要明确告诉mdbook要调用什么命令(可能需要命令行参数或是解释的脚本), 你可以使用command字段。

  1. [book]
  2. title = "mdBook Documentation"
  3. description = "Create book from markdown files. Like Gitbook but implemented in Rust"
  4. authors = ["Mathieu David", "Michael-F-Bryan"]
  5. [output.html]
  6. [output.wordcount]
  7. + command = "python /path/to/wordcount.py"

配置

现在假设您不想计算特定章节上的单词数(可能是生成的文本/代码等)。要做到这样的规范方法,是通过常规book.toml配置文件,添加个别项到您的[output.foo]表格。

Config可以粗略地视为嵌套的hashmap,它允许您调用类似的方法get()使用访问配置的内容,也带get_deserialized()这一方便方法,用于检索值,并自动反序列化为某种任意类型T.

为实现这一点,我们将创建自己的可序列化WordcountConfig结构将封装此后端的所有配置.

首先添加serdeserde_derive到你的Cargo.toml,

  1. $ cargo add serde serde_derive

然后你可以创建配置结构,

  1. extern crate serde;
  2. #[macro_use]
  3. extern crate serde_derive;
  4. ...
  5. #[derive(Debug, Default, Serialize, Deserialize)]
  6. #[serde(default, rename_all = "kebab-case")]
  7. pub struct WordcountConfig {
  8. pub ignores: Vec<String>,
  9. }

现在我们只需要我们的RenderContext,反序列化成WordcountConfig,然后添加一个检查,以确保我们跳过忽略的章节.

  1. fn 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. for item in ctx.book.iter() {
  8. if let BookItem::Chapter(ref ch) = *item {
  9. + if cfg.ignores.contains(&ch.name) {
  10. + continue;
  11. + }
  12. +
  13. let num_words = count_words(ch);
  14. println!("{}: {}", ch.name, num_words);
  15. }
  16. }
  17. }

输出和信号故障

虽然在构建书籍时,将字数计数打印到终端是很好的,但将它们输出到某个文件也可能是个好主意。mdbook能告诉后端,它应该根据RenderContextdestination字段,放置输出的位置,.

  1. + use std::fs::{self, File};
  2. + use std::io::{self, Write};
  3. - use std::io;
  4. use mdbook::renderer::RenderContext;
  5. use mdbook::book::{BookItem, Chapter};
  6. fn main() {
  7. ...
  8. + let _ = fs::create_dir_all(&ctx.destination);
  9. + let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
  10. +
  11. for item in ctx.book.iter() {
  12. if let BookItem::Chapter(ref ch) = *item {
  13. ...
  14. let num_words = count_words(ch);
  15. println!("{}: {}", ch.name, num_words);
  16. + writeln!(f, "{}: {}", ch.name, num_words).unwrap();
  17. }
  18. }
  19. }

注意: 无法保证目标目录存在或为空(mdbook可能会留下以前的内容让后端进行缓存),因此创建它fs::create_dir_all()总不会错。

如果目的地目录已存在, 不要假设它就一定是空的。 要知道,后端是有上一结果缓存的, mdbook 或许会留下 旧的内容在里面。

处理书籍时,总会出现错误(只需查看全部我们已经写过了的unwrap()),所以mdbook会渲染失败后,非零退出代码。

例如,如果我们想确保所有章节的单词,都有偶数数量, 而如果遇到奇数,则输出错误,那么你可以这样做:

  1. + use std::process;
  2. ...
  3. fn main() {
  4. ...
  5. for item in ctx.book.iter() {
  6. if let BookItem::Chapter(ref ch) = *item {
  7. ...
  8. let num_words = count_words(ch);
  9. println!("{}: {}", ch.name, num_words);
  10. writeln!(f, "{}: {}", ch.name, num_words).unwrap();
  11. + if cfg.deny_odds && num_words % 2 == 1 {
  12. + eprintln!("{} has an odd number of words!", ch.name);
  13. + process::exit(1);
  14. }
  15. }
  16. }
  17. }
  18. #[derive(Debug, Default, Serialize, Deserialize)]
  19. #[serde(default, rename_all = "kebab-case")]
  20. pub struct WordcountConfig {
  21. pub ignores: Vec<String>,
  22. + pub deny_odds: bool,
  23. }

现在,如果我们重新安装后端,并构建一本书,

  1. $ cargo install --path . --force
  2. $ mdbook build /path/to/book
  3. ...
  4. 2018-01-16 21:21:39 [INFO] (mdbook::renderer): Invoking the "wordcount" renderer
  5. mdBook: 126
  6. Command Line Tool: 224
  7. init: 283
  8. init has an odd number of words!
  9. 2018-01-16 21:21:39 [ERROR] (mdbook::renderer): Renderer exited with non-zero return code.
  10. 2018-01-16 21:21:39 [ERROR] (mdbook::utils): Error: Rendering failed
  11. 2018-01-16 21:21:39 [ERROR] (mdbook::utils): Caused By: The "mdbook-wordcount" renderer failed

您可能已经注意到,插件的子进程的输出会立即传递给用户。鼓励插件遵循”安静规则”,且仅在必要时生成输出(例如,生成错误或警告).

所有环境变量都传递到后端,允许您使用常用的RUST_LOG,控制日志记录详细程度.

包涵包涵

虽然有点做作,但希望这个例子足以说明,如何创建一个mdbook备用后端。如果你觉得它遗漏了什么,请不要犹豫,创造一个问题的issue tracker,让我们可以一起改进用户指南。

在本章开头提到的现有后端,应该是现实生活中如何完成后端的很好例子,所以请随意浏览源代码,或提出问题.