2019-05-20 04:16:10 +08:00
|
|
|
# Alternative Backends
|
2018-01-21 22:35:11 +08:00
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
A "backend" is simply a program which `mdbook` will invoke during the book
|
2018-01-21 22:35:11 +08:00
|
|
|
rendering process. This program is passed a JSON representation of the book and
|
2018-08-03 10:34:26 +08:00
|
|
|
configuration information via `stdin`. Once the backend receives this
|
2018-01-21 22:35:11 +08:00
|
|
|
information it is free to do whatever it wants.
|
|
|
|
|
2019-05-19 05:38:08 +08:00
|
|
|
There are already several alternative backends on GitHub which can be used as a
|
2018-01-21 22:35:11 +08:00
|
|
|
rough example of how this is accomplished in practice.
|
|
|
|
|
|
|
|
- [mdbook-linkcheck] - a simple program for verifying the book doesn't contain
|
|
|
|
any broken links
|
|
|
|
- [mdbook-epub] - an EPUB renderer
|
|
|
|
- [mdbook-test] - a program to run the book's contents through [rust-skeptic] to
|
|
|
|
verify everything compiles and runs correctly (similar to `rustdoc --test`)
|
2021-08-26 23:45:22 +08:00
|
|
|
- [mdbook-man] - generate manual pages from the book
|
2018-01-21 22:35:11 +08:00
|
|
|
|
2019-05-20 04:16:10 +08:00
|
|
|
This page will step you through creating your own alternative backend in the form
|
2018-01-21 22:35:11 +08:00
|
|
|
of a simple word counting program. Although it will be written in Rust, there's
|
|
|
|
no reason why it couldn't be accomplished using something like Python or Ruby.
|
|
|
|
|
|
|
|
|
|
|
|
## Setting Up
|
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
First you'll want to create a new binary program and add `mdbook` as a
|
2018-01-21 22:35:11 +08:00
|
|
|
dependency.
|
|
|
|
|
2019-05-19 06:05:57 +08:00
|
|
|
```shell
|
2018-01-21 22:35:11 +08:00
|
|
|
$ cargo new --bin mdbook-wordcount
|
2019-05-19 06:05:57 +08:00
|
|
|
$ cd mdbook-wordcount
|
2018-01-21 22:35:11 +08:00
|
|
|
$ cargo add mdbook
|
|
|
|
```
|
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
When our `mdbook-wordcount` plugin is invoked, `mdbook` will send it a JSON
|
|
|
|
version of [`RenderContext`] via our plugin's `stdin`. For convenience, there's
|
2018-01-21 22:35:11 +08:00
|
|
|
a [`RenderContext::from_json()`] constructor which will load a `RenderContext`.
|
|
|
|
|
|
|
|
This is all the boilerplate necessary for our backend to load the book.
|
|
|
|
|
|
|
|
```rust
|
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
> **Note:** The `RenderContext` contains a `version` field. This lets backends
|
2018-01-21 22:35:11 +08:00
|
|
|
figure out whether they are compatible with the version of `mdbook` it's being
|
2018-08-03 10:34:26 +08:00
|
|
|
called by. This `version` comes directly from the corresponding field in
|
2019-05-19 06:05:57 +08:00
|
|
|
`mdbook`'s `Cargo.toml`.
|
|
|
|
|
2018-01-21 22:35:11 +08:00
|
|
|
It is recommended that backends use the [`semver`] crate to inspect this field
|
|
|
|
and emit a warning if there may be a compatibility issue.
|
|
|
|
|
|
|
|
|
|
|
|
## Inspecting the Book
|
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
Now our backend has a copy of the book, lets count how many words are in each
|
2018-01-21 22:35:11 +08:00
|
|
|
chapter!
|
|
|
|
|
|
|
|
Because the `RenderContext` contains a [`Book`] field (`book`), and a `Book` has
|
|
|
|
the [`Book::iter()`] method for iterating over all items in a `Book`, this step
|
|
|
|
turns out to be just as easy as the first.
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Enabling the Backend
|
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
Now we've got the basics running, we want to actually use it. First, install the
|
|
|
|
program.
|
2018-01-21 22:35:11 +08:00
|
|
|
|
2019-05-19 06:05:57 +08:00
|
|
|
```shell
|
2019-08-01 05:49:25 +08:00
|
|
|
$ cargo install --path .
|
2018-01-21 22:35:11 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
Then `cd` to the particular book you'd like to count the words of and update its
|
2019-05-19 06:05:57 +08:00
|
|
|
`book.toml` file.
|
2018-01-21 22:35:11 +08:00
|
|
|
|
|
|
|
```diff
|
|
|
|
[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]
|
|
|
|
```
|
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
When it loads a book into memory, `mdbook` will inspect your `book.toml` file to
|
|
|
|
try and figure out which backends to use by looking for all `output.*` tables.
|
2019-05-19 06:05:57 +08:00
|
|
|
If none are provided it'll fall back to using the default HTML renderer.
|
2018-01-21 22:35:11 +08:00
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
Notably, this means if you want to add your own custom backend you'll also need
|
|
|
|
to make sure to add the HTML backend, even if its table just stays empty.
|
2018-01-21 22:35:11 +08:00
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
Now you just need to build your book like normal, and everything should *Just
|
2018-01-21 22:35:11 +08:00
|
|
|
Work*.
|
|
|
|
|
2019-05-19 06:05:57 +08:00
|
|
|
```shell
|
2018-01-21 22:35:11 +08:00
|
|
|
$ 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
|
2019-05-20 04:16:10 +08:00
|
|
|
Alternative Backends: 710
|
2018-01-21 22:35:11 +08:00
|
|
|
Contributors: 85
|
|
|
|
```
|
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
The reason we didn't need to specify the full name/path of our `wordcount`
|
|
|
|
backend is because `mdbook` will try to *infer* the program's name via
|
|
|
|
convention. The executable for the `foo` backend is typically called
|
2018-01-21 22:35:11 +08:00
|
|
|
`mdbook-foo`, with an associated `[output.foo]` entry in the `book.toml`. To
|
2018-08-03 10:34:26 +08:00
|
|
|
explicitly tell `mdbook` what command to invoke (it may require command-line
|
2018-01-21 22:35:11 +08:00
|
|
|
arguments or be an interpreted script), you can use the `command` field.
|
|
|
|
|
|
|
|
```diff
|
|
|
|
[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"
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Configuration
|
|
|
|
|
|
|
|
Now imagine you don't want to count the number of words on a particular chapter
|
2018-08-03 10:34:26 +08:00
|
|
|
(it might be generated text/code, etc). The canonical way to do this is via the
|
|
|
|
usual `book.toml` configuration file by adding items to your `[output.foo]`
|
2019-05-19 06:05:57 +08:00
|
|
|
table.
|
2018-01-21 22:35:11 +08:00
|
|
|
|
|
|
|
The `Config` can be treated roughly as a nested hashmap which lets you call
|
|
|
|
methods like `get()` to access the config's contents, with a
|
2018-08-03 10:34:26 +08:00
|
|
|
`get_deserialized()` convenience method for retrieving a value and automatically
|
|
|
|
deserializing to some arbitrary type `T`.
|
2018-01-21 22:35:11 +08:00
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
To implement this, we'll create our own serializable `WordcountConfig` struct
|
2018-01-21 22:35:11 +08:00
|
|
|
which will encapsulate all configuration for this backend.
|
|
|
|
|
|
|
|
First add `serde` and `serde_derive` to your `Cargo.toml`,
|
|
|
|
|
|
|
|
```
|
|
|
|
$ cargo add serde serde_derive
|
|
|
|
```
|
|
|
|
|
|
|
|
And then you can create the config struct,
|
|
|
|
|
|
|
|
```rust
|
|
|
|
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>,
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Now we just need to deserialize the `WordcountConfig` from our `RenderContext`
|
|
|
|
and then add a check to make sure we skip ignored chapters.
|
|
|
|
|
|
|
|
```diff
|
|
|
|
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();
|
2019-05-19 06:05:57 +08:00
|
|
|
|
2018-01-21 22:35:11 +08:00
|
|
|
for item in ctx.book.iter() {
|
|
|
|
if let BookItem::Chapter(ref ch) = *item {
|
|
|
|
+ if cfg.ignores.contains(&ch.name) {
|
|
|
|
+ continue;
|
|
|
|
+ }
|
2019-05-19 06:05:57 +08:00
|
|
|
+
|
2018-01-21 22:35:11 +08:00
|
|
|
let num_words = count_words(ch);
|
|
|
|
println!("{}: {}", ch.name, num_words);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Output and Signalling Failure
|
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
While it's nice to print word counts to the terminal when a book is built, it
|
2018-01-21 22:35:11 +08:00
|
|
|
might also be a good idea to output them to a file somewhere. `mdbook` tells a
|
|
|
|
backend where it should place any generated output via the `destination` field
|
|
|
|
in [`RenderContext`].
|
|
|
|
|
|
|
|
```diff
|
|
|
|
+ use std::fs::{self, File};
|
|
|
|
+ use std::io::{self, Write};
|
|
|
|
- use std::io;
|
|
|
|
use mdbook::renderer::RenderContext;
|
|
|
|
use mdbook::book::{BookItem, Chapter};
|
2019-05-19 06:05:57 +08:00
|
|
|
|
2018-01-21 22:35:11 +08:00
|
|
|
fn main() {
|
|
|
|
...
|
2019-05-19 06:05:57 +08:00
|
|
|
|
2018-01-21 22:35:11 +08:00
|
|
|
+ let _ = fs::create_dir_all(&ctx.destination);
|
|
|
|
+ let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
|
2019-05-19 06:05:57 +08:00
|
|
|
+
|
2018-01-21 22:35:11 +08:00
|
|
|
for item in ctx.book.iter() {
|
|
|
|
if let BookItem::Chapter(ref ch) = *item {
|
|
|
|
...
|
2019-05-19 06:05:57 +08:00
|
|
|
|
2018-01-21 22:35:11 +08:00
|
|
|
let num_words = count_words(ch);
|
|
|
|
println!("{}: {}", ch.name, num_words);
|
|
|
|
+ writeln!(f, "{}: {}", ch.name, num_words).unwrap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
> **Note:** There is no guarantee that the destination directory exists or is
|
|
|
|
> empty (`mdbook` may leave the previous contents to let backends do caching),
|
|
|
|
> so it's always a good idea to create it with `fs::create_dir_all()`.
|
2019-07-21 02:40:58 +08:00
|
|
|
>
|
|
|
|
> If the destination directory already exists, don't assume it will be empty.
|
|
|
|
> To allow backends to cache the results from previous runs, `mdbook` may leave
|
|
|
|
> old content in the directory.
|
2018-01-21 22:35:11 +08:00
|
|
|
|
|
|
|
There's always the possibility that an error will occur while processing a book
|
2018-08-03 10:34:26 +08:00
|
|
|
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
|
2018-01-21 22:35:11 +08:00
|
|
|
interpret a non-zero exit code as a rendering failure.
|
|
|
|
|
|
|
|
For example, if we wanted to make sure all chapters have an *even* number of
|
|
|
|
words, erroring out if an odd number is encountered, then you may do something
|
|
|
|
like this:
|
|
|
|
|
|
|
|
```diff
|
|
|
|
+ use std::process;
|
|
|
|
...
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
...
|
2019-05-19 06:05:57 +08:00
|
|
|
|
2018-01-21 22:35:11 +08:00
|
|
|
for item in ctx.book.iter() {
|
|
|
|
if let BookItem::Chapter(ref ch) = *item {
|
|
|
|
...
|
2019-05-19 06:05:57 +08:00
|
|
|
|
2018-01-21 22:35:11 +08:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Now, if we reinstall the backend and build a book,
|
|
|
|
|
2019-05-19 06:05:57 +08:00
|
|
|
```shell
|
2019-08-01 05:49:25 +08:00
|
|
|
$ cargo install --path . --force
|
2018-01-21 22:35:11 +08:00
|
|
|
$ 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-08-03 10:34:26 +08:00
|
|
|
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Caused By: The "mdbook-wordcount" renderer failed
|
2018-01-21 22:35:11 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
As you've probably already noticed, output from the plugin's subprocess is
|
2018-08-03 10:34:26 +08:00
|
|
|
immediately passed through to the user. It is encouraged for plugins to follow
|
|
|
|
the "rule of silence" and only generate output when necessary (e.g. an error in
|
|
|
|
generation or a warning).
|
2018-01-21 22:35:11 +08:00
|
|
|
|
2018-08-03 10:34:26 +08:00
|
|
|
All environment variables are passed through to the backend, allowing you to use
|
|
|
|
the usual `RUST_LOG` to control logging verbosity.
|
2018-01-21 22:35:11 +08:00
|
|
|
|
2019-12-31 18:16:59 +08:00
|
|
|
## Handling missing backends
|
|
|
|
|
2020-04-22 06:34:59 +08:00
|
|
|
If you enable a backend that isn't installed, the default behavior is to throw an error:
|
2019-12-31 18:16:59 +08:00
|
|
|
|
|
|
|
```text
|
2020-07-25 10:18:22 +08:00
|
|
|
The command `mdbook-wordcount` wasn't found, is the "wordcount" backend installed?
|
|
|
|
If you want to ignore this error when the "wordcount" backend is not installed,
|
|
|
|
set `optional = true` in the `[output.wordcount]` section of the book.toml configuration file.
|
2019-12-31 18:16:59 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
This behavior can be changed by marking the backend as optional.
|
|
|
|
|
|
|
|
```diff
|
|
|
|
[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"
|
|
|
|
+ optional = true
|
|
|
|
```
|
|
|
|
|
|
|
|
This demotes the error to a warning, and it will instead look like this:
|
|
|
|
|
|
|
|
```text
|
|
|
|
The command was not found, but was marked as optional.
|
|
|
|
Command: wordcount
|
|
|
|
```
|
|
|
|
|
2018-01-21 22:35:11 +08:00
|
|
|
|
|
|
|
## Wrapping Up
|
|
|
|
|
|
|
|
Although contrived, hopefully this example was enough to show how you'd create
|
2019-05-20 04:16:10 +08:00
|
|
|
an alternative backend for `mdbook`. If you feel it's missing something, don't
|
2018-01-21 22:35:11 +08:00
|
|
|
hesitate to create an issue in the [issue tracker] so we can improve the user
|
|
|
|
guide.
|
|
|
|
|
|
|
|
The existing backends mentioned towards the start of this chapter should serve
|
2018-08-03 10:34:26 +08:00
|
|
|
as a good example of how it's done in real life, so feel free to skim through
|
2018-01-21 22:35:11 +08:00
|
|
|
the source code or ask questions.
|
|
|
|
|
|
|
|
|
|
|
|
[mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck
|
|
|
|
[mdbook-epub]: https://github.com/Michael-F-Bryan/mdbook-epub
|
|
|
|
[mdbook-test]: https://github.com/Michael-F-Bryan/mdbook-test
|
2021-08-26 23:45:22 +08:00
|
|
|
[mdbook-man]: https://github.com/vv9k/mdbook-man
|
2018-01-21 22:35:11 +08:00
|
|
|
[rust-skeptic]: https://github.com/budziq/rust-skeptic
|
2019-07-16 03:51:46 +08:00
|
|
|
[`RenderContext`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
|
|
|
|
[`RenderContext::from_json()`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json
|
2018-01-21 22:35:11 +08:00
|
|
|
[`semver`]: https://crates.io/crates/semver
|
2019-07-16 03:51:46 +08:00
|
|
|
[`Book`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html
|
|
|
|
[`Book::iter()`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html#method.iter
|
|
|
|
[`Config`]: https://docs.rs/mdbook/*/mdbook/config/struct.Config.html
|
2019-10-29 21:04:16 +08:00
|
|
|
[issue tracker]: https://github.com/rust-lang/mdBook/issues
|