Increase Documentation Coverage (#543)

* Added documentation to the `config` module

* Added an example to the `config` module

* Updated the docs in lib.rs regarding implementing backends

* Started writing an alternate backends walkthrough

* Mentioned the output.foo.command key

* Added example output

* Added a config section to the backends tutorial

* Finished off the backends tutorial

* Made sure travis checks mdbook-wordcount

* Fixed the broken link at in the user guide

* Changed how travis builds the project

* Added a conclusion

* Went through and documented a lot of stuff

* Added a preprocessors chapter and updated For Developers
This commit is contained in:
Michael Bryan 2018-01-21 22:35:11 +08:00 committed by GitHub
parent 232a923676
commit 9fe19d8f31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 624 additions and 240 deletions

View File

@ -20,19 +20,19 @@ chrono = "0.4"
handlebars = "0.29" handlebars = "0.29"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
error-chain = "0.11.0" error-chain = "0.11"
serde_json = "1.0" serde_json = "1.0"
pulldown-cmark = "0.1" pulldown-cmark = "0.1"
lazy_static = "1.0" lazy_static = "1.0"
log = "0.4" log = "0.4"
env_logger = "0.5.0-rc.1" env_logger = "0.5.0-rc.1"
toml = "0.4" toml = "0.4"
memchr = "2.0.1" memchr = "2.0"
open = "1.1" open = "1.1"
regex = "0.2.1" regex = "0.2.1"
tempdir = "0.3.4" tempdir = "0.3.4"
itertools = "0.7.4" itertools = "0.7"
shlex = "0.1.1" shlex = "0.1"
toml-query = "0.6" toml-query = "0.6"
# Watch feature # Watch feature
@ -66,3 +66,5 @@ doc = false
name = "mdbook" name = "mdbook"
path = "src/bin/mdbook.rs" path = "src/bin/mdbook.rs"
[workspace]
members = ["book-example/src/for_developers/mdbook-wordcount"]

View File

@ -1,7 +1,7 @@
[book] [book]
title = "mdBook Documentation" title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust" description = "Create book from markdown files. Like Gitbook but implemented in Rust"
author = "Mathieu David" authors = ["Mathieu David", "Michael-F-Bryan"]
[output.html] [output.html]
mathjax-support = true mathjax-support = true

View File

@ -16,6 +16,8 @@
- [Editor](format/theme/editor.md) - [Editor](format/theme/editor.md)
- [MathJax Support](format/mathjax.md) - [MathJax Support](format/mathjax.md)
- [Rust code specific features](format/rust.md) - [Rust code specific features](format/rust.md)
- [For Developers](lib/index.md) - [For Developers](for_developers/index.md)
- [Preprocessors](for_developers/preprocessors.md)
- [Alternate Backends](for_developers/backends.md)
----------- -----------
[Contributors](misc/contributors.md) [Contributors](misc/contributors.md)

View File

@ -0,0 +1,352 @@
# Alternate Backends
A "backend" is simply a program which `mdbook` will invoke during the book
rendering process. This program is passed a JSON representation of the book and
configuration information via `stdin`. Once the backend receives this
information it is free to do whatever it wants.
There are already several alternate backends on GitHub which can be used as a
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`)
This page will step you through creating your own alternate backend in the form
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
First you'll want to create a new binary program and add `mdbook` as a
dependency.
```
$ cargo new --bin mdbook-wordcount
$ cd mdbook-wordcount
$ cargo add mdbook
```
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
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();
}
```
> **Note:** The `RenderContext` contains a `version` field. This lets backends
figure out whether they are compatible with the version of `mdbook` it's being
called by. This `version` comes directly from the corresponding field in
`mdbook`'s `Cargo.toml`.
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
Now our backend has a copy of the book, lets count how many words are in each
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
Now we've got the basics running, we want to actually use it. First, install
the program.
```
$ cargo install
```
Then `cd` to the particular book you'd like to count the words of and update its
`book.toml` file.
```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]
```
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. If none are provided it'll fall back to using the default HTML
renderer.
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 tabke just stays empty.
Now you just need to build your book like normal, and everything should *Just
Work*.
```
$ 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
Alternate Backends: 710
Contributors: 85
```
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
`mdbook-foo`, with an associated `[output.foo]` entry in the `book.toml`. To
explicitly tell `mdbook` what command to invoke (it may require command line
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
(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]`
table.
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
`get_deserialized()` convenience method for retrieving a value and
automatically deserializing to some arbitrary type `T`.
To implement this, we'll create our own serializable `WordcountConfig` struct
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();
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);
}
}
}
```
## Output and Signalling Failure
While it's nice to print word counts to the terminal when a book is built, it
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};
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();
}
}
}
```
> **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()`.
There's always the possibility that an error will occur while processing a book
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
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() {
...
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,
}
```
Now, if we reinstall the backend and build a book,
```
$ cargo install --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
```
As you've probably already noticed, output from the plugin's subprocess is
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).
All environment variables are passed through to the backend, allowing you to
use the usual `RUST_LOG` to control logging verbosity.
## Wrapping Up
Although contrived, hopefully this example was enough to show how you'd create
an alternate backend for `mdbook`. If you feel it's missing something, don't
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
as a good example of how it's done in real life, so feel free to skim through
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
[rust-skeptic]: https://github.com/budziq/rust-skeptic
[`RenderContext`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html
[`RenderContext::from_json()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html#method.from_json
[`semver`]: https://crates.io/crates/semver
[`Book`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html
[`Book::iter()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html#method.iter
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues

View File

@ -0,0 +1,46 @@
# 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. It also has a fairly
flexible plugin mechanism, allowing you to create your own custom tooling and
consumers (often referred to as *backends*) if you need to do some analysis of
the book or render it in a different format.
The *For Developers* chapters are here to show you the more advanced usage of
`mdbook`.
The two main ways a developer can hook into the book's build process is via,
- [Preprocessors](for_developers/preprocessors.html)
- [Alternate Backends](for_developers/backends.html)
## The Build Process
The process of rendering a book project goes through several steps.
1. Load the book
- Parse the `book.toml`, falling back to the default `Config` if it doesn't
exist.
- Load the book chapters into memory
- Discover which preprocessors/backends should be used
2. Run the preprocessors
3. Call each backend in turn
## Using `mdbook` as a Library
The `mdbook` binary is just a wrapper around the `mdbook` crate, exposing its
functionality as a command-line program. As such it is quite easy to create your
own programs which use `mdbook` internally, adding your own functionality (e.g.
a custom preprocessor) or tweaking the build process.
The easiest way to find out how to use the `mdbook` crate is by looking at the
[API Docs]. The top level documentation explains how one would use the
[`MDBook`] type to load and build a book, while the [config] module gives a good
explanation on the configuration system.
[`MDBook`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.MDBook.html
[API Docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
[config]: file:///home/michael/Documents/forks/mdBook/target/doc/mdbook/config/index.html

View File

@ -0,0 +1,10 @@
[package]
name = "mdbook-wordcount"
version = "0.1.0"
authors = ["Michael Bryan <michaelfbryan@gmail.com>"]
workspace = "../../../.."
[dependencies]
mdbook = { path = "../../../.." }
serde = "1.0"
serde_derive = "1.0"

View File

@ -0,0 +1,49 @@
extern crate mdbook;
extern crate serde;
#[macro_use]
extern crate serde_derive;
use std::process;
use std::fs::{self, File};
use std::io::{self, Write};
use mdbook::renderer::RenderContext;
use mdbook::book::{BookItem, Chapter};
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();
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 {
if cfg.ignores.contains(&ch.name) {
continue;
}
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);
}
}
}
}
fn count_words(ch: &Chapter) -> usize {
ch.content.split_whitespace().count()
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct WordcountConfig {
pub ignores: Vec<String>,
pub deny_odds: bool,
}

View File

@ -0,0 +1,32 @@
# Preprocessors
A *preprocessor* is simply a bit of code which gets run immediately after the
book is loaded and before it gets rendered, allowing you to update and mutate
the book. Possible use cases are:
- Creating custom helpers like `{{#include /path/to/file.md}}`
- Updating links so `[some chapter](some_chapter.md)` is automatically changed
to `[some chapter](some_chapter.html)` for the HTML renderer
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
mathjax equivalents
## Implementing a Preprocessor
A preprocessor is represented by the `Preprocessor` trait.
```rust
pub trait Preprocessor {
fn name(&self) -> &str;
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
}
```
Where the `PreprocessorContext` is defined as
```rust
pub struct PreprocessorContext {
pub root: PathBuf,
pub config: Config,
}
```

View File

@ -35,8 +35,10 @@ With the following syntax, you can insert runnable Rust files into your book:
The path to the Rust file has to be relative from the current source file. The path to the Rust file has to be relative from the current source file.
When play is clicked, the code snippet will be send to the [Rust Playpen]() to be compiled and run. The result is send back and displayed directly underneath the code. When play is clicked, the code snippet will be send to the [Rust Playpen] to be compiled and run. The result is send back and displayed directly underneath the code.
Here is what a rendered code snippet looks like: Here is what a rendered code snippet looks like:
{{#playpen example.rs}} {{#playpen example.rs}}
[Rust Playpen]: https://play.rust-lang.org/

View File

@ -1,176 +0,0 @@
# 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

View File

@ -2,10 +2,9 @@
set -ex set -ex
# TODO This is the "test phase", tweak it as you see fit
main() { main() {
cross build --target $TARGET cross build --target $TARGET --all
cross build --target $TARGET --release cross build --target $TARGET --all --release
if [ ! -z $DISABLE_TESTS ]; then if [ ! -z $DISABLE_TESTS ]; then
return return

View File

@ -5,8 +5,6 @@
//! //!
//! [1]: ../index.html //! [1]: ../index.html
#![deny(missing_docs)]
mod summary; mod summary;
mod book; mod book;
mod init; mod init;
@ -38,9 +36,6 @@ pub struct MDBook {
pub book: Book, pub book: Book,
renderers: Vec<Box<Renderer>>, renderers: Vec<Box<Renderer>>,
/// The URL used for live reloading when serving up the book.
pub livereload: Option<String>,
/// List of pre-processors to be run on the book /// List of pre-processors to be run on the book
preprocessors: Vec<Box<Preprocessor>> preprocessors: Vec<Box<Preprocessor>>
} }
@ -85,7 +80,6 @@ impl MDBook {
let src_dir = root.join(&config.book.src); let src_dir = root.join(&config.book.src);
let book = book::load_book(&src_dir, &config.build)?; let book = book::load_book(&src_dir, &config.build)?;
let livereload = None;
let renderers = determine_renderers(&config); let renderers = determine_renderers(&config);
let preprocessors = determine_preprocessors(&config)?; let preprocessors = determine_preprocessors(&config)?;
@ -95,7 +89,6 @@ impl MDBook {
config, config,
book, book,
renderers, renderers,
livereload,
preprocessors, preprocessors,
}) })
} }

View File

@ -1,4 +1,54 @@
//! Mdbook's configuration system. //! Mdbook's configuration system.
//!
//! The main entrypoint of the `config` module is the `Config` struct. This acts
//! essentially as a bag of configuration information, with a couple
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
//! for arbitrary data which is exposed to plugins and alternate backends.
//!
//!
//! # Examples
//!
//! ```rust
//! # extern crate mdbook;
//! # use mdbook::errors::*;
//! # extern crate toml;
//! use std::path::PathBuf;
//! use mdbook::Config;
//! use toml::Value;
//!
//! # fn run() -> Result<()> {
//! let src = r#"
//! [book]
//! title = "My Book"
//! authors = ["Michael-F-Bryan"]
//!
//! [build]
//! src = "out"
//!
//! [other-table.foo]
//! bar = 123
//! "#;
//!
//! // load the `Config` from a toml string
//! let mut cfg = Config::from_str(src)?;
//!
//! // retrieve a nested value
//! let bar = cfg.get("other-table.foo.bar").cloned();
//! assert_eq!(bar, Some(Value::Integer(123)));
//!
//! // Set the `output.html.theme` directory
//! assert!(cfg.get("output.html").is_none());
//! cfg.set("output.html.theme", "./themes");
//!
//! // then load it again, automatically deserializing to a `PathBuf`.
//! let got: PathBuf = cfg.get_deserialized("output.html.theme")?;
//! assert_eq!(got, PathBuf::from("./themes"));
//! # Ok(())
//! # }
//! # fn main() { run().unwrap() }
//! ```
#![deny(missing_docs)]
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::fs::File; use std::fs::File;
@ -14,11 +64,13 @@ use serde_json;
use errors::*; use errors::*;
/// The overall configuration object for MDBook. /// The overall configuration object for MDBook, essentially an in-memory
/// representation of `book.toml`.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Config { pub struct Config {
/// Metadata about the book. /// Metadata about the book.
pub book: BookConfig, pub book: BookConfig,
/// Information about the build environment.
pub build: BuildConfig, pub build: BuildConfig,
rest: Value, rest: Value,
} }
@ -344,15 +396,24 @@ impl Default for BuildConfig {
} }
} }
/// Configuration for the HTML renderer.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")] #[serde(default, rename_all = "kebab-case")]
pub struct HtmlConfig { pub struct HtmlConfig {
/// The theme directory, if specified.
pub theme: Option<PathBuf>, pub theme: Option<PathBuf>,
/// Use "smart quotes" instead of the usual `"` character.
pub curly_quotes: bool, pub curly_quotes: bool,
/// Should mathjax be enabled?
pub mathjax_support: bool, pub mathjax_support: bool,
/// An optional google analytics code.
pub google_analytics: Option<String>, pub google_analytics: Option<String>,
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
pub additional_css: Vec<PathBuf>, pub additional_css: Vec<PathBuf>,
/// Additional JS scripts to include at the bottom of the rendered page's
/// `<body>`.
pub additional_js: Vec<PathBuf>, pub additional_js: Vec<PathBuf>,
/// Playpen settings.
pub playpen: Playpen, pub playpen: Playpen,
/// This is used as a bit of a workaround for the `mdbook serve` command. /// This is used as a bit of a workaround for the `mdbook serve` command.
/// Basically, because you set the websocket port from the command line, the /// Basically, because you set the websocket port from the command line, the
@ -362,6 +423,7 @@ pub struct HtmlConfig {
/// This config item *should not be edited* by the end user. /// This config item *should not be edited* by the end user.
#[doc(hidden)] #[doc(hidden)]
pub livereload_url: Option<String>, pub livereload_url: Option<String>,
/// Should section labels be rendered?
pub no_section_label: bool, pub no_section_label: bool,
} }
@ -369,7 +431,11 @@ pub struct HtmlConfig {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")] #[serde(default, rename_all = "kebab-case")]
pub struct Playpen { pub struct Playpen {
/// The path to the editor to use. Defaults to the [Ace Editor].
///
/// [Ace Editor]: https://ace.c9.io/
pub editor: PathBuf, pub editor: PathBuf,
/// Should playpen snippets be editable? Defaults to `false`.
pub editable: bool, pub editable: bool,
} }

View File

@ -12,7 +12,7 @@
//! - Integrate mdbook in a current project //! - Integrate mdbook in a current project
//! - Extend the capabilities of mdBook //! - Extend the capabilities of mdBook
//! - Do some processing or test before building your book //! - Do some processing or test before building your book
//! - Write a new Renderer //! - Accessing the public API to help create a new Renderer
//! - ... //! - ...
//! //!
//! # Examples //! # Examples
@ -50,48 +50,30 @@
//! md.build().expect("Building failed"); //! md.build().expect("Building failed");
//! ``` //! ```
//! //!
//! ## Implementing a new Renderer //! ## Implementing a new Backend
//! //!
//! If you want to create a new renderer for mdBook, the only thing you have to //! `mdbook` has a fairly flexible mechanism for creating additional backends
//! do is to implement the [Renderer](renderer/renderer/trait.Renderer.html) //! for your book. The general idea is you'll add an extra table in the book's
//! trait. //! `book.toml` which specifies an executable to be invoked by `mdbook`. This
//! executable will then be called during a build, with an in-memory
//! representation ([`RenderContext`]) of the book being passed to the
//! subprocess via `stdin`.
//! //!
//! And then you can swap in your renderer like this: //! The [`RenderContext`] gives the backend access to the contents of
//! `book.toml` and lets it know which directory all generated artefacts should
//! be placed in. For a much more in-depth explanation, consult the [relevant
//! chapter] in the *For Developers* section of the user guide.
//! //!
//! ```no_run //! To make creating a backend easier, the `mdbook` crate can be imported
//! # extern crate mdbook; //! directly, making deserializing the `RenderContext` easy and giving you
//! # //! access to the various methods for working with the [`Config`].
//! # use mdbook::MDBook;
//! # use mdbook::renderer::HtmlHandlebars;
//! #
//! # #[allow(unused_variables)]
//! # fn main() {
//! # let your_renderer = HtmlHandlebars::new();
//! #
//! let mut book = MDBook::load("my-book").unwrap();
//! book.with_renderer(your_renderer);
//! # }
//! ```
//!
//! If you make a renderer, you get the book constructed in form of
//! `Vec<BookItems>` and you get ! the book config in a `BookConfig` struct.
//!
//! It's your responsability to create the necessary files in the correct
//! directories.
//!
//! ## utils
//!
//! I have regrouped some useful functions in the [utils](utils/index.html)
//! module, like the following function [`utils::fs::create_file(path:
//! &Path)`](utils/fs/fn.create_file.html).
//!
//! This function creates a file and returns it. But before creating the file
//! it checks every directory in the path to see if it exists, and if it does
//! not it will be created.
//!
//! Make sure to take a look at it.
//! //!
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/ //! [user guide]: https://rust-lang-nursery.github.io/mdBook/
//! [`RenderContext`]: renderer/struct.RenderContext.html
//! [relevant chapter]: https://rust-lang-nursery.github.io/mdBook/for_developers/backends.html
//! [`Config`]: config/struct.Config.html
#![deny(missing_docs)]
#[macro_use] #[macro_use]
extern crate error_chain; extern crate error_chain;
@ -128,6 +110,7 @@ pub mod utils;
pub use book::MDBook; pub use book::MDBook;
pub use book::BookItem; pub use book::BookItem;
pub use renderer::Renderer; pub use renderer::Renderer;
pub use config::Config;
/// The error types used through out this crate. /// The error types used through out this crate.
pub mod errors { pub mod errors {
@ -135,27 +118,30 @@ pub mod errors {
error_chain!{ error_chain!{
foreign_links { foreign_links {
Io(::std::io::Error); Io(::std::io::Error) #[doc = "A wrapper around `std::io::Error`"];
HandlebarsRender(::handlebars::RenderError); HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
HandlebarsTemplate(Box<::handlebars::TemplateError>); HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
Utf8(::std::string::FromUtf8Error); Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
} }
links { links {
TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind); TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind) #[doc = "A TomlQuery error"];
} }
errors { errors {
/// A subprocess exited with an unsuccessful return code.
Subprocess(message: String, output: ::std::process::Output) { Subprocess(message: String, output: ::std::process::Output) {
description("A subprocess failed") description("A subprocess failed")
display("{}: {}", message, String::from_utf8_lossy(&output.stdout)) display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
} }
/// An error was encountered while parsing the `SUMMARY.md` file.
ParseError(line: usize, col: usize, message: String) { ParseError(line: usize, col: usize, message: String) {
description("A SUMMARY.md parsing error") description("A SUMMARY.md parsing error")
display("Error at line {}, column {}: {}", line, col, message) display("Error at line {}, column {}: {}", line, col, message)
} }
/// The user tried to use a reserved filename.
ReservedFilenameError(filename: PathBuf) { ReservedFilenameError(filename: PathBuf) {
description("Reserved Filename") description("Reserved Filename")
display("{} is reserved for internal use", filename.display()) display("{} is reserved for internal use", filename.display())

View File

@ -10,9 +10,12 @@ use book::{Book, BookItem};
const ESCAPE_CHAR: char = '\\'; const ESCAPE_CHAR: char = '\\';
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
/// helpers in a chapter.
pub struct LinkPreprocessor; pub struct LinkPreprocessor;
impl LinkPreprocessor { impl LinkPreprocessor {
/// Create a new `LinkPreprocessor`.
pub fn new() -> Self { pub fn new() -> Self {
LinkPreprocessor LinkPreprocessor
} }

View File

@ -1,3 +1,5 @@
//! Book preprocessing.
pub use self::links::LinkPreprocessor; pub use self::links::LinkPreprocessor;
mod links; mod links;
@ -8,18 +10,29 @@ use errors::*;
use std::path::PathBuf; use std::path::PathBuf;
/// Extra information for a `Preprocessor` to give them more context when
/// processing a book.
pub struct PreprocessorContext { pub struct PreprocessorContext {
/// The location of the book directory on disk.
pub root: PathBuf, pub root: PathBuf,
/// The book configuration (`book.toml`).
pub config: Config, pub config: Config,
} }
impl PreprocessorContext { impl PreprocessorContext {
pub fn new(root: PathBuf, config: Config) -> Self { /// Create a new `PreprocessorContext`.
pub(crate) fn new(root: PathBuf, config: Config) -> Self {
PreprocessorContext { root, config } PreprocessorContext { root, config }
} }
} }
/// An operation which is run immediately after loading a book into memory and
/// before it gets rendered.
pub trait Preprocessor { pub trait Preprocessor {
/// Get the `Preprocessor`'s name.
fn name(&self) -> &str; fn name(&self) -> &str;
/// Run this `Preprocessor`, allowing it to update the book before it is
/// given to a renderer.
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>; fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
} }

View File

@ -1,3 +1,5 @@
#![allow(missing_docs)] // FIXME: Document this
pub use self::hbs_renderer::HtmlHandlebars; pub use self::hbs_renderer::HtmlHandlebars;
mod hbs_renderer; mod hbs_renderer;

View File

@ -1,3 +1,4 @@
#![allow(missing_docs)] // FIXME: Document this
pub mod playpen_editor; pub mod playpen_editor;
use std::path::Path; use std::path::Path;

View File

@ -1,3 +1,5 @@
#![allow(missing_docs)] // FIXME: Document this
pub mod fs; pub mod fs;
mod string; mod string;
use errors::Error; use errors::Error;