介绍

mdBook 是一个命令行工具,可以将 Markdown 文档,变成 HTML 网站。 这样的工具,用在产品信息或是 API 文档, 教程, 课件资料等等场景。

本指南,带你进入,快进快出的现代化文档工具。 mdBook 是 Rust 官方的项目, 还有 The Rust Programming Language 也是使用 本工具

Contributing

译者:这就不翻译了,有心者事竟成。

mdBook is free and open source. You can find the source code on GitHub and issues and feature requests can be posted on the GitHub issue tracker. mdBook relies on the community to fix bugs and add features: if you’d like to contribute, please read the CONTRIBUTING guide and consider opening a pull request.

License

mdBook,所有源代码,都是在Mozilla Public License v2.0下发布的.

Installation

安装 mdBook CLI 工具有多种方法。选择下面最适合您需要的任何一种方法。如果要安装 mdBook,是为了自动部署,请查看 持续集成 有关如何安装的更多示例。。

Pre-compiled binaries

可执行二进制文件,可在GitHub Releases page下载。选择适用于您的平台(Windows、macOS 或 Linux)的二进制文件,下载后解压缩存档。存档包含一个mdbook可执行文件,您可以运行它,来构建书籍。

为了便于运行,请将二进制文件的路径放入PATH.

Build from source using Rust

可从源代码构建mdbook,您首先需要安装 Rust 和 Cargo。按照屏幕上的说明操作Rust installation page。mdBook 当前至少需要 Rust 1.46。

安装 Rust 后,可以使用以下命令,构建和安装 mdBook:

cargo install mdbook

这将自动从crates.io下载 mdBook,构建并安装在 Cargo 的全局二进制目录中(默认情况下,~/.cargo/bin/)。

Installing the latest master version

发布到 crates.io 的箱子版本,稍微落后于 GitHub 上托管的版本。如果您需要最新版本,您可以自己构建 mdBook 的 git 版本。cargo 构建起来**超级容易**!

cargo install --git https://github.com/rust-lang/mdBook.git mdbook

同样,确保将命令行 cargo 的目录添加到您的PATH.

如果您对修改 mdBook 本身感兴趣,请查看Contributing Guide了解更多信息。

读读读

书的制作

Command Line Tool

mdBook既可以用作命令行工具,也可以用作Rust crate。让我们首先关注命令行工具功能.

二进制

在尽力而为的基础上,预先编译主要平台的二进制文件。访问 releases 页面下载适合您平台的版本.

源码安装

mdBook也可以从源代码安装

Pre-requisite

mdBook是写的 Rust 因此需要Cargo编译。如果您还没有安装Rust,请现在就官方安装

安装 Crates.io 的版本

如果您已安装Rust和Cargo,则安装mdBook相当容易。您只需在终端中键入以下代码段:

cargo install mdbook

这将在Crates.io获取最新版本的源代码,并编译它。你需要添加Cargo的bin目录,到你的环境变量PATH.

在您的终端,运行mdbook help验证它是否有效。

恭喜你,你已经安装了mdBook!

安装 Git 版本

git 版本 包含所有最新的错误修复和功能, 且是在下一个版本中才发布Crates.io, 如果你不能等到下一个版本。你可以自己构建git版本。打开终端,并导航到您选择的目录。我们需要克隆git存储库,然后使用Cargo构建它.

git clone --depth=1 https://github.com/rust-lang/mdBook.git
cd mdBook
cargo build --release

可执行文件mdbook输出在./target/release文件夹,这应该添加到环境变量PATH中,方便使用。

init 命令

每本新书都有一些最小的样板。为此目的,mdBook 支持init命令.

init命令使用如下:

mdbook init

第一次使用init命令,将为您设置几个文件:

book-test/
├── book
└── src
    ├── chapter_1.md
    └── SUMMARY.md
  • src目录是你在写的 markdown 书。它包含所有源文件,配置文件等.

  • book目录是您的书 html 页面输出的位置。所有输出都可以上传到服务器,供观众查看.

  • SUMMARY.md文件是您图书的骨架,并将在其他章节进行更详细的讨论

Tip: 根据 SUMMARY.md 生成

当一个SUMMARY.md文件已存在,init命令将首先解析它,并根据SUMMARY.md中,帮其补全丢失的文件路径。这允许您思考和创建书的整个结构,然后让 mdBook 为您生成它.

指定目录

init命令可以将目录作为参数,用作本书的根目录,而不是当前工作目录.

mdbook init path/to/book

–theme

当你使用--theme,默认主题将被复制到一个名为theme的目录.

在您的源目录中,以便您可以修改它.

主题会被选择性地覆盖,这意味着如果您不想覆盖,只需删除它,就会使用默认文件.

–title

指定书的标题。没有的话,它会问你

mdbook init --title="my amazing book"

–ignore

创建一个 .gitignore,忽略构建的目录。 没有的话,它会问你

build 命令

构建命令,用于渲染您的 md,并输出静态 html:

mdbook build

它会尝试解析你的SUMMARY.md文件,以了解您的图书的结构并获取相应的文件。 要注意的是,如果你写在SUMMARY.md里面的文件链接不存在,它会帮你创建文件。

为方便起见,渲染的输出将保持与源目录结构相同。因此,大型书籍在渲染时能保持结构化。

指定目录

build命令可以将目录作为参数,用作本书的根目录,而不是当前工作目录.

mdbook build path/to/book

–open

当你使用--open(-o),mdbook 将在构建之后,在默认 Web 浏览器中打开网页书.

–dest-dir

--dest-dir(-d)选项允许您更改书籍的输出目录。为相对路径,(相对于书籍的根目录)。如果未指定,则默认为book.toml配置的build.build-dir字段, 或者./book目录.


注意: build 命令会复制源目录的所有文件(除了.md后缀),到构建目录

watch 命令

当您希望在每次更改文件,都生成图书时, watch命令会很有用。你当然可以在每次更改文件反复发出mdbook build。但,聪明的是使用mdbook watch,这样就能观察您的文件,并会在您修改文件时,自动触发构建。 其中包括,那么删除掉,但仍在SUMMARY.md里的文件。

指定目录

watch命令可以将目录作为参数,用作本书的根目录,而不是当前工作目录.

mdbook watch path/to/book

–open

当你使用--open(-o)选项,mdbook 将在您的默认 Web 浏览器中打开网页书.

–dest-dir

--dest-dir(-d)选项允许您更改书籍的输出目录。为相对路径,(相对于书籍的根目录)。如果未指定,则默认为book.toml配置的build.build-dir字段, 或者./book目录.

指定排除的模式

watch.gitignore 里的文件,不会触发构建命令。.gitignore 是有匹配模式的文件 gitignore documentation。忽略那些缓存文件就挺有用的。

注意: 只有项目的顶层.gitignore是工作的。 全局的 $HOME/.gitignore or 上级文件夹的.gitignore 都不起作用

The serve command

serve 命令用于通过 HTTP 服务来预览书籍,默认情况下localhost:3000

mdbook serve

serve 观察 src 目录的变化,对每次变化,都会执行重建与刷新客户端; 还包括重新创建已删除,但仍在 SUMMARY.md里的文件! 一个 websocket 连接用于触发客户端刷新.

注意: serve命令用于测试书籍的 HTML 输出,并不打算成为网站的完整 HTTP 服务器.

指定目录

serve命令可以将目录作为参数,用作本书的根目录,而不是当前工作目录.

mdbook serve path/to/book

Server 选项

serve 默认端口:localhost:3000;或是通过下面命令改变:

mdbook serve path/to/book -p 8000 -n 127.0.0.1 

–open

当你使用--open(-o)标志,mdbook 将在启动服务器后,在您的默认 Web 浏览器中打开该书.

–dest-dir

--dest-dir(-d)选项允许您更改书籍的输出目录。为相对路径,(相对于书籍的根目录)。如果未指定,则默认为book.toml配置的build.build-dir字段, 或者./book目录.

指定排除的模式

watch.gitignore 里的文件,不会触发构建命令。.gitignore 是有匹配模式的文件 gitignore documentation。忽略那些缓存文件就挺有用的。

注意: 只有项目的顶层.gitignore是工作的。 全局的 $HOME/.gitignore or 上级文件夹的.gitignore 都不起作用

test 命令

写书时,有时需要一些自动化测试。例如,The Rust Programming Book使用了许多可能过时的代码示例。因此,能够自动测试这些代码示例对他们来说非常重要。

mdBook 支持test将运行,书中所有可用测试的命令。目前,只支持 rustdoc 测试,但未来可能会扩展。

在一个代码块,禁用测试

rustdoc 不会测试,包含ignore属性的代码块:

fn main() {}

rustdoc 也不会测试,指定了除 Rust 之外的语言的代码块:

**Foo**: _bar_

rustdoc不会测试,没有指定语言的代码块:

This is going to cause an error!

指定目录

test命令可以将目录作为参数,用作本书的根目录,而不是当前工作目录.

mdbook test path/to/book

–library-path

--library-path(-L)选项允许您,当rustdoc构建和测试示例时,将目录添加到搜索路径。可以指定多个目录(-L foo -L bar),或用逗号分隔的列表(-L foo,bar)。路径应该指向 Cargo build cache deps 目录,在其中包含了项目的输出。例如:你的目录叫 my-book, 下面的测试命令就可以带上 crate 的 依赖:

mdbook test my-book -L target/debug/deps/

See the rustdoc command-line documentation for more information.

–dest-dir

--dest-dir(-d)选项允许您更改书籍的输出目录。为相对路径,(相对于书籍的根目录)。如果未指定,则默认为book.toml配置的build.build-dir字段, 或者./book目录.

clean 命令

clean 命令用于删除生成的书籍,和任何其他构建工件.

mdbook clean

指定目录

clean命令可以将目录作为参数,用作本书的根目录,而不是当前工作目录.

mdbook clean path/to/book

–dest-dir

--dest-dir(-d)选项允许您覆盖书籍的输出目录,该目录会删除。 为相对路径,(相对于书籍的根目录)。如果未指定,则默认为book.toml配置的build.build-dir字段, 或者./book目录.

mdbook clean --dest-dir=path/to/book

path/to/book可以是绝对的,或相对的.

The completions command

completions 用于自动补全命令。 这意味着,当你在终端上,输入 mdbook, 你可以按下补全键 (通常是 Tab) ,就会补全你的命令。

当然,要先安装下:

mdbook completions bash > ~/.local/share/bash-completion/completions/mdbook

这个命令会打印补全脚本。 运行 mdbook completions --help 看看所支持的。

格式

在本节中,您将学习如何:

  • 正确构建您的书
  • 格式化你的SUMMARY.md文件
  • 使用book.toml配置您的图书
  • 自定义主题

SUMMARY.md

mdBook 使用 Summary 文件,来了解要书的章节,应显示的顺序,层次结构以及源文件的位置。没有这个文件,就没有书.

必须叫SUMMARY.md,它是一个 markdown 文件, 但格式是非常严格,以便于给 mdbook 解析。我们来看看你应该如何结构化你的SUMMARY.md文件.

结构

  1. Title 一般来说,通常以# Summary。标题开头是常见的做法,但它不是强制性的,解析器只是忽略它。如果你也是这样想,也忽略它。

    # Summary
    
  2. 开头章节 位于主编号章节前,您可以添加一些不编号的开头章节。这对前言,介绍等很有用。但是有一些限制。你不能嵌套开头章节,它们都应该在根级别。一旦添加了编号章节,就无法添加开头章节。

[A Prefix Chapter](relative/path/to/markdown.md)

- [First Chapter](relative/path/to/markdown2.md)
  1. Part Title - 标题,对书进行逻辑分割。它们渲染成不能点击的。 它是可选的,然后编号章节可以放在它下面:

    # My Part Title
    
    - [First Chapter](relative/path/to/markdown.md)
    
  2. 编号章节 是本书的主要内容,它们将被编号,并可以嵌套,从而产生一个很好的层次结构(章节,子章节等)

    # Title of Part
    
    - [First Chapter](relative/path/to/markdown.md)
    - [Second Chapter](relative/path/to/markdown2.md)
    - [Sub Chapter](relative/path/to/markdown3.md)
    
    # Title of Another Part
    
    - [Another Chapter](relative/path/to/markdown4.md)
    

    你可以使用-*表示编号的章节.

  3. 结尾章节 位于在编号章节后,您可以添加几个未编号的章节.它们与开头章节相同,但是在编号章节之后,而不是之前.

    - [Last Chapter](relative/path/to/markdown.md)
    
    [Title of Suffix Chapter](relative/path/to/markdown2.md)
    
  4. Draft chapters - 草稿章节。 它的作用就是,给章节的 TODO 字符串。 这类章节会被渲染,但是一个禁用的链接。 写法如下:

    - [Draft Chapter]()
    
  5. Separators - Separators 可以在任意其他元素,之前,之间,之后添加。 ---.

    # My Part Title
    
    [A Prefix Chapter](relative/path/to/markdown.md)
    
    ---
    
    - [First Chapter](relative/path/to/markdown2.md)
    

Example

下面是指南的 SUMMARY.md,渲染结果正是这本书左边的内容。

# Summary

[介绍](README.zh.md)

# User Guide

- [安装](guide/installation.zh.md)
- [读读读](guide/reading.zh.md)
- [书的制作](guide/creating.zh.md)

# Reference Guide

- [命令行](cli/README.zh.md)
  - [init](cli/init.zh.md)
  - [build](cli/build.zh.md)
  - [watch](cli/watch.zh.md)
  - [serve](cli/serve.zh.md)
  - [test](cli/test.zh.md)
  - [clean](cli/clean.zh.md)
  - [completions](cli/completions.zh.md)
- [规范化](format/README.zh.md)
  - [SUMMARY.md](format/summary.zh.md)
    - [Draft chapter]()
  - [配置](format/configuration/README.zh.md)
    - [通用](format/configuration/general.zh.md)
    - [预处理器](format/configuration/preprocessors.zh.md)
    - [渲染器](format/configuration/renderers.zh.md)
    - [环境变量](format/configuration/environment-variables.zh.md)
  - [主题](format/theme/README.zh.md)
    - [index.hbs](format/theme/index-hbs.zh.md)
    - [Syntax highlighting](format/theme/syntax-highlighting.zh.md)
    - [Editor](format/theme/editor.zh.md)
  - [MathJax 支持](format/mathjax.zh.md)
  - [mdBook 特殊功能](format/mdbook.zh.md)
  - [Markdown](format/markdown.zh.md)
- [集成服务器](continuous-integration.zh.md)
- [For Developers](for_developers/README.zh.md)
  - [预处理器的大小事](for_developers/preprocessors.zh.md)
  - [备用的后端](for_developers/backends.zh.md)

---

[Contributors](misc/contributors.zh.md)

Configuration

本节详细介绍了**book.toml**中可用的配置选项:

配置

您可以在 book.toml 文件中配置图书的参数.

这是一个 book.toml 文件示例,如下所示:

[book]
title = "Example book"
author = "John Doe"
description = "The example book covers examples."

[build]
build-dir = "my-example-book"
create-missing = false

[preprocessor.index]

[preprocessor.links]

[output.html]
additional-css = ["custom.css"]

[output.html.search]
limit-results = 15

支持的配置选项

重要的是要注意到,配置中指定的任何相对路径,都是相对于配置文件所在的书籍的根目录.

通用元数据

这是有关您图书的一般

信息描述
title这本书的标题
author本书的作者
description该书的描述,作为元信息,添加在每页 html 的<head>
src默认情况下,源目录位于名为src的目录中(在根文件夹)。但这src是可配置的,就在book.toml
language:国家语言, 例如用在网页的 <html lang="en"> 属性。

book.toml

[book]
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
src = "my-src"  # 源文件夹,用 `root/my-src` 替代 `root/src`
language = "zh-CN"

Rust options

Rust 语言的选项,与运行测试和集成相关。

[rust]
edition = "2015"   # 默认版本
  • 版本:默认情况下用于代码段的 Rust edition。默认值为2015。单个代码块可以通过edition2015, edition2018edition2021注释,例如:

    ```rust,edition2015
    // This only works in 2015.
    let try = true;
    ```
    

Build 选项

这可以控制您图书的构建过程.

[build]
build-dir = "book"                # the directory where the output is placed
create-missing = true             # whether or not to create missing pages
use-default-preprocessors = true  # use the default preprocessors
  • build-dir | 放置渲染图书的目录。默认情况下在根目录的book/:

  • create-missing | 默认情况(create-missing = true)下,在书籍建成时,会创建SUMMARY.md中缺失的文件。如果是false,则有文件不存在,那么构建过程将以错误退出。

  • use-default-preprocessors | 设为 false,会禁用(links&index)的默认预处理器。

    如果您通过配置文件声明了,相同的和/或其他预处理器,由它们主导.

    • 为清楚起见,没有预处理器配置,默认linksindex运行.
    • 设置use-default-preprocessors = false,将禁用这些默认预处理器运行.
    • 若添加[preprocessor.links],那无论如何,都能确保use-default-preprocessors运行links

配置,预处理器

预处理器的作用是,在送进 renderer 之前,修改原始的 Markdown 文本。

内置的有:

  • links:扩展章节中{{ #playground }}{{ #include }},和 {{ #rustdoc_include }} 控制条,能帮助引入文件的内容。查看 Including files for more.
  • index:将所有名为README.md的章节文件,转换index.md。也就是说,所有README.md将被渲染成index.html

内置的预处理器,可以通过build.use-default-preprocessors禁用。

社区也开发了几个预处理器,可以看看这里Third Party Plugins

关于如何创建一个新的预处理器,可以看看Preprocessors for Developers 章节。

自定义预处理器配置

与渲染器一样,预处理器需要有自己的表格(例如[preprocessor.mathjax])。在该部分中,您可以通过向特有表中,添加键值对来将额外配置传递给预处理器.

例如,如果你有个预处理器叫mdbook-example那么可以这样

[preprocessor.example]

有了这个配置,mdBook 会执行,mdbook-example 的预处理器。

在这上面,还能添加 key-value 的配置。例如,需要额外的配置选项:

[preprocessor.example]
some-extra-feature = true

锁住一个预处理依赖给一个渲染器

您可以通过将两者绑定在一起,显式指定预处理器将运行的渲染器

[preprocessor.example]
renderers = ["html"]  # example 只 HTML 渲染合作

添加你的命令

默认情况下,添加[preprocessor.foo]到你的book.toml文件,mdbook将尝试调用mdbook-foo启动。如果你想使用不同的程序名 或传递一个命令行参数。这些都能通过command字段覆盖完成。

[preprocessor.random]
command = "python random.py"

顺序执行

执行的顺序可以通过, beforeafter 字段完成。 例如:假设你想 {{#include}} 之后,再执行linenos; 然后,你想在links 之后运行,beforeafter 就派上用场了:

[preprocessor.linenos]
after = [ "links" ]

[preprocessor.links]
before = [ "linenos" ]

虽然多余,但也可以在同一个配置文件中,指定上述两者。

具有相同优先级的预处理器通过 beforeafter,以名字排序。 将检测到任何无限循环并产生错误。

配置渲染器

渲染器(也叫后端),职责是书的输出。

内置的渲染器有:

  • html — HTML 的输出. 默认启动, 如果 book.toml 上的[output],没有其他设置。
  • markdown — 运行 预处理器之后,输出 markdown。 调试专用。

社区也有,可以看看 Third Party Plugins

开发的话,看这里 Backends for Developers

Output tables

Backends 通过 book.tomloutput 设置,记得带上名称。 比如:它叫做 mdbook-wordcount, 写入:

[output.wordcount]

有了这个设置, mdBook 会执行 mdbook-wordcount 后端。

还能添加额外的配置参数,例如: 如果我们 wordcount 需要其他的配置选项:

[output.wordcount]
ignores = ["Example Chapter"]

如果,定义了任意的 [output],那么 html 就不会启动。 如果你想 html 在后台运行,请写入 book.toml 文件。 For example:

[book]
title = "My Awesome Book"

[output.wordcount]

[output.html]

如果定义output,超过一个 ,那么这会,改变输出目录的布局。 如果只有一个后端,那么,输出目录就在 book 目录 (配置 build.build-dir,覆盖位置)。 如果超过一个后端,那么,就会放在 book下,分开的目录。 例如:上面的命令就会有 book/htmlbook/wordcount

Custom backend commands

默认情况下,将 [output.foo] 添加到 book.toml 文件, mdbook 会尝试调用 mdbook-foo 可执行文件。 如果,你使用了一个不同的程序名称,或传递命令行参数,这些行为都会被 command 覆盖。

[output.random]
command = "python random.py"

Optional backends

如果你的后端是没有安装的,那么默认的行为就是抛出一个错误。 这个错误的纠正,可以将后端设为可选:

[output.wordcount]
optional = true

这会将 错误,变成警告。

HTML 渲染器选项

HTML 渲染器也有几个选项,在 TOML 下指定渲染器的所有选项.

# Example book.toml file with all output options.
[book]
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."

[output.html]
theme = "my-theme"
default-theme = "light"
preferred-dark-theme = "navy"
curly-quotes = true
mathjax-support = false
copy-fonts = true
additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"]
no-section-label = false
git-repository-url = "https://github.com/rust-lang/mdBook"
git-repository-icon = "fa-github"
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
site-url = "/example-book/"
cname = "myproject.rs"
input-404 = "not-found.md"

| [output.html]可以使用以下配置选项: | 描述 | | ———————————— | —————————————————————————————————————————————————————————————————————————————————————————————————————————— | — | | theme | mdBook 附带一个默认主题,及其所需的所有资源文件.但是如果设置了此选项,mdBook 将选择性地使用,能在指定文件夹中找到的主题文件,覆盖主题文件。 | | default-theme | 默认情况下在“更改主题“下拉列表中,选择的主题颜色方案。默认为light。 | | preferred-dark-theme: | 默认 dark 主题。如果要求 dark 主题,通过 ‘prefers-color-scheme’ CSS 媒体查询,就是网站的 dark 版本 。 默认是 navy。 | | curly-quotes | 将直引号转换为反引号,除了代码块和代码 spans 中出现的引号。默认为false。 | | copy-fonts: | 复制 fonts.css 和 各自 font 文件到输出目录,和在默认主题使用它们。 默认为true。 | | mathjax-support: | 增加对 MathJax 的支持。 默认是false。 | | google-analytics | 如果您使用 Google Analytics,则可以通过在配置文件中指定 ID 来启用此选项。 | | additional-css | 如果您需要稍微更改图书的外观,而不覆盖整个样式,则可以指定一组 css 样式表,这些样式表将在默认情况下加载,这样您就通过’外科手术’更改样式。 | | additional-js | 如果您需要在不删除当前行为的情况下,向书中添加某些行为,则可以指定一组,将与默认文件一起加载的 JavaScript 文件。 | | no-section-label | 默认情况下,mdBook 在目录列中,添加章节标签编号。例如,“1.”,“2.1”。将此选项设置为 true 可禁用这些标签.默认为false。 | | | git-repository-url | 这本书的 git 存储库的 URL。如果提供,将在书的菜单栏中,输出图标链接。 | | git-repository-icon | 用于 git 存储库链接的 FontAwesome 图标类。默认为fa-github。看起来就像 。 | | edit-url-template: | 编辑按钮, 提供 “编辑” 按钮 (看起来就像 ) ,方便定位到源代码的文档文件。例如:如果你是 Github 项目,设为 https://github.com/<owner>/<repo>/edit/master/{path} 或是 Bitbucket 项目,设为 https://bitbucket.org/<owner>/<repo>/src/master/{path}?mode=edit ,其中的 {path} 自然要修改成你的项目路径 | | input-404: | 404 要展示的文件,文件后缀会换成 html。 默认为404.md。 | | site-url: | 主页网址。确保网页的链接 和 script/css 为正确路径。默认为/。 | | cname: | DNS 子域名 或是 apex 域名 。写入根目录下的 CNAME 文件中,就像 GitHub Pages 的 CNAME 一样 ( 管理你 GitHub Pages 的域名). |

[output.html.print]

[output.html.print] 定义打印式输出. 默认情况下,mdBook 给出打印按钮 (看起来就像 ) ,将打印单页。

[output.html.print]
enable = true    # 添加打印功能
  • enable: 启用打印。 默认为true

[output.html.fold]

[output.html.fold] 能控制导航栏的折叠。

[output.html.fold]
enable = false    # whether or not to enable section folding
level = 0         # the depth to start folding
  • enable: 默认是false
  • level: 开启折叠的层级。level 如果是 0, 全部折叠。默认为0

[output.html.playground]

[output.html.playground] 提供导入 Rust 代码,和与 Rust Playground 集成的功能。

[output.html.playground]
editable = false         # allows editing the source code
copyable = true          # include the copy button for copying code snippets
copy-js = true           # includes the JavaScript for the code editor
line-numbers = false     # displays line numbers for editable code
[output.html.playground]可用的配置选项表:描述
editable允许编辑源代码。默认为false
copyable:复制按钮,默认为 true
copy-js将编辑器的 JavaScript 文件,复制到输出目录。默认为true
line-numbers显示行数,要求 editablecopy-js 都为 true。默认为 false

[output.html.search]

[output.html.search] 控制 search 的配置。 mdBook 需要 search 功能开启 (默认就有)。

[output.html.search]
enable = true            # enables the search feature
limit-results = 30       # maximum number of search results
teaser-word-count = 30   # number of words used for a search result teaser
use-boolean-and = true   # multiple search terms must all match
boost-title = 2          # ranking boost factor for matches in headers
boost-hierarchy = 1      # ranking boost factor for matches in page names
boost-paragraph = 1      # ranking boost factor for matches in text
expand = true            # partial words will match longer terms
heading-split-level = 3  # link results to heading levels
copy-js = true           # include Javascript code for search
[output.html.search]可用的配置选项表:描述
enable启用搜索功能.默认为true
limit-results搜索结果的最大数量.默认为30
teaser-word-count搜索结果预告的单词数。默认为30
use-boolean-and定义多个搜索词之间的逻辑链接。如果为 true,则所有搜索词必须出现在每个结果中。默认为true
boost-title如果标题中出现搜索词,则提升搜索结果。默认为2
boost-hierarchy如果搜索结果出现在层次结构中,则提升搜索结果。层次结构包含父文档的所有标题,和所有父标题。默认为1
boost-paragraph如果搜索词出现在文本中,则提升搜索结果。默认为1
expand默认搜索匹配更长的结果。搜索micro应该匹配microwave。默认为true
heading-split-level搜索结果将链接到包含结果的文档部分。文档按此级别或更低级别划分为多个部分。默认为3.(### This is a level 3 heading)
copy-js将搜索实现的 JavaScript 文件,复制到输出目录。默认为true

[output.html.redirect]

[output.html.redirect] 添加跳转 用于你对一个页面的移动,重命名,移除,确保旧链接,跳转到新位置。

[output.html.redirect]
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
"/other-installation-methods.html" = "../infra/other-installation-methods.html"

key-value 格式,key 是需要跳转文件,来自构建目录的绝对路径。 (例如: /appendices/bibliography.html). 将要跳转到的位置 (例如:https://rust-lang.org/, /overview.html, or ../bibliography.html).

生成跳转到新位置的 HTML 页面。 注意,是不支持 # 的网络链接定位的。

Markdown 渲染器

Markdown 渲染器会运行预处理器,再输出结果的 Markdown。 最大的用途就是调试预处理器, 尤其是,可以结合 mdbook test看到,mdbook 传递给rustdoc的 Markdown 文本。

Markdown 渲染器 已包含在 mdbook ,但默认禁用。 启用方式,是在book.toml中,添加:

[output.markdown]

现在还没有配置选项; 只能开和关

查看 preprocessors 文档 ,关于如何指定哪个 preprocessors 应在 the Markdown renderer 之前运行。

环境 变量

通过设置相应的环境变量,命令行的运行,覆盖到所有配置值。因为许多操作系统将环境变量限制为_字母数字字符,配置字段需要格式化成,正常情况的foo.bar.baz形式。

变量以MDBOOK_开头配置。通过删除MDBOOK_前缀,并将结果字符串转换为kebab-case。双下划线(__)变. ,而单个下划线(_)用短划线代替(-).

例如:

  • MDBOOK_foo -> foo
  • MDBOOK_FOO -> foo
  • MDBOOK_FOO__BAR -> foo.bar
  • MDBOOK_FOO_BAR -> foo-bar
  • MDBOOK_FOO_bar__baz -> foo-bar.baz

所以通过设置MDBOOK_BOOK__TITLE环境变量,你可以覆盖书的标题,而无需修改你的book.toml.

注意 为了便于设置更复杂的配置项,首先将环境变量的值解析为 JSON,如果解析失败,则返回到字符串.

这意味着,如果您愿意,可以在构建书籍时覆盖所有书籍元数据

$ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
$ mdbook build

后一种情况,在以下情况下可能有用,

  • 脚本或 CI 调用mdbook,
  • 有时在构建前,无法更新book.toml

Theme

默认渲染器使用一个handlebars模板,用于渲染 markdown 文件,并 mdBook 二进制文件包含默认主题.

主题是完全可定制的,您可以通过在根目录src旁边,新建一个theme文件夹,在其中选择性地添加文件名称,覆盖主题的任意文件。

以下是您可以覆盖的文件:描述
index.hbshbs 模板.
head.hbsHTML <head> 部分.
header.hbs每个页面的头部
- css/样式文件
- css/chrome.cssUI 元素
- css/general.css基础样式
- css/print.css打印输出的样式
- css/variables.csscss 变量
book.js主要用于添加客户端功能,如隐藏/取消隐藏侧边栏,更改主题,…
highlight.js是用于突出显示代码片段的 JavaScript,您不需要修改它.
highlight.css是用于代码突出显示的主题
favicon.svg将使用的 favicon

通常,当您想要调整主题时,您不需要覆盖所有文件。如果您只需要更改 css 样式表,那么覆盖所有其他文件是没有意义的。由于自定义文件优先于内置文件,那以后的新的修补程序/功能,你都更新不了。

注意: 覆盖文件时,可能会破坏某些功能。因此,我建议使用默认主题中的文件作为模板,只添加/修改您需要的内容。您可以使用mdbook init --theme命令自动将默认主题自动复制到源目录中,只需删除您不想覆盖的文件。

mdbook init --theme 不会创建上面所提到的文件。 比如 head.hbs文件,是没有同等(示例)文件。 所以,你需要自行完成创建。

如果你想完全替换内置主题,请在 output.html.preferred-dark-theme设置,不然默认还是 navy 主题

index.hbs

index.hbs是用于渲染书籍的 hbs 模板。markdown 文件被处理为 html,然后注入该模板.

如果您想更改图书的布局或样式,您可能需要稍微修改此模板。那下面是你需要知道的。

Data

大量数据通过“上下文“暴露给 hbs 模板.在 hbs 模板中,您可以使用以下方式访问此信息

{{name_of_property}}

以下是公开的属性列表:

  • language |> 书的语言en。例如<code class="language-html">\\<html lang="{{ language }}"></code>
  • title |> 该书的标题,如book.toml中所述
  • chapter_title |> 当前章节的标题,格式是 {{ chapter_title }} - {{ book_title }},除非 unless book_title 没有设置,这种情况下,默认为 chapter_title
  • path |> 源目录中原始 markdown 文件的相对路径
  • content |> 这是渲染的 markdown.
  • path_to_root |> 这是一条完全包含../的路径,这会是从当前文件指向书的根。由于维护了原始目录结构,因此使用此前缀相对链接很有用.
  • chapters |> 是一个字典数组
{
  "section": "1.2.1",
  "name": "name of this chapter",
  "path": "dir/markdown.md"
}

包含本书的所有章节.它用于例如构建目录(侧边栏).

Handlebars 帮手

除了您可以访问的属性外,您还可以使用一些 hbs 帮手.

1. toc

toc助手就像这样使用

```handlebars
{{#toc}}{{/toc}}
```

并输出看起来像这样的东西,这取决于你的书的结构

```html
<ul class="chapter">
    <li><a href="link/to/file.html">Some chapter</a></li>
    <li>
        <ul class="section">
            <li><a href="link/to/other_file.html">Some other Chapter</a></li>
        </ul>
    </li>
</ul>
```

如果您想使用其他结构创建一个toc,则可以访问包含所有数据的chapters属性。
目前唯一的限制是您必须使用JavaScript,而不是使用hbs帮助程序。

```html
<script>
var chapters = {{chapters}};
// Processing here
</script>
```

2. previous / next

上一个和下一个助手将`link`和`name`属性暴露给前一章和下一章。

就像这样使用

```handlebars
{{#previous}}
    <a href="{{link}}" class="nav-chapters previous">
        <i class="fa fa-angle-left"></i>
    </a>
{{/previous}}
```

只有在前一个/下一个章节存在时,才会渲染内部html。
当然内部html可以根据自己的喜好进行更改。

如果您希望其他属性或帮手,请create a new issue

语法高亮

mdBook 使用Highlight.js自定义主题,完成语法高亮的功能。

自动语言检测已关闭,因此您可能希望指定您使用的编程语言

```rust
fn main() {
    // Some code
}
```

Supported languages

这些是默认支持的语言,你还可以通过应用你的highlight.js 文件,达到添加的作用:

  • apache
  • armasm
  • bash
  • c
  • coffeescript
  • cpp
  • csharp
  • css
  • d
  • diff
  • go
  • handlebars
  • haskell
  • http
  • ini
  • java
  • javascript
  • json
  • julia
  • kotlin
  • less
  • lua
  • makefile
  • markdown
  • nginx
  • objectivec
  • perl
  • php
  • plaintext
  • properties
  • python
  • r
  • ruby
  • rust
  • scala
  • scss
  • shell
  • sql
  • swift
  • typescript
  • x86asm
  • xml
  • yaml

自定义主题

与主题的其余部分一样,用于语法突出显示的 css,可以使用您自己的文件覆盖.

  • highlight.js 通常你不应该覆盖这个文件,除非你想使用更新的版本.
  • highlight.css highlight.js 用于语法高亮的主题.

如果你想使用highlight.js另一个主题,可从他们的网站下载,或自己制作,重命名为highlight.css,并把它放进去根目录下的theme

现在将使用您的主题,而不是默认主题.

隐藏代码行数

mdBook 中有一个功能,可以通过在代码行前加上来隐藏代码行#.

# fn main() {
    let x = 5;
    let y = 6;

    println!("{}", x + y);
# }

将渲染为

fn main() {
    let x = 5;
    let y = 7;

    println!("{}", x + y);
}

目前,这仅适用于带注释的代码示例rust。因为它会与某些编程语言的语义冲突.在未来,我们希望通过这个,可在book.toml配置,这样每个人都可以从中受益.

加强默认主题

如果您认为默认主题看起来不适合特定语言,或者可以改进。随意地submit a new issue解释你的想法,我会看看它.

您还可以使用建议的改进创建拉取请求.

总的来说,主题应该是清淡和清醒,没有许多华丽的颜色.

编辑器

除了提供可运行的代码 playgrounds 之外,mdBook 还可以选择进行编辑。为了启用可编辑的代码块,需要添加以下内容到**book.toml**:

[output.html.playground]
editable = true

要使特定块可用于编辑,请使用该属性editable添加:

```rust,editable
fn main() {
    let number = 5;
    print!("{}", number);
}
```

注意新的Undo Changes的按钮会出现在可编辑的 playgrounds 中.

定制编辑器

默认情况下,编辑器是Ace编辑器,但是,如果需要,可以通过提供不同的文件夹来覆盖功能:

[output.html.playground]
editable = true
editor = "/path/to/editor"

请注意,要让编辑器更改正常运行,book.js里面的theme文件夹,你需要覆盖下,因为它与默认的 Ace 编辑器有一些耦合.

MathJax 支持

mdBook,可以用MathJax,支持数学公式

要启用 MathJax,您需要在book.toml中的output.html部分添加mathjax-support

[output.html]
mathjax-support = true

注意: MathJax 使用的常用分隔符尚不支持。你目前无法使用$$ ... $$作为分隔符,且\[ ... \]分隔符需要额外的反斜杠才能工作。希望这个限制很快就会解除.

注意: 在 MathJax 块中使用双反斜杠时(例如,在\begin{cases} \frac 1 2 \\ \frac 3 4 \end{cases}之类的命令中)你需要添加另外两个反斜杠(如,\begin{cases} \frac 1 2 \\\\ \frac 3 4 \end{cases}).

内联方程

内联方程由\\(\\)分隔。例如,渲染以下内联方程 \( \int x dx = \frac{x^2}{2} + C \),你要写下面的内容:

\\( \int x dx = \frac{x^2}{2} + C \\)

块方程

块方程由\\[\\]分隔.要渲染以下等式

\[ \mu = \frac{1}{N} \sum_{i=0} x_i \]

你会写:

\\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\]

mdBook 特有 markdown

隐藏代码行数

mdBook 中有一个功能,可以通过在代码行前加上#来隐藏代码行,像在 Rustdoc 一样。目前只适用 Rust 代码。

# fn main() {
    let x = 5;
    let y = 6;

    println!("{}", x + y);
# }

渲染为

fn main() {
    let x = 5;
    let y = 6;

    println!("{}", x + y);
}

这个代码块会有一个眼睛的图标 () ,控制那些隐藏的代码行。

Rust Playground

Rust language 会有一个 () w 按钮,调用 API,执行 rust 代码。 具体是发送给 Rust Playground,从而获取结果。


#![allow(unused)]
fn main() {
println!("Hello, World!");
}

如果没有 main 函数,自动包裹进入。

不想有运行按钮,你要加上 noplayground

```rust,noplayground
let mut name = String::new();
std::io::stdin().read_line(&mut name).expect("failed to read line");
println!("Hello {}!", name);
```

Rust code block attributes

额外的选项,可以通过 逗号,空格,tab 分隔。例如:

```rust,ignore
# This example won't be tested.
panic!("oops!");
```

mdbook test 去测试 Rust 代码时,尤为重要。 会用到与 rustdoc attributes相同的选项,如下:

  • editable — 启用 editor
  • noplayground — 移除运行按钮,但仍会测试。
  • mdbook-runnable — 强制具备,运行按钮 主要是搭配ignore 使用。例如代码没有被测试, 但你又想运行它。
  • ignore — 既不测试,也不能运行,只剩下语法高亮。
  • should_panic — 应该给出一个 panic
  • no_run — 测试时编译,但不运行。 运行按钮也不会出现。
  • compile_fail — 代码应该编译失败。
  • edition2015, edition2018, edition2021 — 强制使用特定的版本。 可以用 rust.edition ,全局设置。

include,包含文件内容

使用以下语法,您可以将文件包含到您的书中:

{{#include file.rs}}

文件的路径必须是 当前 源文件的 相对 路径.

通常,此命令用于包含代码段和示例。在这种情况下, 可指定文件的包含部分,例如其中只包含示例的相关行.

mdBook 会将 include 文件 解释成 markdown。 自从 include 命令常用来插入代码片段和示例,你会习惯使用 ``` 包裹这些命令,显示(include)文件的内容,而不是去解释它们。

```
{{#include file.rs}} ```

只 Include 一个文件的部分内容

常见要求是你只要文件的部分内容 e.g. 以行编号作为例子。我们对部分内容插入,支持了几种模式:

{{#include file.rs:2}}
{{#include file.rs::10}}
{{#include file.rs:2:}}
{{#include file.rs:2:10}}

我们支持四种不同的决定file.rs部分模式:

  • 第一个命令仅包含文件中的第二行.
  • 第二个命令包含直到第 10 行的所有行。即,从 11 到文件末尾的行被省略.
  • 第三个命令包含第 2 行的所有行,即省略第一行.
  • 最后一个命令包含摘录file.rs由 2 到 10 行组成.

为了防止文件的修改(行数的变化),导致图书呈现内容的变化, 你还可以使用锚点(anchor),选择一个特定的部分, 行数就不再相关。 一个 anchor 是一对匹配的行。 匹配的内容正是正则式,如开头的行 anchor 必须匹配 ANCHOR:\s*[\w_-]+ and similarly 结尾行则是 ANCHOR_END:\s*[\w_-]+。只要内容匹配了,注释的写法格式倒是没有限制。

考虑下面 要 include 的文件:

/* ANCHOR: all */

// ANCHOR: component
struct Paddle {
    hello: f32,
}
// ANCHOR_END: component

////////// ANCHOR: system
impl System for MySystem { ... }
////////// ANCHOR_END: system

/* ANCHOR_END: all */

给个示例,你要做的:

Here is a component: ```rust,no_run,noplayground
{{#include file.rs:component}} ``` Here is a system:
```rust,no_run,noplayground
{{#include file.rs:system}} ``` This is the full file.
```rust,no_run,noplayground
{{#include file.rs:all}} ```

在已 include anchor 内的,若还包含 anchor 匹配模式,则会被忽略。

Including a file but initially hiding all except specified lines

rustdoc_include 带有行数,与锚点的属性,其他的就与 include 一样。

被过滤的行数,仍会被包含进去,只不过用#隐藏了而已。你完全可以显示,隐藏的代码,而 mdbook test依旧是执行完全的代码。

例如,file.rs 包含下面的 Rust 代码:

fn main() {
    let x = add_one(2);
    assert_eq!(x, 3);
}

fn add_one(num: i32) -> i32 {
    num + 1
}

只显示第二行:

To call the `add_one` function, we pass it an `i32` and bind the returned value
to `x`: ```rust
{{#rustdoc_include file.rs:2}} ```

#完成相同的效果:

To call the `add_one` function, we pass it an `i32` and bind the returned value
to `x`: ```rust # fn main() { let x = add_one(2); # assert_eq!(x, 3); # } # # fn
add_one(num: i32) -> i32 { # num + 1 # } ```

可以通过点击 ( “expand” 按钮看到剩下的文件):

fn main() {
    let x = add_one(2);
    assert_eq!(x, 3);
}

fn add_one(num: i32) -> i32 {
    num + 1
}

插入可运行的 Rust 文件

使用以下语法,您可以将可运行的 Rust 文件插入到您的书中:

{{#playground file.rs}}

Rust 文件的路径必须是当前源文件的相对路径.

点击播放后,代码段将被发送到rust 的游乐场编译和运行。结果被返回,并直接显示在代码下方.

以下是代码段的渲染的样子:

fn main() {
    println!("Hello World!");

   // You can even hide lines! :D
  println!("I am hidden! Expand the code snippet to see me");
}

试试 点击 播放箭头

要可编辑,请添加

{{#playground example.rs editable no_run should_panic}}
fn main() {
    println!("Hello World!");

   // You can even hide lines! :D
  println!("I am hidden! Expand the code snippet to see me");
}

额外属性的添加:{{#playground example.rs editable}} ,它会变成:

```rust,editable
# Contents of example.rs here.
```

editable 会启用 editor ,就像Rust code block attributes所描述的。

Controlling page <title>

可以用 <title> 改变边栏的标题:

{{#title My Title}}

Markdown

在持续集成服务器中运行 mdbook

有几种集成服务器(CI),比如: GitHub Actions or GitLab CI/CD ,它们的作用是自动测试和发布你的文档。

接下来,有几个方式配置这些服务器。 更多信息,可看 Automated Deployment

Pre-compiled binaries

最简单的方式,莫过于去 GitHub Releases page,下载执行文件。 使用 curl 快速下载:

mkdir bin
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.15/mdbook-v0.4.15-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
bin/mdbook build

这个方法的几个特点:

  • 够快,没有什么缓存。
  • 不用安装 Rust。
  • 需要自己手动更新 当然,如果是要锁定某个版本的话,这是一个优势。 然而,自动更新,也是许多人想要的。
  • 搭上 GitHub CDN。

Building from source

这个方式,要求安装了 Rust。 会有些服务器是安装的, 但如果没有,你需要加安装 Rust 的步骤。

一旦 Rust 安装上了, cargo install 下载和构建 mdBook。. 强烈建议使用 SemVer 版本语法,这样你就能得到一个 non-breaking 版本的 mdBook。 For example:

cargo install mdbook --no-default-features --features search --vers "^0.4" --locked

其中,包含了几项建议:

  • --no-default-features — 禁掉默认功能,比如:mdbook serve命令需要的 HTTP server,这些在 CI 上都是不需要的。加速构建。

  • --features search — 禁用了默认功能,意味着你要指定功能,比如,搜寻 search 就挺有用的。

  • --vers "^0.4" — 会下载与 0.4 最为接近的版本。 但是,像 0.5.0之后的版本就不会安装。因为,它们有可能不兼容。 Cargo 会帮你自动升级。

  • --locked — 锁定依赖的版本,要是没有 --locked, 所有的依赖就会用最新的版本。即可能是 bug 修复,也可能导致构建问题。

最好看看,关于缓存的选项,能帮你的构建节省时间。

Running tests

mdbook test 会测试你的 Rust 测试代码。也用来测试书内的代码示例。

这个方式,要求安装了 Rust。 会有些服务器是安装的, 但如果没有,你需要加安装 Rust 的步骤。

除了,恰当的 Rust 版本,和mdbook test,你已然不需要其他的了。

当然,若是你像运行其他测试,像 mdbook-linkcheck 检查,网络链接的完整. 或是 样式,拼写啊,杂七杂八的测试啊,都可以在 CI 上,搞定。

Deploying

自动发布的功能,有些人希望每次改变就来一次;当然,也特殊 tag 再来一次,这些都可以做到。

或许,你还想了解下,如何推送新改变的。 例如, GitHub Pages 仅仅像正常 push 下,就自动搞定。

下一步,自然是 mdbook build,生成内容,输出到目标目录 (默认是,当地的 book 目录) 。

更多请看 Automated Deployment ,上面有多个不同示例。

404 handling

错误链接就给 404 页面。默认名称就是 404.html 。 像 GitHub Pages ,就会自动跳转。 其他,可能需要配置配置。

如果你的书,不是放置在顶层网址上,那么 output.html.site-url 必不可少。 书用到的静态文件 (like CSS) ,都是需要正确获取的。 就好比,这本书 https://chinanf-boy.github.io/mdBook-zh/, 那么 site-url 就是:

# book.toml
[output.html]
site-url = "/mdBook-zh/"

自定义 404 页面也是可以的,比如: src/404.md。 当然,对应要配置下output.html.input-404

致 Developers

虽然mdbook主要用作命令行工具, 但您也可以直接导入底层库,并使用它来管理书籍。它还具有相当灵活的插件机制,允许您创建自己的自定义工具和消费者(通常称为后端),如果您需要对书籍进行一些分析,或以不同的格式渲染它.

对于开发人员 章节在这里向您展示mdbook更高级的用法.

开发人员可以通过以下两种方式,影响本书的构建过程,

构建过程

渲染图书项目的过程经历了几个步骤.

  1. 加载书
    • 解析book.toml。 如果其中不存在,使用Config默认值。
    • 将书籍章节加载到内存中
    • 了解应该使用哪些预处理器/后端
  2. 对于每个后端:
    1. 运行所有的预处理器
    2. 调用后端,渲染处理过的结果

mdbook作为库使用

mdbook二进制只是一个mdbook箱的包装器,将其功能暴露出来,作为命令行程序。因此,很容易自制使用mdbook的程序,并添加自己的功能(例如自定义预处理器)或调整构建过程。

如何找到使用mdbook箱子最简单方法,答案就是API 文档。顶级 API 文档解释了如何使用MDBook类型,加载和构建一本书,而config模块很好地解释了配置系统。

Preprocessors

一个 预处理器 只是一些代码,运行在加载书之后,和渲染之前,允许您更新和改变本书。可能的用例是:

  • 创建自定义帮助程序{{#include /path/to/file.md}}
  • 用 latex 样式($$ \frac{1}{3} $$)的表达式代替为 mathjax 的等价物

配置 预处理器 获取更多信息。

勾住 MDBook

MDBook 使用一种相当简单的机制来发现第三方插件。book.toml添加了一个新表格(例如preprocessor.foo,给foo预处理器),然后mdbook将尝试调用mdbook-foo程序,作为构建过程的一部分.

一旦预处理器定义了,和构建过程开始,mdBook 会执行preprocessor.foo.command命令两次。 第一次是,预处理器检测出是否支持所给予的渲染器。 mdBook 会传递两个参数: 第一个是 supports 和第二个参数是渲染器的名字。 支持的话,状态就是 0;不然退出代码为非零。

如果支持, 那么 mdbook 开启命令的第二次运行, 将 JSON 数据传递到 stdin。 The JSON 的组成是一个 [context, book] 数组 ( context 是序列化对象 PreprocessorContext) 和 book (是一个Book 包含书的内容)。

预处理器应该返回 Book 对象的 JSON 格式到 stdout,带有它对内容的修改。

最简单的入门方法是创建自己的实现Preprocessor trait(例如在lib.rs),然后创建一个 shell 二进制文件,将输入转换为正确的Preprocessor方法。为方便起见,有个无操作预处理器:示例examples/目录,可以很容易地适应其他预处理器.

Example 无操作预处理器
// nop-preprocessors.rs

extern crate clap;
extern crate mdbook;
extern crate serde_json;

use clap::{App, Arg, ArgMatches, SubCommand};
use mdbook::book::Book;
use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
use std::io;
use std::process;
use nop_lib::Nop;

pub fn make_app() -> App<'static, 'static> {
    App::new("nop-preprocessor")
        .about("A mdbook preprocessor which does precisely nothing")
        .subcommand(
            SubCommand::with_name("supports")
                .arg(Arg::with_name("renderer").required(true))
                .about("Check whether a renderer is supported by this preprocessor"))
}

fn main() {
    let matches = make_app().get_matches();

    // Users will want to construct their own preprocessor here
    let preprocessor = Nop::new();

    if let Some(sub_args) = matches.subcommand_matches("supports") {
        handle_supports(&preprocessor, sub_args);
    } else {
        if let Err(e) = handle_preprocessing(&preprocessor) {
            eprintln!("{}", e);
            process::exit(1);
        }
    }
}

fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
    let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;

    if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
        // We should probably use the `semver` crate to check compatibility
        // here...
        eprintln!(
            "Warning: The {} plugin was built against version {} of mdbook, \
             but we're being called from version {}",
            pre.name(),
            mdbook::MDBOOK_VERSION,
            ctx.mdbook_version
        );
    }

    let processed_book = pre.run(&ctx, book)?;
    serde_json::to_writer(io::stdout(), &processed_book)?;

    Ok(())
}

fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
    let renderer = sub_args.value_of("renderer").expect("Required argument");
    let supported = pre.supports_renderer(&renderer);

    // Signal whether the renderer is supported by exiting with 1 or 0.
    if supported {
        process::exit(0);
    } else {
        process::exit(1);
    }
}

/// The actual implementation of the `Nop` preprocessor. This would usually go
/// in your main `lib.rs` file.
mod nop_lib {
    use super::*;

    /// A no-op preprocessor.
    pub struct Nop;

    impl Nop {
        pub fn new() -> Nop {
            Nop
        }
    }

    impl Preprocessor for Nop {
        fn name(&self) -> &str {
            "nop-preprocessor"
        }

        fn run(
            &self,
            ctx: &PreprocessorContext,
            book: Book,
        ) -> Result<Book, Error> {
            // In testing we want to tell the preprocessor to blow up by setting a
            // particular config value
            if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
                if nop_cfg.contains_key("blow-up") {
                    return Err("Boom!!1!".into());
                }
            }

            // we *are* a no-op preprocessor after all
            Ok(book)
        }

        fn supports_renderer(&self, renderer: &str) -> bool {
            renderer != "not-supported"
        }
    }
}

实现一个预处理器的提示

通过拉取mdbook,作为一个库,预处理器可以访问现有的基础架构来处理书籍.

例如,自定义预处理器可以使用CmdPreprocessor::parse_input()函数, 用于反序列化写入stdin的 JSON。然后是Book的每一章可以通过Book::for_each_mut()成为可变权限,然后随着serde_json箱写到stdout.

章节可以直接访问(通过递归迭代章节)或通过便利方法Book::for_each_mut().

chapter.content只是一个恰好是 markdown 的字符串。虽然完全可以使用正则表达式或进行手动查找和替换,但您可能希望将输入处理为更加计算机友好的内容。该pulldown-cmarkcrate 实现了一个基于事件,生产质量的 Markdown 解析器,而pulldown-cmark-to-cmark允许您将事件转换回 markdown 文本.

以下代码块,显示了如何从 markdown 中删除所有强调(粗体),而不会意外地破坏文档.


#![allow(unused)]
fn main() {
fn remove_emphasis(
    num_removed_items: &mut usize,
    chapter: &mut Chapter,
) -> Result<String> {
    let mut buf = String::with_capacity(chapter.content.len());

    let events = Parser::new(&chapter.content).filter(|e| {
        let should_keep = match *e {
            Event::Start(Tag::Emphasis)
            | Event::Start(Tag::Strong)
            | Event::End(Tag::Emphasis)
            | Event::End(Tag::Strong) => false,
            _ => true,
        };
        if !should_keep {
            *num_removed_items += 1;
        }
        should_keep
    });

    cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
        Error::from(format!("Markdown serialization failed: {}", err))
    })
}
}

对于其他的一切,看完整的例子.

Implementing a preprocessor with a different language

stdin 与 stdout 是预处理器的交流频道,这种方式让其他语言也能参与进来。比如 Python,下面是修改第一章内容的示例: preprocessor.foo.command 指向的实际命令。

import json
import sys


if __name__ == '__main__':
    if len(sys.argv) > 1: # we check if we received any argument
        if sys.argv[1] == "supports":
            # then we are good to return an exit status code of 0, since the other argument will just be the renderer's name
            sys.exit(0)

    # load both the context and the book representations from stdin
    context, book = json.load(sys.stdin)
    # and now, we can just modify the content of the first chapter
    book['sections'][0]['Chapter']['content'] = '# Hello'
    # we are done with the book's modification, we can just print it to stdout,
    print(json.dumps(book))

备用后端

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

Configuring Renderers 有更多信息。

社区也开发了几个后端 [Third Party Plugins]

目录

设置好

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

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

$ cargo new --bin mdbook-wordcount
$ cd mdbook-wordcount
$ cargo add mdbook

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

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

// src/main.rs
extern crate mdbook;

use std::io;
use mdbook::renderer::RenderContext;

fn main() {
    let mut stdin = io::stdin();
    let ctx = RenderContext::from_json(&mut stdin).unwrap();
}

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

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

检查 Book

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

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

fn main() {
    let mut stdin = io::stdin();
    let ctx = RenderContext::from_json(&mut stdin).unwrap();

    for item in ctx.book.iter() {
        if let BookItem::Chapter(ref ch) = *item {
            let num_words = count_words(ch);
            println!("{}: {}", ch.name, num_words);
        }
    }
}

fn count_words(ch: &Chapter) -> usize {
    ch.content.split_whitespace().count()
}

启用吧,我的 Backend

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

$ cargo install --path .

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

  [book]
  title = "mdBook Documentation"
  description = "Create book from markdown files. Like Gitbook but implemented in Rust"
  authors = ["Mathieu David", "Michael-F-Bryan"]

+ [output.html]

+ [output.wordcount]

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

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

现在你只需要像平常一样构建你的书,一切都应该 可以工作

$ mdbook build
...
2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
mdBook: 126
Command Line Tool: 224
init: 283
build: 145
watch: 146
serve: 292
test: 139
Format: 30
SUMMARY.md: 259
Configuration: 784
Theme: 304
index.hbs: 447
Syntax highlighting: 314
MathJax Support: 153
Rust code specific features: 148
For Developers: 788
Alternative Backends: 710
Contributors: 85

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

  [book]
  title = "mdBook Documentation"
  description = "Create book from markdown files. Like Gitbook but implemented in Rust"
  authors = ["Mathieu David", "Michael-F-Bryan"]

  [output.html]

  [output.wordcount]
+ command = "python /path/to/wordcount.py"

配置

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

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

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

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

$ cargo add serde serde_derive

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


#![allow(unused)]
fn main() {
extern crate serde;
#[macro_use]
extern crate serde_derive;

...

#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct WordcountConfig {
  pub ignores: Vec<String>,
}
}

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

  fn main() {
      let mut stdin = io::stdin();
      let ctx = RenderContext::from_json(&mut stdin).unwrap();
+     let cfg: WordcountConfig = ctx.config
+         .get_deserialized("output.wordcount")
+         .unwrap_or_default();

      for item in ctx.book.iter() {
          if let BookItem::Chapter(ref ch) = *item {
+             if cfg.ignores.contains(&ch.name) {
+                 continue;
+             }
+
              let num_words = count_words(ch);
              println!("{}: {}", ch.name, num_words);
          }
      }
  }

输出和信号故障

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

+ use std::fs::{self, File};
+ use std::io::{self, Write};
- use std::io;
  use mdbook::renderer::RenderContext;
  use mdbook::book::{BookItem, Chapter};

  fn main() {
    ...

+     let _ = fs::create_dir_all(&ctx.destination);
+     let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
+
      for item in ctx.book.iter() {
          if let BookItem::Chapter(ref ch) = *item {
              ...

              let num_words = count_words(ch);
              println!("{}: {}", ch.name, num_words);
+             writeln!(f, "{}: {}", ch.name, num_words).unwrap();
          }
      }
  }

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

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

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

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

+ use std::process;
  ...

  fn main() {
      ...

      for item in ctx.book.iter() {
          if let BookItem::Chapter(ref ch) = *item {
              ...

              let num_words = count_words(ch);
              println!("{}: {}", ch.name, num_words);
              writeln!(f, "{}: {}", ch.name, num_words).unwrap();

+             if cfg.deny_odds && num_words % 2 == 1 {
+               eprintln!("{} has an odd number of words!", ch.name);
+               process::exit(1);
              }
          }
      }
  }

  #[derive(Debug, Default, Serialize, Deserialize)]
  #[serde(default, rename_all = "kebab-case")]
  pub struct WordcountConfig {
      pub ignores: Vec<String>,
+     pub deny_odds: bool,
  }

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

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

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

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

包涵包涵

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

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

Contributors

Here is a list of the contributors who have helped improving mdBook. Big shout-out to them!

If you feel you’re missing from this list, feel free to add yourself in a PR.