diff --git a/Cargo.toml b/Cargo.toml index b499eebc..69b35461 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,19 +20,19 @@ chrono = "0.4" handlebars = "0.29" serde = "1.0" serde_derive = "1.0" -error-chain = "0.11.0" +error-chain = "0.11" serde_json = "1.0" pulldown-cmark = "0.1" lazy_static = "1.0" log = "0.4" env_logger = "0.5.0-rc.1" toml = "0.4" -memchr = "2.0.1" +memchr = "2.0" open = "1.1" regex = "0.2.1" tempdir = "0.3.4" -itertools = "0.7.4" -shlex = "0.1.1" +itertools = "0.7" +shlex = "0.1" toml-query = "0.6" # Watch feature @@ -66,3 +66,5 @@ doc = false name = "mdbook" path = "src/bin/mdbook.rs" +[workspace] +members = ["book-example/src/for_developers/mdbook-wordcount"] diff --git a/book-example/book.toml b/book-example/book.toml index 1c2e9699..df1ed5c5 100644 --- a/book-example/book.toml +++ b/book-example/book.toml @@ -1,7 +1,7 @@ [book] title = "mdBook Documentation" description = "Create book from markdown files. Like Gitbook but implemented in Rust" -author = "Mathieu David" +authors = ["Mathieu David", "Michael-F-Bryan"] [output.html] mathjax-support = true diff --git a/book-example/src/SUMMARY.md b/book-example/src/SUMMARY.md index 078eef90..aba9ab5d 100644 --- a/book-example/src/SUMMARY.md +++ b/book-example/src/SUMMARY.md @@ -16,6 +16,8 @@ - [Editor](format/theme/editor.md) - [MathJax Support](format/mathjax.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) diff --git a/book-example/src/for_developers/backends.md b/book-example/src/for_developers/backends.md new file mode 100644 index 00000000..e09018f4 --- /dev/null +++ b/book-example/src/for_developers/backends.md @@ -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, +} +``` + +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, ++ 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 \ No newline at end of file diff --git a/book-example/src/for_developers/index.md b/book-example/src/for_developers/index.md new file mode 100644 index 00000000..3a193a25 --- /dev/null +++ b/book-example/src/for_developers/index.md @@ -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 \ No newline at end of file diff --git a/book-example/src/for_developers/mdbook-wordcount/Cargo.toml b/book-example/src/for_developers/mdbook-wordcount/Cargo.toml new file mode 100644 index 00000000..55962233 --- /dev/null +++ b/book-example/src/for_developers/mdbook-wordcount/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "mdbook-wordcount" +version = "0.1.0" +authors = ["Michael Bryan "] +workspace = "../../../.." + +[dependencies] +mdbook = { path = "../../../.." } +serde = "1.0" +serde_derive = "1.0" diff --git a/book-example/src/for_developers/mdbook-wordcount/src/main.rs b/book-example/src/for_developers/mdbook-wordcount/src/main.rs new file mode 100644 index 00000000..607338dd --- /dev/null +++ b/book-example/src/for_developers/mdbook-wordcount/src/main.rs @@ -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, + pub deny_odds: bool, +} diff --git a/book-example/src/for_developers/preprocessors.md b/book-example/src/for_developers/preprocessors.md new file mode 100644 index 00000000..4d952902 --- /dev/null +++ b/book-example/src/for_developers/preprocessors.md @@ -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, +} +``` \ No newline at end of file diff --git a/book-example/src/format/rust.md b/book-example/src/format/rust.md index d684dc75..3b84534a 100644 --- a/book-example/src/format/rust.md +++ b/book-example/src/format/rust.md @@ -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. -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: {{#playpen example.rs}} + +[Rust Playpen]: https://play.rust-lang.org/ \ No newline at end of file diff --git a/book-example/src/lib/index.md b/book-example/src/lib/index.md deleted file mode 100644 index 23b96ec7..00000000 --- a/book-example/src/lib/index.md +++ /dev/null @@ -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, -} - -# 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 = 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 \ No newline at end of file diff --git a/ci/script.sh b/ci/script.sh index c1e10bf4..f07c4451 100644 --- a/ci/script.sh +++ b/ci/script.sh @@ -2,10 +2,9 @@ set -ex -# TODO This is the "test phase", tweak it as you see fit main() { - cross build --target $TARGET - cross build --target $TARGET --release + cross build --target $TARGET --all + cross build --target $TARGET --all --release if [ ! -z $DISABLE_TESTS ]; then return diff --git a/src/book/mod.rs b/src/book/mod.rs index 4666e271..0a0fe681 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -5,8 +5,6 @@ //! //! [1]: ../index.html -#![deny(missing_docs)] - mod summary; mod book; mod init; @@ -38,9 +36,6 @@ pub struct MDBook { pub book: Book, renderers: Vec>, - /// The URL used for live reloading when serving up the book. - pub livereload: Option, - /// List of pre-processors to be run on the book preprocessors: Vec> } @@ -85,7 +80,6 @@ impl MDBook { let src_dir = root.join(&config.book.src); let book = book::load_book(&src_dir, &config.build)?; - let livereload = None; let renderers = determine_renderers(&config); let preprocessors = determine_preprocessors(&config)?; @@ -95,7 +89,6 @@ impl MDBook { config, book, renderers, - livereload, preprocessors, }) } diff --git a/src/config.rs b/src/config.rs index e74f1917..5d252b17 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,54 @@ //! 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::fs::File; @@ -14,11 +64,13 @@ use serde_json; 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)] pub struct Config { /// Metadata about the book. pub book: BookConfig, + /// Information about the build environment. pub build: BuildConfig, rest: Value, } @@ -344,15 +396,24 @@ impl Default for BuildConfig { } } +/// Configuration for the HTML renderer. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] pub struct HtmlConfig { + /// The theme directory, if specified. pub theme: Option, + /// Use "smart quotes" instead of the usual `"` character. pub curly_quotes: bool, + /// Should mathjax be enabled? pub mathjax_support: bool, + /// An optional google analytics code. pub google_analytics: Option, + /// Additional CSS stylesheets to include in the rendered page's ``. pub additional_css: Vec, + /// Additional JS scripts to include at the bottom of the rendered page's + /// ``. pub additional_js: Vec, + /// Playpen settings. pub playpen: Playpen, /// 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 @@ -362,6 +423,7 @@ pub struct HtmlConfig { /// This config item *should not be edited* by the end user. #[doc(hidden)] pub livereload_url: Option, + /// Should section labels be rendered? pub no_section_label: bool, } @@ -369,7 +431,11 @@ pub struct HtmlConfig { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] pub struct Playpen { + /// The path to the editor to use. Defaults to the [Ace Editor]. + /// + /// [Ace Editor]: https://ace.c9.io/ pub editor: PathBuf, + /// Should playpen snippets be editable? Defaults to `false`. pub editable: bool, } diff --git a/src/lib.rs b/src/lib.rs index 08c4c37e..559cec30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ //! - Integrate mdbook in a current project //! - Extend the capabilities of mdBook //! - Do some processing or test before building your book -//! - Write a new Renderer +//! - Accessing the public API to help create a new Renderer //! - ... //! //! # Examples @@ -50,48 +50,30 @@ //! 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 -//! do is to implement the [Renderer](renderer/renderer/trait.Renderer.html) -//! trait. -//! -//! And then you can swap in your renderer like this: -//! -//! ```no_run -//! # extern crate mdbook; -//! # -//! # 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` 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. +//! `mdbook` has a fairly flexible mechanism for creating additional backends +//! for your book. The general idea is you'll add an extra table in the book's +//! `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`. +//! +//! 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. +//! +//! To make creating a backend easier, the `mdbook` crate can be imported +//! directly, making deserializing the `RenderContext` easy and giving you +//! access to the various methods for working with the [`Config`]. //! //! [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] extern crate error_chain; @@ -128,6 +110,7 @@ pub mod utils; pub use book::MDBook; pub use book::BookItem; pub use renderer::Renderer; +pub use config::Config; /// The error types used through out this crate. pub mod errors { @@ -135,27 +118,30 @@ pub mod errors { error_chain!{ foreign_links { - Io(::std::io::Error); - HandlebarsRender(::handlebars::RenderError); - HandlebarsTemplate(Box<::handlebars::TemplateError>); - Utf8(::std::string::FromUtf8Error); + Io(::std::io::Error) #[doc = "A wrapper around `std::io::Error`"]; + HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"]; + HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"]; + Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"]; } links { - TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind); + TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind) #[doc = "A TomlQuery error"]; } errors { + /// A subprocess exited with an unsuccessful return code. Subprocess(message: String, output: ::std::process::Output) { description("A subprocess failed") 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) { description("A SUMMARY.md parsing error") display("Error at line {}, column {}: {}", line, col, message) } + /// The user tried to use a reserved filename. ReservedFilenameError(filename: PathBuf) { description("Reserved Filename") display("{} is reserved for internal use", filename.display()) diff --git a/src/preprocess/links.rs b/src/preprocess/links.rs index c3cab8bf..aacf295f 100644 --- a/src/preprocess/links.rs +++ b/src/preprocess/links.rs @@ -10,9 +10,12 @@ use book::{Book, BookItem}; const ESCAPE_CHAR: char = '\\'; +/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}` +/// helpers in a chapter. pub struct LinkPreprocessor; impl LinkPreprocessor { + /// Create a new `LinkPreprocessor`. pub fn new() -> Self { LinkPreprocessor } diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs index 091604d6..6f82c338 100644 --- a/src/preprocess/mod.rs +++ b/src/preprocess/mod.rs @@ -1,3 +1,5 @@ +//! Book preprocessing. + pub use self::links::LinkPreprocessor; mod links; @@ -8,18 +10,29 @@ use errors::*; use std::path::PathBuf; +/// Extra information for a `Preprocessor` to give them more context when +/// processing a book. pub struct PreprocessorContext { + /// The location of the book directory on disk. pub root: PathBuf, + /// The book configuration (`book.toml`). pub config: Config, } 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 } } } +/// An operation which is run immediately after loading a book into memory and +/// before it gets rendered. pub trait Preprocessor { + /// Get the `Preprocessor`'s name. 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<()>; } \ No newline at end of file diff --git a/src/renderer/html_handlebars/mod.rs b/src/renderer/html_handlebars/mod.rs index f1df6d8d..aca09dbe 100644 --- a/src/renderer/html_handlebars/mod.rs +++ b/src/renderer/html_handlebars/mod.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] // FIXME: Document this + pub use self::hbs_renderer::HtmlHandlebars; mod hbs_renderer; diff --git a/src/theme/mod.rs b/src/theme/mod.rs index dc31a247..68bb4411 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] // FIXME: Document this pub mod playpen_editor; use std::path::Path; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 255a7f77..56291aeb 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] // FIXME: Document this + pub mod fs; mod string; use errors::Error;