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;
|
2017-11-18 21:22:30 +08:00
|
|
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
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();
|
2018-01-07 22:10:48 +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
|
2018-01-07 22:10:48 +08:00
|
|
|
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
|
|
|
|
#[doc(hidden)]
|
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) {
|
2018-01-07 22:10:48 +08:00
|
|
|
value
|
|
|
|
.clone()
|
|
|
|
.try_into()
|
|
|
|
.chain_err(|| "Couldn't deserialize the value")
|
2017-11-12 02:03:28 +08:00
|
|
|
} else {
|
|
|
|
bail!("Key not found, {:?}", name)
|
|
|
|
}
|
2017-09-30 20:34:27 +08:00
|
|
|
}
|
2017-11-12 21:00:18 +08:00
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
/// Set a config key, clobbering any existing values along the way.
|
|
|
|
///
|
|
|
|
/// The only way this can fail is if we can't serialize `value` into a
|
|
|
|
/// `toml::Value`.
|
|
|
|
pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
|
|
|
|
let pieces: Vec<_> = index.as_ref().split(".").collect();
|
|
|
|
let value =
|
|
|
|
Value::try_from(value).chain_err(|| "Unable to represent the item as a JSON Value")?;
|
|
|
|
recursive_set(&pieces, &mut self.rest, value);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
// This complicated chain of and_then's is so we can move
|
|
|
|
// "output.html.destination" to "build.build_dir" and parse it into a
|
2017-11-12 21:00:18 +08:00
|
|
|
// PathBuf.
|
2018-01-07 22:10:48 +08:00
|
|
|
let destination: Option<PathBuf> = table
|
|
|
|
.get_mut("output")
|
2017-11-12 21:00:18 +08:00
|
|
|
.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
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
/// Recursively walk down a table and try to set some `foo.bar.baz` value.
|
|
|
|
///
|
|
|
|
/// If at any table along the way doesn't exist (or isn't itself a `Table`!) an
|
|
|
|
/// empty `Table` will be inserted. e.g. if the `foo` table didn't contain a
|
|
|
|
/// nested table called `bar`, we'd insert one and then keep recursing.
|
|
|
|
fn recursive_set(key: &[&str], table: &mut Table, value: Value) {
|
|
|
|
if key.is_empty() {
|
|
|
|
unreachable!();
|
|
|
|
} else if key.len() == 1 {
|
|
|
|
table.insert(key[0].to_string(), value);
|
|
|
|
} else {
|
|
|
|
let first = key[0];
|
|
|
|
let rest = &key[1..];
|
|
|
|
|
|
|
|
// if `table[first]` isn't a table, replace whatever is there with a
|
|
|
|
// new table.
|
|
|
|
if table.get(first).and_then(|t| t.as_table()).is_none() {
|
|
|
|
table.insert(first.to_string(), Value::Table(Table::new()));
|
|
|
|
}
|
|
|
|
|
|
|
|
let nested = table.get_mut(first).and_then(|t| t.as_table_mut()).unwrap();
|
|
|
|
recursive_set(rest, nested, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The "getter" version of `recursive_set()`.
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
/// The mutable version of `recursive_get()`.
|
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
|
|
|
|
2018-01-07 22:10:48 +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
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
let build: BuildConfig = table
|
|
|
|
.remove("build")
|
|
|
|
.and_then(|value| value.try_into().ok())
|
|
|
|
.unwrap_or_default();
|
2017-11-30 12:02:58 +08:00
|
|
|
|
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-18 21:22:30 +08:00
|
|
|
impl Serialize for Config {
|
|
|
|
fn serialize<S: Serializer>(&self, s: S) -> ::std::result::Result<S::Ok, S::Error> {
|
|
|
|
let mut table = self.rest.clone();
|
|
|
|
|
|
|
|
let book_config = match Value::try_from(self.book.clone()) {
|
|
|
|
Ok(cfg) => cfg,
|
|
|
|
Err(_) => {
|
|
|
|
use serde::ser::Error;
|
|
|
|
return Err(S::Error::custom("Unable to serialize the BookConfig"));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
table.insert("book".to_string(), book_config);
|
2018-01-07 22:10:48 +08:00
|
|
|
|
2017-11-18 21:22:30 +08:00
|
|
|
Value::Table(table).serialize(s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-12 21:00:18 +08:00
|
|
|
fn is_legacy_format(table: &Table) -> bool {
|
|
|
|
let top_level_items = ["title", "author", "authors"];
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
top_level_items
|
|
|
|
.iter()
|
|
|
|
.any(|key| table.contains_key(&key.to_string()))
|
2017-11-12 21:00:18 +08:00
|
|
|
}
|
|
|
|
|
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,
|
2018-01-07 22:10:48 +08:00
|
|
|
/// 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
|
|
|
|
/// `mdbook serve` command needs a way to let the HTML renderer know where
|
|
|
|
/// to point livereloading at, if it has been enabled.
|
|
|
|
///
|
|
|
|
/// This config item *should not be edited* by the end user.
|
|
|
|
#[doc(hidden)]
|
|
|
|
pub livereload_url: Option<String>,
|
2018-01-08 00:31:46 +08:00
|
|
|
pub no_section_label: bool,
|
2017-09-30 20:11:24 +08:00
|
|
|
}
|
|
|
|
|
2017-11-12 02:03:28 +08:00
|
|
|
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
2017-12-21 13:18:12 +08:00
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
#[serde(default, rename_all = "kebab-case")]
|
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
|
|
|
|
2017-12-21 13:18:12 +08:00
|
|
|
impl Default for Playpen {
|
|
|
|
fn default() -> Playpen {
|
|
|
|
Playpen {
|
|
|
|
editor: PathBuf::from("ace"),
|
|
|
|
editable: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
}
|
2018-01-07 22:10:48 +08:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn set_a_config_item() {
|
|
|
|
let mut cfg = Config::default();
|
|
|
|
let key = "foo.bar.baz";
|
|
|
|
let value = "Something Interesting";
|
|
|
|
|
|
|
|
assert!(cfg.get(key).is_none());
|
|
|
|
cfg.set(key, value).unwrap();
|
|
|
|
|
|
|
|
let got: String = cfg.get_deserialized(key).unwrap();
|
|
|
|
assert_eq!(got, value);
|
|
|
|
}
|
2017-09-30 20:34:27 +08:00
|
|
|
}
|