Skip to content

Commit fd7e8d1

Browse files
Add support for alternative backends (#507)
* Added a mechanism for creating alternate backends * Added a CmdRenderer and the ability to have multiple renderers * Made MDBook::load() autodetect renderers * Added a couple methods to RenderContext * Converted RenderContext.version to a String * Made sure all alternate renderers are invoked as `mdbook-*` * Factored out the logic for determining which renderer to use * Added tests for renderer detection * Made it so `mdbook test` works on the book-example again * Updated the "For Developers" docs * Removed `[output.epub]` from the example book's book.toml * Added a bit more info on how backends should work * Added a `destination` key to the RenderContext * Altered how we wait for an alternate backend to finish * Refactored the Renderer trait to not use MDBook and moved livereload to the template * Moved info for developers out of the book.toml format chapter * MOAR docs * MDBook::build() no longer takes &mut self * Replaced a bunch of println!()'s with proper log macros * Cleaned up the build() method and backend discovery * Added a couple notes and doc-comments * Found a race condition when backends exit really quickly * Added support for backends with arguments * Fixed a funny doc-comment
1 parent dedc208 commit fd7e8d1

File tree

20 files changed

+848
-297
lines changed

20 files changed

+848
-297
lines changed

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ open = "1.1"
3232
regex = "0.2.1"
3333
tempdir = "0.3.4"
3434
itertools = "0.7.4"
35+
tempfile = "2.2.0"
36+
shlex = "0.1.1"
3537

3638
# Watch feature
3739
notify = { version = "4.0", optional = true }

book-example/src/SUMMARY.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
- [Syntax highlighting](format/theme/syntax-highlighting.md)
1616
- [MathJax Support](format/mathjax.md)
1717
- [Rust code specific features](format/rust.md)
18-
- [Rust Library](lib/lib.md)
18+
- [For Developers](lib/index.md)
1919
-----------
2020
[Contributors](misc/contributors.md)

book-example/src/format/config.md

-50
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ renderer need to be specified under the TOML table `[output.html]`.
6969

7070
The following configuration options are available:
7171

72-
pub playpen: Playpen,
73-
7472
- **theme:** mdBook comes with a default theme and all the resource files
7573
needed for it. But if this option is set, mdBook will selectively overwrite
7674
the theme files with the ones found in the specified folder.
@@ -105,51 +103,3 @@ additional-js = ["custom.js"]
105103
editor = "./path/to/editor"
106104
editable = false
107105
```
108-
109-
110-
## For Developers
111-
112-
If you are developing a plugin or alternate backend then whenever your code is
113-
called you will almost certainly be passed a reference to the book's `Config`.
114-
This can be treated roughly as a nested hashmap which lets you call methods like
115-
`get()` and `get_mut()` to get access to the config's contents.
116-
117-
By convention, plugin developers will have their settings as a subtable inside
118-
`plugins` (e.g. a link checker would put its settings in `plugins.link_check`)
119-
and backends should put their configuration under `output`, like the HTML
120-
renderer does in the previous examples.
121-
122-
As an example, some hypothetical `random` renderer would typically want to load
123-
its settings from the `Config` at the very start of its rendering process. The
124-
author can take advantage of serde to deserialize the generic `toml::Value`
125-
object retrieved from `Config` into a struct specific to its use case.
126-
127-
```rust
128-
#[derive(Debug, Deserialize, PartialEq)]
129-
struct RandomOutput {
130-
foo: u32,
131-
bar: String,
132-
baz: Vec<bool>,
133-
}
134-
135-
let src = r#"
136-
[output.random]
137-
foo = 5
138-
bar = "Hello World"
139-
baz = [true, true, false]
140-
"#;
141-
142-
let book_config = Config::from_str(src)?; // usually passed in by mdbook
143-
let random: Value = book_config.get("output.random").unwrap_or_default();
144-
let got: RandomOutput = random.try_into()?;
145-
146-
assert_eq!(got, should_be);
147-
148-
if let Some(baz) = book_config.get_deserialized::<Vec<bool>>("output.random.baz") {
149-
println!("{:?}", baz); // prints [true, true, false]
150-
151-
// do something interesting with baz
152-
}
153-
154-
// start the rendering process
155-
```

book-example/src/lib/index.md

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# For Developers
2+
3+
While `mdbook` is mainly used as a command line tool, you can also import the
4+
underlying library directly and use that to manage a book.
5+
6+
- Creating custom backends
7+
- Automatically generating and reloading a book on the fly
8+
- Integration with existing projects
9+
10+
The best source for examples on using the `mdbook` crate from your own Rust
11+
programs is the [API Docs].
12+
13+
14+
## Configuration
15+
16+
The mechanism for using alternative backends is very simple, you add an extra
17+
table to your `book.toml` and the `MDBook::load()` function will automatically
18+
detect the backends being used.
19+
20+
For example, if you wanted to use a hypothetical `latex` backend you would add
21+
an empty `output.latex` table to `book.toml`.
22+
23+
```toml
24+
# book.toml
25+
26+
[book]
27+
...
28+
29+
[output.latex]
30+
```
31+
32+
And then during the rendering stage `mdbook` will run the `mdbook-latex`
33+
program, piping it a JSON serialized [RenderContext] via stdin.
34+
35+
You can set the command used via the `command` key.
36+
37+
```toml
38+
# book.toml
39+
40+
[book]
41+
...
42+
43+
[output.latex]
44+
command = "python3 my_plugin.py"
45+
```
46+
47+
If no backend is supplied (i.e. there are no `output.*` tables), `mdbook` will
48+
fall back to the `html` backend.
49+
50+
### The `Config` Struct
51+
52+
If you are developing a plugin or alternate backend then whenever your code is
53+
called you will almost certainly be passed a reference to the book's `Config`.
54+
This can be treated roughly as a nested hashmap which lets you call methods like
55+
`get()` and `get_mut()` to get access to the config's contents.
56+
57+
By convention, plugin developers will have their settings as a subtable inside
58+
`plugins` (e.g. a link checker would put its settings in `plugins.link_check`)
59+
and backends should put their configuration under `output`, like the HTML
60+
renderer does in the previous examples.
61+
62+
As an example, some hypothetical `random` renderer would typically want to load
63+
its settings from the `Config` at the very start of its rendering process. The
64+
author can take advantage of serde to deserialize the generic `toml::Value`
65+
object retrieved from `Config` into a struct specific to its use case.
66+
67+
```rust
68+
extern crate serde;
69+
#[macro_use]
70+
extern crate serde_derive;
71+
extern crate toml;
72+
extern crate mdbook;
73+
74+
use toml::Value;
75+
use mdbook::config::Config;
76+
77+
#[derive(Debug, Deserialize, PartialEq)]
78+
struct RandomOutput {
79+
foo: u32,
80+
bar: String,
81+
baz: Vec<bool>,
82+
}
83+
84+
# fn run() -> Result<(), Box<::std::error::Error>> {
85+
let src = r#"
86+
[output.random]
87+
foo = 5
88+
bar = "Hello World"
89+
baz = [true, true, false]
90+
"#;
91+
92+
let book_config = Config::from_str(src)?; // usually passed in via the RenderContext
93+
let random = book_config.get("output.random")
94+
.cloned()
95+
.ok_or("output.random not found")?;
96+
let got: RandomOutput = random.try_into()?;
97+
98+
let should_be = RandomOutput {
99+
foo: 5,
100+
bar: "Hello World".to_string(),
101+
baz: vec![true, true, false]
102+
};
103+
104+
assert_eq!(got, should_be);
105+
106+
let baz: Vec<bool> = book_config.get_deserialized("output.random.baz")?;
107+
println!("{:?}", baz); // prints [true, true, false]
108+
109+
// do something interesting with baz
110+
# Ok(())
111+
# }
112+
# fn main() { run().unwrap() }
113+
```
114+
115+
116+
## Render Context
117+
118+
The `RenderContext` encapsulates all the information a backend needs to know
119+
in order to generate output. Its Rust definition looks something like this:
120+
121+
```rust
122+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123+
pub struct RenderContext {
124+
pub version: String,
125+
pub root: PathBuf,
126+
pub book: Book,
127+
pub config: Config,
128+
pub destination: PathBuf,
129+
}
130+
```
131+
132+
A backend will receive the `RenderContext` via `stdin` as one big JSON blob. If
133+
possible, it is recommended to import the `mdbook` crate and use the
134+
`RenderContext::from_json()` method. This way you should always be able to
135+
deserialize the `RenderContext`, and as a bonus will also have access to the
136+
methods already defined on the underlying types.
137+
138+
Although backends are told the book's root directory on disk, it is *strongly
139+
discouraged* to load chapter content from the filesystem. The `root` key is
140+
provided as an escape hatch for certain plugins which may load additional,
141+
non-markdown, files.
142+
143+
144+
## Output Directory
145+
146+
To make things more deterministic, a backend will be told where it should place
147+
its generated artefacts.
148+
149+
The general algorithm for deciding the output directory goes something like
150+
this:
151+
152+
- If there is only one backend:
153+
- `destination` is `config.build.build_dir` (usually `book/`)
154+
- Otherwise:
155+
- `destination` is `config.build.build_dir` joined with the backend's name
156+
(e.g. `build/latex/` for the "latex" backend)
157+
158+
159+
## Output and Signalling Failure
160+
161+
To signal that the plugin failed it just needs to exit with a non-zero return
162+
code.
163+
164+
All output from the plugin's subprocess is immediately passed through to the
165+
user, so it is encouraged for plugins to follow the ["rule of silence"] and
166+
by default only tell the user about things they directly need to respond to
167+
(e.g. an error in generation or a warning).
168+
169+
This "silent by default" behaviour can be overridden via the `RUST_LOG`
170+
environment variable (which `mdbook` will pass through to the backend if set)
171+
as is typical with Rust applications.
172+
173+
174+
[API Docs]: https://docs.rs/mdbook
175+
[RenderContext]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
176+
["rule of silence"]: http://www.linfo.org/rule_of_silence.html

book-example/src/lib/lib.md

-24
This file was deleted.

src/bin/build.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
3030
book.build()?;
3131

3232
if args.is_present("open") {
33-
open(book.get_destination().join("index.html"));
33+
// FIXME: What's the right behaviour if we don't use the HTML renderer?
34+
open(book.build_dir_for("html").join("index.html"));
3435
}
3536

3637
Ok(())

src/bin/init.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
2626
// Skip this if `--force` is present
2727
if !args.is_present("force") {
2828
// Print warning
29-
print!("\nCopying the default theme to {}", builder.config().book.src.display());
29+
println!();
30+
println!(
31+
"Copying the default theme to {}",
32+
builder.config().book.src.display()
33+
);
3034
println!("This could potentially overwrite files already present in that directory.");
3135
print!("\nAre you sure you want to continue? (y/n) ");
3236

src/bin/mdbook.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use clap::{App, AppSettings, ArgMatches};
1616
use chrono::Local;
1717
use log::LevelFilter;
1818
use env_logger::Builder;
19-
use error_chain::ChainedError;
19+
use mdbook::utils;
2020

2121
pub mod build;
2222
pub mod init;
@@ -64,7 +64,7 @@ fn main() {
6464
};
6565

6666
if let Err(e) = res {
67-
eprintln!("{}", e.display_chain());
67+
utils::log_backtrace(&e);
6868

6969
::std::process::exit(101);
7070
}
@@ -101,12 +101,12 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
101101
p.to_path_buf()
102102
}
103103
} else {
104-
env::current_dir().unwrap()
104+
env::current_dir().expect("Unable to determine the current directory")
105105
}
106106
}
107107

108108
fn open<P: AsRef<OsStr>>(path: P) {
109109
if let Err(e) = open::that(path) {
110-
println!("Error opening web browser: {}", e);
110+
error!("Error opening web browser: {}", e);
111111
}
112112
}

0 commit comments

Comments
 (0)