|
| 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 |
0 commit comments