2017-09-30 20:11:24 +08:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use std::fs::File;
|
|
|
|
use std::io::Read;
|
|
|
|
use toml::{self, Value};
|
2017-11-12 02:03:28 +08:00
|
|
|
use toml::value::Table;
|
|
|
|
use serde::{Deserialize, Deserializer};
|
2017-09-30 20:11:24 +08:00
|
|
|
|
|
|
|
use errors::*;
|
|
|
|
|
|
|
|
/// The overall configuration object for MDBook.
|
2017-11-12 02:03:28 +08:00
|
|
|
#[derive(Debug, Clone, Default, PartialEq)]
|
2017-09-30 20:11:24 +08:00
|
|
|
pub struct Config {
|
|
|
|
/// Metadata about the book.
|
2017-09-30 21:04:05 +08:00
|
|
|
pub book: BookConfig,
|
2017-11-30 12:02:58 +08:00
|
|
|
pub build: BuildConfig,
|
2017-11-12 02:03:28 +08:00
|
|
|
rest: Table,
|
2017-09-30 20:11:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Config {
|
|
|
|
/// Load a `Config` from some string.
|
|
|
|
pub fn from_str(src: &str) -> Result<Config> {
|
|
|
|
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Load the configuration file from disk.
|
|
|
|
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
|
|
|
|
let mut buffer = String::new();
|
2017-10-16 20:47:22 +08:00
|
|
|
File::open(config_file).chain_err(|| "Unable to open the configuration file")?
|
|
|
|
.read_to_string(&mut buffer)
|
|
|
|
.chain_err(|| "Couldn't read the file")?;
|
2017-09-30 20:11:24 +08:00
|
|
|
|
|
|
|
Config::from_str(&buffer)
|
|
|
|
}
|
|
|
|
|
2017-11-12 02:03:28 +08:00
|
|
|
/// Fetch an arbitrary item from the `Config` as a `toml::Value`.
|
|
|
|
///
|
|
|
|
/// You can use dotted indices to access nested items (e.g.
|
|
|
|
/// `output.html.playpen` will fetch the "playpen" out of the html output
|
|
|
|
/// table).
|
|
|
|
pub fn get(&self, key: &str) -> Option<&Value> {
|
|
|
|
let pieces: Vec<_> = key.split(".").collect();
|
|
|
|
recursive_get(&pieces, &self.rest)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Fetch a value from the `Config` so you can mutate it.
|
|
|
|
pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
|
|
|
|
let pieces: Vec<_> = key.split(".").collect();
|
|
|
|
recursive_get_mut(&pieces, &mut self.rest)
|
|
|
|
}
|
|
|
|
|
2017-09-30 20:11:24 +08:00
|
|
|
/// Convenience method for getting the html renderer's configuration.
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This is for compatibility only. It will be removed completely once the
|
|
|
|
/// rendering and plugin system is established.
|
2017-09-30 21:04:05 +08:00
|
|
|
pub fn html_config(&self) -> Option<HtmlConfig> {
|
2017-11-12 02:03:28 +08:00
|
|
|
self.get_deserialized("output.html").ok()
|
2017-09-30 20:34:27 +08:00
|
|
|
}
|
|
|
|
|
2017-11-12 02:03:28 +08:00
|
|
|
/// Convenience function to fetch a value from the config and deserialize it
|
|
|
|
/// into some arbitrary type.
|
|
|
|
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
|
|
|
|
let name = name.as_ref();
|
|
|
|
|
|
|
|
if let Some(value) = self.get(name) {
|
|
|
|
value.clone()
|
|
|
|
.try_into()
|
|
|
|
.chain_err(|| "Couldn't deserialize the value")
|
|
|
|
} else {
|
|
|
|
bail!("Key not found, {:?}", name)
|
|
|
|
}
|
2017-09-30 20:34:27 +08:00
|
|
|
}
|
2017-11-12 21:00:18 +08:00
|
|
|
|
|
|
|
fn from_legacy(mut table: Table) -> Config {
|
|
|
|
let mut cfg = Config::default();
|
|
|
|
|
|
|
|
// we use a macro here instead of a normal loop because the $out
|
|
|
|
// variable can be different types. This way we can make type inference
|
|
|
|
// figure out what try_into() deserializes to.
|
|
|
|
macro_rules! get_and_insert {
|
|
|
|
($table:expr, $key:expr => $out:expr) => {
|
|
|
|
if let Some(value) = $table.remove($key).and_then(|v| v.try_into().ok()) {
|
|
|
|
$out = value;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
get_and_insert!(table, "title" => cfg.book.title);
|
|
|
|
get_and_insert!(table, "authors" => cfg.book.authors);
|
|
|
|
get_and_insert!(table, "source" => cfg.book.src);
|
|
|
|
get_and_insert!(table, "description" => cfg.book.description);
|
|
|
|
|
|
|
|
// This complicated chain of and_then's is so we can move
|
2017-11-30 23:26:30 +08:00
|
|
|
// "output.html.destination" to "build.build_dir" and parse it into a
|
2017-11-12 21:00:18 +08:00
|
|
|
// PathBuf.
|
|
|
|
let destination: Option<PathBuf> = table.get_mut("output")
|
|
|
|
.and_then(|output| output.as_table_mut())
|
|
|
|
.and_then(|output| output.get_mut("html"))
|
|
|
|
.and_then(|html| html.as_table_mut())
|
|
|
|
.and_then(|html| html.remove("destination"))
|
|
|
|
.and_then(|dest| dest.try_into().ok());
|
|
|
|
|
|
|
|
if let Some(dest) = destination {
|
2017-11-30 23:26:30 +08:00
|
|
|
cfg.build.build_dir = dest;
|
2017-11-12 21:00:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
cfg.rest = table;
|
|
|
|
cfg
|
|
|
|
}
|
2017-11-12 02:03:28 +08:00
|
|
|
}
|
2017-09-30 20:34:27 +08:00
|
|
|
|
2017-11-12 02:03:28 +08:00
|
|
|
fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> {
|
|
|
|
if key.is_empty() {
|
|
|
|
return None;
|
|
|
|
} else if key.len() == 1 {
|
|
|
|
return table.get(key[0]);
|
2017-09-30 20:34:27 +08:00
|
|
|
}
|
|
|
|
|
2017-11-12 02:03:28 +08:00
|
|
|
let first = key[0];
|
|
|
|
let rest = &key[1..];
|
|
|
|
|
|
|
|
if let Some(&Value::Table(ref nested)) = table.get(first) {
|
|
|
|
recursive_get(rest, nested)
|
|
|
|
} else {
|
|
|
|
None
|
2017-09-30 20:34:27 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-12 02:03:28 +08:00
|
|
|
fn recursive_get_mut<'a>(key: &[&str], table: &'a mut Table) -> Option<&'a mut Value> {
|
|
|
|
// TODO: Figure out how to abstract over mutability to reduce copy-pasta
|
|
|
|
if key.is_empty() {
|
|
|
|
return None;
|
|
|
|
} else if key.len() == 1 {
|
|
|
|
return table.get_mut(key[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
let first = key[0];
|
|
|
|
let rest = &key[1..];
|
|
|
|
|
|
|
|
if let Some(&mut Value::Table(ref mut nested)) = table.get_mut(first) {
|
|
|
|
recursive_get_mut(rest, nested)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'de> Deserialize<'de> for Config {
|
|
|
|
fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
|
|
|
|
let raw = Value::deserialize(de)?;
|
2017-11-12 21:00:18 +08:00
|
|
|
|
|
|
|
let mut table = match raw {
|
|
|
|
Value::Table(t) => t,
|
|
|
|
_ => {
|
|
|
|
use serde::de::Error;
|
|
|
|
return Err(D::Error::custom(
|
|
|
|
"A config file should always be a toml table",
|
|
|
|
));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if is_legacy_format(&table) {
|
|
|
|
warn!("It looks like you are using the legacy book.toml format.");
|
|
|
|
warn!("We'll parse it for now, but you should probably convert to the new format.");
|
2017-11-12 21:26:59 +08:00
|
|
|
warn!("See the mdbook documentation for more details, although as a rule of thumb");
|
2017-11-30 23:26:30 +08:00
|
|
|
warn!("just move all top level configuration entries like `title`, `author` and");
|
|
|
|
warn!("`description` under a table called `[book]`, move the `destination` entry");
|
|
|
|
warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
|
|
|
|
warn!("`[build]`, and it should all work.");
|
2017-11-12 21:26:59 +08:00
|
|
|
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
|
2017-11-12 21:00:18 +08:00
|
|
|
return Ok(Config::from_legacy(table));
|
2017-10-16 20:47:22 +08:00
|
|
|
}
|
2017-11-12 21:00:18 +08:00
|
|
|
|
|
|
|
let book: BookConfig = table.remove("book")
|
|
|
|
.and_then(|value| value.try_into().ok())
|
|
|
|
.unwrap_or_default();
|
2017-11-30 12:02:58 +08:00
|
|
|
|
|
|
|
let build: BuildConfig = table.remove("build")
|
|
|
|
.and_then(|value| value.try_into().ok())
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
2017-11-12 21:00:18 +08:00
|
|
|
Ok(Config {
|
|
|
|
book: book,
|
2017-11-30 12:02:58 +08:00
|
|
|
build: build,
|
2017-11-12 21:00:18 +08:00
|
|
|
rest: table,
|
|
|
|
})
|
2017-09-30 20:11:24 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-12 21:00:18 +08:00
|
|
|
fn is_legacy_format(table: &Table) -> bool {
|
|
|
|
let top_level_items = ["title", "author", "authors"];
|
|
|
|
|
|
|
|
top_level_items.iter().any(|key| table.contains_key(&key.to_string()))
|
|
|
|
}
|
|
|
|
|
2017-09-30 20:11:24 +08:00
|
|
|
|
|
|
|
/// Configuration options which are specific to the book and required for
|
|
|
|
/// loading it from disk.
|
2017-09-30 20:34:27 +08:00
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
2017-09-30 20:11:24 +08:00
|
|
|
#[serde(default, rename_all = "kebab-case")]
|
2017-09-30 21:04:05 +08:00
|
|
|
pub struct BookConfig {
|
2017-09-30 20:11:24 +08:00
|
|
|
/// The book's title.
|
|
|
|
pub title: Option<String>,
|
|
|
|
/// The book's authors.
|
|
|
|
pub authors: Vec<String>,
|
|
|
|
/// An optional description for the book.
|
|
|
|
pub description: Option<String>,
|
2017-11-12 02:03:28 +08:00
|
|
|
/// Location of the book source relative to the book's root directory.
|
2017-09-30 20:11:24 +08:00
|
|
|
pub src: PathBuf,
|
|
|
|
/// Does this book support more than one language?
|
|
|
|
pub multilingual: bool,
|
|
|
|
}
|
|
|
|
|
2017-09-30 21:04:05 +08:00
|
|
|
impl Default for BookConfig {
|
|
|
|
fn default() -> BookConfig {
|
|
|
|
BookConfig {
|
2017-09-30 20:34:27 +08:00
|
|
|
title: None,
|
|
|
|
authors: Vec::new(),
|
|
|
|
description: None,
|
|
|
|
src: PathBuf::from("src"),
|
|
|
|
multilingual: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-30 12:02:58 +08:00
|
|
|
/// Configuration for the build procedure.
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
#[serde(default, rename_all = "kebab-case")]
|
|
|
|
pub struct BuildConfig {
|
2017-11-30 23:26:30 +08:00
|
|
|
/// Where to put built artefacts relative to the book's root directory.
|
|
|
|
pub build_dir: PathBuf,
|
2017-11-30 12:02:58 +08:00
|
|
|
/// Should non-existent markdown files specified in `SETTINGS.md` be created
|
|
|
|
/// if they don't exist?
|
|
|
|
pub create_missing: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for BuildConfig {
|
|
|
|
fn default() -> BuildConfig {
|
|
|
|
BuildConfig {
|
2017-11-30 23:26:30 +08:00
|
|
|
build_dir: PathBuf::from("book"),
|
2017-11-30 12:02:58 +08:00
|
|
|
create_missing: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-30 20:34:27 +08:00
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
2017-09-30 20:11:24 +08:00
|
|
|
#[serde(default, rename_all = "kebab-case")]
|
2017-09-30 21:04:05 +08:00
|
|
|
pub struct HtmlConfig {
|
2017-09-30 20:11:24 +08:00
|
|
|
pub theme: Option<PathBuf>,
|
|
|
|
pub curly_quotes: bool,
|
|
|
|
pub mathjax_support: bool,
|
|
|
|
pub google_analytics: Option<String>,
|
|
|
|
pub additional_css: Vec<PathBuf>,
|
|
|
|
pub additional_js: Vec<PathBuf>,
|
|
|
|
pub playpen: Playpen,
|
|
|
|
}
|
|
|
|
|
2017-11-12 02:03:28 +08:00
|
|
|
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
2017-09-30 20:34:27 +08:00
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
2017-09-30 21:36:03 +08:00
|
|
|
pub struct Playpen {
|
|
|
|
pub editor: PathBuf,
|
|
|
|
pub editable: bool,
|
|
|
|
}
|
2017-09-30 20:34:27 +08:00
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
2017-11-12 02:03:28 +08:00
|
|
|
const COMPLEX_CONFIG: &'static str = r#"
|
2017-09-30 20:34:27 +08:00
|
|
|
[book]
|
|
|
|
title = "Some Book"
|
|
|
|
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
|
|
|
|
description = "A completely useless book"
|
|
|
|
multilingual = true
|
|
|
|
src = "source"
|
|
|
|
|
2017-11-30 12:02:58 +08:00
|
|
|
[build]
|
2017-11-30 23:26:30 +08:00
|
|
|
build-dir = "outputs"
|
2017-11-30 12:02:58 +08:00
|
|
|
create-missing = false
|
|
|
|
|
2017-09-30 20:34:27 +08:00
|
|
|
[output.html]
|
2017-09-30 21:54:25 +08:00
|
|
|
theme = "./themedir"
|
2017-09-30 20:34:27 +08:00
|
|
|
curly-quotes = true
|
|
|
|
google-analytics = "123456"
|
|
|
|
additional-css = ["./foo/bar/baz.css"]
|
2017-09-30 21:54:25 +08:00
|
|
|
|
|
|
|
[output.html.playpen]
|
|
|
|
editable = true
|
|
|
|
editor = "ace"
|
2017-09-30 20:34:27 +08:00
|
|
|
"#;
|
|
|
|
|
2017-11-12 02:03:28 +08:00
|
|
|
#[test]
|
|
|
|
fn load_a_complex_config_file() {
|
|
|
|
let src = COMPLEX_CONFIG;
|
|
|
|
|
2017-09-30 21:54:25 +08:00
|
|
|
let book_should_be = BookConfig {
|
2017-09-30 20:34:27 +08:00
|
|
|
title: Some(String::from("Some Book")),
|
|
|
|
authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
|
|
|
|
description: Some(String::from("A completely useless book")),
|
|
|
|
multilingual: true,
|
|
|
|
src: PathBuf::from("source"),
|
|
|
|
..Default::default()
|
|
|
|
};
|
2017-11-30 12:02:58 +08:00
|
|
|
let build_should_be = BuildConfig {
|
2017-11-30 23:26:30 +08:00
|
|
|
build_dir: PathBuf::from("outputs"),
|
2017-11-30 12:02:58 +08:00
|
|
|
create_missing: false,
|
|
|
|
};
|
2017-09-30 21:54:25 +08:00
|
|
|
let playpen_should_be = Playpen {
|
|
|
|
editable: true,
|
|
|
|
editor: PathBuf::from("ace"),
|
|
|
|
};
|
|
|
|
let html_should_be = HtmlConfig {
|
2017-09-30 20:34:27 +08:00
|
|
|
curly_quotes: true,
|
|
|
|
google_analytics: Some(String::from("123456")),
|
|
|
|
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
2017-09-30 21:54:25 +08:00
|
|
|
theme: Some(PathBuf::from("./themedir")),
|
|
|
|
playpen: playpen_should_be,
|
2017-09-30 20:34:27 +08:00
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
|
|
|
|
let got = Config::from_str(src).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(got.book, book_should_be);
|
2017-11-30 12:02:58 +08:00
|
|
|
assert_eq!(got.build, build_should_be);
|
2017-09-30 20:34:27 +08:00
|
|
|
assert_eq!(got.html_config().unwrap(), html_should_be);
|
2017-09-30 20:11:24 +08:00
|
|
|
}
|
|
|
|
|
2017-09-30 20:34:27 +08:00
|
|
|
#[test]
|
|
|
|
fn load_arbitrary_output_type() {
|
|
|
|
#[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 should_be = RandomOutput {
|
|
|
|
foo: 5,
|
|
|
|
bar: String::from("Hello World"),
|
|
|
|
baz: vec![true, true, false],
|
|
|
|
};
|
|
|
|
|
|
|
|
let cfg = Config::from_str(src).unwrap();
|
2017-11-12 02:03:28 +08:00
|
|
|
let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
|
2017-09-30 20:34:27 +08:00
|
|
|
|
|
|
|
assert_eq!(got, should_be);
|
2017-11-12 02:03:28 +08:00
|
|
|
|
|
|
|
let baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
|
|
|
|
let baz_should_be = vec![true, true, false];
|
|
|
|
|
|
|
|
assert_eq!(baz, baz_should_be);
|
2017-09-30 20:34:27 +08:00
|
|
|
}
|
2017-11-12 02:03:28 +08:00
|
|
|
|
2017-11-12 21:00:18 +08:00
|
|
|
#[test]
|
|
|
|
fn mutate_some_stuff() {
|
|
|
|
// really this is just a sanity check to make sure the borrow checker
|
|
|
|
// is happy...
|
|
|
|
let src = COMPLEX_CONFIG;
|
|
|
|
let mut config = Config::from_str(src).unwrap();
|
|
|
|
let key = "output.html.playpen.editable";
|
|
|
|
|
|
|
|
assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
|
|
|
|
*config.get_mut(key).unwrap() = Value::Boolean(false);
|
|
|
|
assert_eq!(config.get(key).unwrap(), &Value::Boolean(false));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The config file format has slightly changed (metadata stuff is now under
|
|
|
|
/// the `book` table instead of being at the top level) so we're adding a
|
|
|
|
/// **temporary** compatibility check. You should be able to still load the
|
|
|
|
/// old format, emitting a warning.
|
|
|
|
#[test]
|
|
|
|
fn can_still_load_the_previous_format() {
|
|
|
|
let src = r#"
|
|
|
|
title = "mdBook Documentation"
|
|
|
|
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
|
|
|
authors = ["Mathieu David"]
|
|
|
|
source = "./source"
|
|
|
|
|
|
|
|
[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"]
|
|
|
|
"#;
|
|
|
|
|
|
|
|
let book_should_be = BookConfig {
|
|
|
|
title: Some(String::from("mdBook Documentation")),
|
|
|
|
description: Some(String::from(
|
|
|
|
"Create book from markdown files. Like Gitbook but implemented in Rust",
|
|
|
|
)),
|
|
|
|
authors: vec![String::from("Mathieu David")],
|
|
|
|
src: PathBuf::from("./source"),
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
|
2017-11-30 23:26:30 +08:00
|
|
|
let build_should_be = BuildConfig {
|
|
|
|
build_dir: PathBuf::from("my-book"),
|
|
|
|
create_missing: true,
|
|
|
|
};
|
|
|
|
|
2017-11-12 21:00:18 +08:00
|
|
|
let html_should_be = HtmlConfig {
|
|
|
|
theme: Some(PathBuf::from("my-theme")),
|
|
|
|
curly_quotes: true,
|
|
|
|
google_analytics: Some(String::from("123456")),
|
|
|
|
additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
|
|
|
|
additional_js: vec![PathBuf::from("custom.js")],
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
|
|
|
|
let got = Config::from_str(src).unwrap();
|
|
|
|
assert_eq!(got.book, book_should_be);
|
2017-11-30 23:26:30 +08:00
|
|
|
assert_eq!(got.build, build_should_be);
|
2017-11-12 21:00:18 +08:00
|
|
|
assert_eq!(got.html_config().unwrap(), html_should_be);
|
|
|
|
}
|
2017-09-30 20:34:27 +08:00
|
|
|
}
|