Skip to content

Making configuration more flexible #457

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 16 commits into from
Nov 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
126 changes: 87 additions & 39 deletions book-example/src/format/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@

You can configure the parameters for your book in the ***book.toml*** file.

>**Note:**
JSON configuration files were previously supported but have been deprecated in favor of
the TOML configuration file. If you are still using JSON we strongly encourage you to migrate to
the TOML configuration because JSON support will be removed in the future.

Here is an example of what a ***book.toml*** file might look like:

```toml
[book]
title = "Example book"
author = "John Doe"
description = "The example book covers examples."
Expand All @@ -24,66 +20,118 @@ additional-css = ["custom.css"]
It is important to note that **any** relative path specified in the in the configuration will
always be taken relative from the root of the book where the configuration file is located.

### General metadata

- **title:** The title of the book
- **author:** The author of the book
- **description:** A description for the book, which is added as meta information in the html `<head>` of each page

**book.toml**
```toml
title = "Example book"
author = "John Doe"
description = "The example book covers examples."
```
### General metadata

Some books may have multiple authors, there is an alternative key called `authors` plural that lets you specify an array
of authors.
This is general information about your book.

**book.toml**
```toml
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
```

### Source directory
By default, the source directory is found in the directory named `src` directly under the root folder. But this is configurable
with the `source` key in the configuration file.
- **title:** The title of the book
- **authors:** The author(s) of the book
- **description:** A description for the book, which is added as meta
information in the html `<head>` of each page
- **src:** By default, the source directory is found in the directory named
`src` directly under the root folder. But this is configurable with the `src`
key in the configuration file.
- **build-dir:** The directory to put the rendered book in. By default this is
`book/` in the book's root directory.

**book.toml**
```toml
[book]
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."

source = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
build-dir = "build"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You moved the build-dir into the common configuration. Hmmm. If I have multiple output generators, I cannot put them into different directorys. I think the other way was better. What are the arguments for doing it this way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts were that (in the long term) you would have a single build/ directory where all renderers would put their rendered output. During the rendering stage a particular renderer is simply told the directory they should put things in, instead of each renderer doing their own thing and deciding it themselves.

That way mdbook would ensure you have a standardized directory layout looking something like this:

  • my_book
    • build
      • html
      • pdf
      • some_other_renderer
    • src
    • book.toml

With the special case being that if you're only using one renderer, mdbook will put tell a renderer to put its stuff directly in the root (avoiding the redundant extra level of nesting from build/html/).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, that sounds fine :)

```

### HTML renderer options
The HTML renderer has a couple of options aswell. All the options for the renderer need to be specified under the TOML table `[output.html]`.
The HTML renderer has a couple of options as well. All the options for the
renderer need to be specified under the TOML table `[output.html]`.

The following configuration options are available:

- **`destination`:** By default, the HTML book will be rendered in the `root/book` directory, but this option lets you specify another
destination fodler.
- **`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.
- **`curly-quotes`:** Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans. Defaults to `false`.
- **`google-analytics`:** If you use Google Analytics, this option lets you enable it by simply specifying your ID in the configuration file.
- **`additional-css`:** If you need to slightly change the appearance of your book without overwriting the whole style, you can specify a set of stylesheets that will be loaded after the default ones where you can surgically change the style.
- **`additional-js`:** If you need to add some behaviour to your book without removing the current behaviour, you can specify a set of javascript files that will be loaded alongside the default one.
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.
- **curly-quotes:** Convert straight quotes to curly quotes, except for
those that occur in code blocks and code spans. Defaults to `false`.
- **google-analytics:** If you use Google Analytics, this option lets you
enable it by simply specifying your ID in the configuration file.
- **additional-css:** If you need to slightly change the appearance of your
book without overwriting the whole style, you can specify a set of
stylesheets that will be loaded after the default ones where you can
surgically change the style.
- **additional-js:** If you need to add some behaviour to your book without
removing the current behaviour, you can specify a set of javascript files
that will be loaded alongside the default one.
- **playpen:** A subtable for configuring various playpen settings.

**book.toml**
```toml
[book]
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."

[output.html]
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
theme = "my-theme"
curly-quotes = true
google-analytics = "123456"
additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"]

[output.html.playpen]
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
```
20 changes: 6 additions & 14 deletions src/bin/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::{App, ArgMatches, SubCommand};
use std::path::PathBuf;
use clap::{ArgMatches, SubCommand, App};
use mdbook::MDBook;
use mdbook::errors::Result;
use {get_book_dir, open};
Expand All @@ -15,10 +16,6 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.arg_from_usage(
"--no-create 'Will not create non-existent files linked from SUMMARY.md'",
)
.arg_from_usage(
"--curly-quotes 'Convert straight quotes to curly quotes, except for those \
that occur in code blocks and code spans'",
)
.arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the "curly quote" option removed?

Copy link
Contributor Author

@Michael-F-Bryan Michael-F-Bryan Oct 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I removed it because something like that belongs more in the config file than as a command line argument.

My thoughts were that options to alter the generated output and which usually stay the same belong in the config file, while options that alter runtime behaviour (e.g. --open or --no-create) are command line arguments... Does that sound logical?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable to me, but I am not sure about the intent behind the curly quote option.

when omitted)'",
Expand All @@ -28,21 +25,16 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
// Build command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args);
let book = MDBook::new(&book_dir).read_config()?;
let mut book = MDBook::new(&book_dir).read_config()?;

let mut book = match args.value_of("dest-dir") {
Some(dest_dir) => book.with_destination(dest_dir),
None => book,
};
if let Some(dest_dir) = args.value_of("dest-dir") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.. did you move the destination because this argument must be independent of the backend?

Copy link
Contributor Author

@Michael-F-Bryan Michael-F-Bryan Nov 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I was thinking a renderer would be told to put its stuff in the $dest-dir/renderer_name/ directory, or $dest-dir/ if there's only one renderer.

That way renderers are "sandboxed" and should only be touching stuff in the directory they're told to write output to. #409 means renderers are being given an entirely in-memory representation of the book, so these two combined would mean we're a lot more decoupled from the file system and the environment.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kk

book.config.book.build_dir = PathBuf::from(dest_dir);
}

if args.is_present("no-create") {
book.create_missing = false;
}

if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
}

book.build()?;

if args.is_present("open") {
Expand Down
2 changes: 1 addition & 1 deletion src/bin/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
}

// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
let is_dest_inside_root = book.get_destination().starts_with(book.get_root());
let is_dest_inside_root = book.get_destination().starts_with(&book.root);

if !args.is_present("force") && is_dest_inside_root {
println!("\nDo you want a .gitignore to be created? (y/n)");
Expand Down
20 changes: 5 additions & 15 deletions src/bin/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ extern crate staticfile;
extern crate ws;

use std;
use std::path::Path;
use std::path::PathBuf;
use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response,
Set};
use clap::{App, ArgMatches, SubCommand};
Expand All @@ -29,10 +29,6 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
"-d, --dest-dir=[dest-dir] 'The output directory for \
your book{n}(Defaults to ./book when omitted)'",
)
.arg_from_usage(
"--curly-quotes 'Convert straight quotes to curly quotes, except \
for those that occur in code blocks and code spans'",
)
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
.arg_from_usage(
"-w, --websocket-port=[ws-port] 'Use another port for the \
Expand All @@ -53,15 +49,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
const RELOAD_COMMAND: &'static str = "reload";

let book_dir = get_book_dir(args);
let book = MDBook::new(&book_dir).read_config()?;

let mut book = match args.value_of("dest-dir") {
Some(dest_dir) => book.with_destination(Path::new(dest_dir)),
None => book,
};
let mut book = MDBook::new(&book_dir).read_config()?;

if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.book.build_dir = PathBuf::from(dest_dir);
}

let port = args.value_of("port").unwrap_or("3000");
Expand All @@ -73,8 +64,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let address = format!("{}:{}", interface, port);
let ws_address = format!("{}:{}", interface, ws_port);

book.set_livereload(format!(
r#"
book.livereload = Some(format!(r#"
<script type="text/javascript">
var socket = new WebSocket("ws://{}:{}");
socket.onmessage = function (event) {{
Expand Down
24 changes: 7 additions & 17 deletions src/bin/watch.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
extern crate notify;

use std::path::Path;
use std::path::{Path, PathBuf};
use self::notify::Watcher;
use std::time::Duration;
use std::sync::mpsc::channel;
Expand All @@ -18,10 +18,6 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
"-d, --dest-dir=[dest-dir] 'The output directory for \
your book{n}(Defaults to ./book when omitted)'",
)
.arg_from_usage(
"--curly-quotes 'Convert straight quotes to curly quotes, except \
for those that occur in code blocks and code spans'",
)
.arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to \
Current Directory when omitted)'",
Expand All @@ -31,15 +27,10 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
// Watch command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args);
let book = MDBook::new(&book_dir).read_config()?;
let mut book = MDBook::new(&book_dir).read_config()?;

let mut book = match args.value_of("dest-dir") {
Some(dest_dir) => book.with_destination(dest_dir),
None => book,
};

if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.book.build_dir = PathBuf::from(dest_dir);
}

if args.is_present("open") {
Expand Down Expand Up @@ -84,18 +75,17 @@ where
};

// Add the theme directory to the watcher
watcher.watch(book.get_theme_path(), Recursive)
watcher.watch(book.theme_dir(), Recursive)
.unwrap_or_default();


// Add the book.{json,toml} file to the watcher if it exists, because it's not
// located in the source directory
if watcher.watch(book.get_root().join("book.json"), NonRecursive)
if watcher.watch(book.root.join("book.json"), NonRecursive)
.is_err()
{
// do nothing if book.json is not found
}
if watcher.watch(book.get_root().join("book.toml"), NonRecursive)
if watcher.watch(book.root.join("book.toml"), NonRecursive)
.is_err()
{
// do nothing if book.toml is not found
Expand Down
Loading