mdBook

mdBook是一个命令行工具和Rust crate。可将Markdown文件创建为书籍。它与Gitbook非常相似,但用Rust编写.

您正在阅读的内容,是mdBook输出的一个示例,同时也是一个高级文档.

mdBook是免费的开源,你可以在GitHub找到源代码。问题和功能请求可以发布在GitHub issue tracker.

API docs

除了这本书,你还可以阅读Rustdoc生成的API 文档。如果您想将mdBook用作包或者编写新的渲染器,并需要浏览底层代码.

License

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

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-nursery/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的目录.

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

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

build 命令

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

mdbook build

它会尝试解析你的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 命令,而不是在源src目录中运行

watch 命令

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

指定目录

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

mdbook watch path/to/book

--open

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

--dest-dir

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

The serve command

serve 命令用于通过 HTTP 服务来预览书籍,默认情况下localhost:3000。此外,它还会观察图书的目录的更改,自动重建图书,以及为每次更改刷新客户端。

websocket 连接用于触发客户端刷新.

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

指定目录

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

mdbook serve path/to/book

Server 选项

serve有四个选项:

  • HTTP 端口,
  • WebSocket 端口,
  • 要侦听的 HTTP 主机名,以及
  • 要连接到 WebSockets 的浏览器的主机名.

例如:假设您有一个公共地址为 192.168.1.100,可用 SSL 终止符 的 nginx 服务器,其端口 80 , 在127.0.0.1的 8000端口 上代理。要运行使用 nginx 代理,请执行以下操作:

mdbook serve path/to/book -p 8000 -n 127.0.0.1 --websocket-hostname 192.168.1.100

如果你想要实时重新加载,你需要通过 nginx 来代理 websocket 调用192.168.1.100:<WS_PORT>127.0.0.1:<WS_PORT>

-w选项会允许配置 websocket 端口.

--open

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

--dest-dir

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

test 命令

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

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

在一个代码块,禁用测试

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

```rust,ignore
fn main() {}
```

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

```markdown
**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).

--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可以是绝对的,或相对的.

格式

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

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

SUMMARY.md

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

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

允许的 elements

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

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

    [开头章节的标题](relative/path/to/markdown.md)
    
  3. 编号章节是本书的主要内容,它们将被编号,并可以嵌套,从而产生一个很好的层次结构(章节,子章节等)

    - [编号章节的标题](relative/path/to/markdown.md)
    

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

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

所有其他元素都不受支持,最多将被忽略或导致错误.

配置

您可以在 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"

Build 选项

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

  • 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

配置,预处理器

默认情况下,以下预处理器运行,并包含:

  • links:扩展章节中{{ #playpen }}{{ #include }}控制条,能帮助引入文件的内容.
  • index:将所有名为README.md的章节文件转换index.md。也就是说,所有README.md将被渲染成index.html,在渲染的书中.

book.toml

[build]
build-dir = "build"
create-missing = false

[preprocessor.links]

[preprocessor.index]

自定义预处理器配置

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

例如

[preprocessor.links]
# 设置此预处理器将运行的渲染器
renderers = ["html"]
some_extra_feature = true

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

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

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

添加你的命令

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

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

配置渲染器

HTML 渲染器选项

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

[output.html]可以使用以下配置选项:描述
thememdBook 附带一个默认主题,及其所需的所有资源文件.但是如果设置了此选项,mdBook 将选择性地使用,能在指定文件夹中找到的主题文件,覆盖主题文件。
default-theme默认情况下在”更改主题”下拉列表中,选择的主题颜色方案。默认为light
curly-quotes将直引号转换为反引号,除了代码块和代码 spans 中出现的引号。默认为false
mathjax-support:增加对 MathJax 的支持。 默认是

falsegoogle-analytics | 如果您使用 Google Analytics,则可以通过在配置文件中指定 ID 来启用此选项。 additional-css | 如果您需要稍微更改图书的外观,而不覆盖整个样式,则可以指定一组 css 样式表,这些样式表将在默认情况下加载,这样您就通过’外科手术’更改样式。 additional-js | 如果您需要在不删除当前行为的情况下,向书中添加某些行为,则可以指定一组,将与默认文件一起加载的 JavaScript 文件。 no-section-label | 默认情况下,mdBook 在目录列中,添加章节标签编号。例如,”1.”,”2.1”。将此选项设置为 true 可禁用这些标签.默认为falseplaypen | 用于配置各种 playpen 设置的子表。 search | 用于配置浏览器内搜索功能的子表。mdBook 必须启用search功能编译(默认情况下已启用)。 git-repository-url | 这本书的 git 存储库的 URL。如果提供,将在书的菜单栏中,输出图标链接。 git-repository-icon | 用于 git 存储库链接的 FontAwesome 图标类。默认为fa-github.


[output.html.playpen]可用的配置选项表:描述
editable允许编辑源代码。默认为false
copy-js将编辑器的 JavaScript 文件,复制到输出目录。默认为true.

[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.

这显示了book.toml所有可用的 HTML 输出选项:

[book]
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."

[output.html]
theme = "my-theme"
default-theme = "light"
curly-quotes = true
mathjax-support = false
google-analytics = "123456"
additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"]
no-section-label = false
git-repository-url = "https://github.com/rust-lang-nursery/mdBook"
git-repository-icon = "fa-github"

[output.html.playpen]
editable = false
copy-js = true

[output.html.search]
enable = true
limit-results = 30
teaser-word-count = 30
use-boolean-and = true
boost-title = 2
boost-hierarchy = 1
boost-paragraph = 1
expand = true
heading-split-level = 3
copy-js = true

Markdown 渲染器

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

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

[output.markdown]

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

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

自定义 Renderers

可以通过添加一个[output.foo]表到你的book.toml,来启用自定义渲染器。与preprocessors相似,指示mdbook将书的表达传递给mdbook-foo渲染.

自定义渲染器可以访问其表中的所有配置(即,在其[output.foo]下的任何内容),并且可以使用command字段手动指定要调用的命令.

环境 变量

通过设置相应的环境变量,命令行的运行,覆盖到所有配置值。因为许多操作系统将环境变量限制为_字母数字字符,配置字段需要格式化成,正常情况的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模板.
book.css是输出中使用的样式。如果要更改图书的设计,可能是您要修改的文件。有时与index.hbs一起,当你想从根本上改变布局.
book.js主要用于添加客户端功能,如隐藏/取消隐藏侧边栏,更改主题,...
highlight.js是用于突出显示代码片段的JavaScript,您不需要修改它.
highlight.css是用于代码突出显示的主题
favicon.png将使用的favicon

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

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

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 |> 本章的标题,如SUMMARY.md下所列
  • 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

语法高亮

对于我使用的语法高亮Highlight.js自定义主题.

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

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

自定义主题

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

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

如果你想使用highlight.js另一个主题,可从他们的网站下载,或自己制作,重命名为highlight.css,并把它放进去src/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解释你的想法,我会看看它.

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

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

编辑器

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

[output.html.playpen]
editable = true

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

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

以上将导致这个可编辑的围栏:

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

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

定制编辑器

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

[output.html.playpen]
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\ xi]

你会写:

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

mdBook 特有 markdown

隐藏代码行数

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

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

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

渲染为

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

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

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,noplaypen
{{#include file.rs:component}}
```

Here is a system:
```rust,no_run,noplaypen
{{#include file.rs:system}}
```

This is the full file.
```rust,no_run,noplaypen
{{#include file.rs:all}}
```

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

插入可运行的 Rust 文件

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

{{#playpen 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");
}

试试 点击 播放箭头

要可编辑,请添加

{{#playpen 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");
}

在持续集成中运行 mdbook

虽然以下示例中使用在Travis CI,但原则上应该,也可以直接转移到其他持续集成提供商.

确保mdbook的构建与测试成功

以下是Travis CI的.travis.yml示例,确保配置了mdbook buildmdbook test运行成功。加快CI运转时间的关键是缓存mdbook的安装,以便您可以不用每次CI运行就编译一次mdbook

language: rust
sudo: false

cache:
  - cargo

rust:
  - stable

before_script:
  - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
  - (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.3" mdbook)
  - cargo install-update -a

script:
  - mdbook build path/to/mybook && mdbook test path/to/mybook

发布你的图书到 GitHub Pages

遵循这些命令的结果是,在您的存储库运行CI成功后,您的图书将发布到GitHub页面的master分支.

首先,创建一个新的GitHub的”Personal Access Token”,保证”public_repo”权限(或私有存储库的”repo”)。转到Travis CI网站的该库设置页面,并添加一个名为GITHUB_TOKEN的环境变量,该变量能保证安全和显示在日志中.

然后,将此代码段附加到您的.travis.yml,并更新为book目录路径:

book目录是mdbook build的默认输出目录,请根据你的构建目录填写

deploy:
  provider: pages
  skip-cleanup: true
  github-token: $GITHUB_TOKEN
  local-dir: path/to/mybook/book
  keep-history: false
  on:
    branch: master

就这样!

手动发布你的图书到 Github Pages

如果你的 CI 并不支持 GitHub pages, 或是与其他平台进行整合: 注意: 不要求一定是 tmp 目录:

$> git worktree add /tmp/book gh-pages
$> mdbook build
$> rm -rf /tmp/book/* # this won't delete the .git directory
$> cp -rp book/* /tmp/book/
$> cd /tmp/book
$> git add -A
$> git commit 'new book message'
$> git push origin gh-pages
$> cd -

或是放入 Makefile 文件:

.PHONY: deploy
deploy: book
	@echo "====> deploying to github"
	git worktree add /tmp/book gh-pages
	rm -rf /tmp/book/*
	cp -rp book/* /tmp/book/
	cd /tmp/book && \
		git add -A && \
		git commit -m "deployed on $(shell date) by ${USER}" && \
		git push origin gh-pages

致 Developers

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

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

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

构建过程

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

  1. 加载书
    • 解析book.toml。 如果其中不存在,使用Config默认值。
    • 将书籍章节加载到内存中
    • 了解应该使用哪些预处理器/后端
  2. 运行预处理器
  3. 依次运行每个后端

mdbook作为库使用

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

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

Preprocessors

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

  • 创建自定义帮助程序{{#include /path/to/file.md}}
  • 更新链接[some chapter](some_chapter.md)自动更改为[some chapter](some_chapter.html),这是 HTML 渲染器功能
  • 用 latex 样式($$ \frac{1}{3} $$)的表达式代替为 mathjax 的等价物

勾住 MDBook

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

虽然预处理器可以进行硬编码,以指定应该运行哪个后端,来处理如preprocessor.foo.renderer的字段(但奇奇怪怪的是,像 MathJax 用于非 HTML 渲染器没有意义).

[book]
title = "My Book"
authors = ["Michael-F-Bryan"]

[preprocessor.foo]
# 指定命令的使用
command = "python3 /path/to/foo.py"
#  `foo` 预处理器 只被用于 HTML 和 EPUB 的渲染器
renderer = ["html", "epub"]

在典型的 unix 样式中,插件的所有输入都被写入stdin作为 JSON,和mdbook将从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_variables)]
#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))
    })
}
#}

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

备用后端

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

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

此页面将引导您,创建自己的单词计数程序的简单形式的备用后端。虽然它将用 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_variables)]
#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

以下列出了帮助改进mdBook的贡献者。为他们欢呼!

如果您觉得自己在此列表中遗漏了,请随意添加PR.