Skip to content

Add support for alternative backends #507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jan 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fdff29e
Added a mechanism for creating alternate backends
Michael-F-Bryan Dec 11, 2017
5e177a9
Added a CmdRenderer and the ability to have multiple renderers
Michael-F-Bryan Dec 12, 2017
fea52b3
Made MDBook::load() autodetect renderers
Michael-F-Bryan Dec 12, 2017
87253e6
Added a couple methods to RenderContext
Michael-F-Bryan Dec 12, 2017
1ae0362
Converted RenderContext.version to a String
Michael-F-Bryan Dec 12, 2017
f606e49
Made sure all alternate renderers are invoked as `mdbook-*`
Michael-F-Bryan Dec 12, 2017
89f80af
Factored out the logic for determining which renderer to use
Michael-F-Bryan Dec 12, 2017
ecccf9f
Added tests for renderer detection
Michael-F-Bryan Dec 12, 2017
0b0ae6e
Made it so `mdbook test` works on the book-example again
Michael-F-Bryan Dec 12, 2017
e6db026
Updated the "For Developers" docs
Michael-F-Bryan Dec 12, 2017
d93e62d
Removed `[output.epub]` from the example book's book.toml
Michael-F-Bryan Dec 12, 2017
80850f3
Added a bit more info on how backends should work
Michael-F-Bryan Dec 22, 2017
07f6c8e
Added a `destination` key to the RenderContext
Michael-F-Bryan Dec 25, 2017
90328c7
Altered how we wait for an alternate backend to finish
Michael-F-Bryan Dec 26, 2017
dbba75e
Refactored the Renderer trait to not use MDBook and moved livereload …
Michael-F-Bryan Jan 3, 2018
d919a48
Moved info for developers out of the book.toml format chapter
Michael-F-Bryan Jan 3, 2018
1018f17
MOAR docs
Michael-F-Bryan Jan 3, 2018
e7693bc
MDBook::build() no longer takes &mut self
Michael-F-Bryan Jan 5, 2018
69e26e6
Replaced a bunch of println!()'s with proper log macros
Michael-F-Bryan Jan 7, 2018
fe6c1c1
Cleaned up the build() method and backend discovery
Michael-F-Bryan Jan 7, 2018
38eecb8
Added a couple notes and doc-comments
Michael-F-Bryan Jan 7, 2018
683f014
Found a race condition when backends exit really quickly
Michael-F-Bryan Jan 7, 2018
5ed8cbf
Added support for backends with arguments
Michael-F-Bryan Jan 7, 2018
263a624
Fixed a funny doc-comment
Michael-F-Bryan Jan 7, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ open = "1.1"
regex = "0.2.1"
tempdir = "0.3.4"
itertools = "0.7.4"
tempfile = "2.2.0"
shlex = "0.1.1"

# Watch feature
notify = { version = "4.0", optional = true }
Expand Down
2 changes: 1 addition & 1 deletion book-example/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
- [Syntax highlighting](format/theme/syntax-highlighting.md)
- [MathJax Support](format/mathjax.md)
- [Rust code specific features](format/rust.md)
- [Rust Library](lib/lib.md)
- [For Developers](lib/index.md)
-----------
[Contributors](misc/contributors.md)
50 changes: 0 additions & 50 deletions book-example/src/format/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ renderer need to be specified under the TOML table `[output.html]`.

The following configuration options are available:

pub playpen: Playpen,

- **theme:** mdBook comes with a default theme and all the resource files
needed for it. But if this option is set, mdBook will selectively overwrite
the theme files with the ones found in the specified folder.
Expand Down Expand Up @@ -105,51 +103,3 @@ additional-js = ["custom.js"]
editor = "./path/to/editor"
editable = false
```


## For Developers

If you are developing a plugin or alternate backend then whenever your code is
called you will almost certainly be passed a reference to the book's `Config`.
This can be treated roughly as a nested hashmap which lets you call methods like
`get()` and `get_mut()` to get access to the config's contents.

By convention, plugin developers will have their settings as a subtable inside
`plugins` (e.g. a link checker would put its settings in `plugins.link_check`)
and backends should put their configuration under `output`, like the HTML
renderer does in the previous examples.

As an example, some hypothetical `random` renderer would typically want to load
its settings from the `Config` at the very start of its rendering process. The
author can take advantage of serde to deserialize the generic `toml::Value`
object retrieved from `Config` into a struct specific to its use case.

```rust
#[derive(Debug, Deserialize, PartialEq)]
struct RandomOutput {
foo: u32,
bar: String,
baz: Vec<bool>,
}

let src = r#"
[output.random]
foo = 5
bar = "Hello World"
baz = [true, true, false]
"#;

let book_config = Config::from_str(src)?; // usually passed in by mdbook
let random: Value = book_config.get("output.random").unwrap_or_default();
let got: RandomOutput = random.try_into()?;

assert_eq!(got, should_be);

if let Some(baz) = book_config.get_deserialized::<Vec<bool>>("output.random.baz") {
println!("{:?}", baz); // prints [true, true, false]

// do something interesting with baz
}

// start the rendering process
```
176 changes: 176 additions & 0 deletions book-example/src/lib/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# For Developers

While `mdbook` is mainly used as a command line tool, you can also import the
underlying library directly and use that to manage a book.

- Creating custom backends
- Automatically generating and reloading a book on the fly
- Integration with existing projects

The best source for examples on using the `mdbook` crate from your own Rust
programs is the [API Docs].


## Configuration

The mechanism for using alternative backends is very simple, you add an extra
table to your `book.toml` and the `MDBook::load()` function will automatically
detect the backends being used.

For example, if you wanted to use a hypothetical `latex` backend you would add
an empty `output.latex` table to `book.toml`.

```toml
# book.toml

[book]
...

[output.latex]
```

And then during the rendering stage `mdbook` will run the `mdbook-latex`
program, piping it a JSON serialized [RenderContext] via stdin.

You can set the command used via the `command` key.

```toml
# book.toml

[book]
...

[output.latex]
command = "python3 my_plugin.py"
```

If no backend is supplied (i.e. there are no `output.*` tables), `mdbook` will
fall back to the `html` backend.

### The `Config` Struct

If you are developing a plugin or alternate backend then whenever your code is
called you will almost certainly be passed a reference to the book's `Config`.
This can be treated roughly as a nested hashmap which lets you call methods like
`get()` and `get_mut()` to get access to the config's contents.

By convention, plugin developers will have their settings as a subtable inside
`plugins` (e.g. a link checker would put its settings in `plugins.link_check`)
and backends should put their configuration under `output`, like the HTML
renderer does in the previous examples.

As an example, some hypothetical `random` renderer would typically want to load
its settings from the `Config` at the very start of its rendering process. The
author can take advantage of serde to deserialize the generic `toml::Value`
object retrieved from `Config` into a struct specific to its use case.

```rust
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate toml;
extern crate mdbook;

use toml::Value;
use mdbook::config::Config;

#[derive(Debug, Deserialize, PartialEq)]
struct RandomOutput {
foo: u32,
bar: String,
baz: Vec<bool>,
}

# fn run() -> Result<(), Box<::std::error::Error>> {
let src = r#"
[output.random]
foo = 5
bar = "Hello World"
baz = [true, true, false]
"#;

let book_config = Config::from_str(src)?; // usually passed in via the RenderContext
let random = book_config.get("output.random")
.cloned()
.ok_or("output.random not found")?;
let got: RandomOutput = random.try_into()?;

let should_be = RandomOutput {
foo: 5,
bar: "Hello World".to_string(),
baz: vec![true, true, false]
};

assert_eq!(got, should_be);

let baz: Vec<bool> = book_config.get_deserialized("output.random.baz")?;
println!("{:?}", baz); // prints [true, true, false]

// do something interesting with baz
# Ok(())
# }
# fn main() { run().unwrap() }
```


## Render Context

The `RenderContext` encapsulates all the information a backend needs to know
in order to generate output. Its Rust definition looks something like this:

```rust
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RenderContext {
pub version: String,
pub root: PathBuf,
pub book: Book,
pub config: Config,
pub destination: PathBuf,
}
```

A backend will receive the `RenderContext` via `stdin` as one big JSON blob. If
possible, it is recommended to import the `mdbook` crate and use the
`RenderContext::from_json()` method. This way you should always be able to
deserialize the `RenderContext`, and as a bonus will also have access to the
methods already defined on the underlying types.

Although backends are told the book's root directory on disk, it is *strongly
discouraged* to load chapter content from the filesystem. The `root` key is
provided as an escape hatch for certain plugins which may load additional,
non-markdown, files.


## Output Directory

To make things more deterministic, a backend will be told where it should place
its generated artefacts.

The general algorithm for deciding the output directory goes something like
this:

- If there is only one backend:
- `destination` is `config.build.build_dir` (usually `book/`)
- Otherwise:
- `destination` is `config.build.build_dir` joined with the backend's name
(e.g. `build/latex/` for the "latex" backend)


## Output and Signalling Failure

To signal that the plugin failed it just needs to exit with a non-zero return
code.

All output from the plugin's subprocess is immediately passed through to the
user, so it is encouraged for plugins to follow the ["rule of silence"] and
by default only tell the user about things they directly need to respond to
(e.g. an error in generation or a warning).

This "silent by default" behaviour can be overridden via the `RUST_LOG`
environment variable (which `mdbook` will pass through to the backend if set)
as is typical with Rust applications.


[API Docs]: https://docs.rs/mdbook
[RenderContext]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
["rule of silence"]: http://www.linfo.org/rule_of_silence.html
24 changes: 0 additions & 24 deletions book-example/src/lib/lib.md

This file was deleted.

3 changes: 2 additions & 1 deletion src/bin/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
book.build()?;

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

Ok(())
Expand Down
6 changes: 5 additions & 1 deletion src/bin/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
// Skip this if `--force` is present
if !args.is_present("force") {
// Print warning
print!("\nCopying the default theme to {}", builder.config().book.src.display());
println!();
println!(
"Copying the default theme to {}",
builder.config().book.src.display()
);
println!("This could potentially overwrite files already present in that directory.");
print!("\nAre you sure you want to continue? (y/n) ");

Expand Down
8 changes: 4 additions & 4 deletions src/bin/mdbook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use clap::{App, AppSettings, ArgMatches};
use chrono::Local;
use log::LevelFilter;
use env_logger::Builder;
use error_chain::ChainedError;
use mdbook::utils;

pub mod build;
pub mod init;
Expand Down Expand Up @@ -64,7 +64,7 @@ fn main() {
};

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

::std::process::exit(101);
}
Expand Down Expand Up @@ -101,12 +101,12 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
p.to_path_buf()
}
} else {
env::current_dir().unwrap()
env::current_dir().expect("Unable to determine the current directory")
}
}

fn open<P: AsRef<OsStr>>(path: P) {
if let Err(e) = open::that(path) {
println!("Error opening web browser: {}", e);
error!("Error opening web browser: {}", e);
}
}
Loading