Merge pull request #457 from Michael-F-Bryan/config

Making configuration more flexible
This commit is contained in:
Michael Bryan 2017-11-12 21:47:10 +08:00 committed by GitHub
commit fb99276f52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 638 additions and 1253 deletions

View File

@ -2,14 +2,10 @@
You can configure the parameters for your book in the ***book.toml*** file. 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: Here is an example of what a ***book.toml*** file might look like:
```toml ```toml
[book]
title = "Example book" title = "Example book"
author = "John Doe" author = "John Doe"
description = "The example book covers examples." description = "The example book covers examples."
@ -24,66 +20,118 @@ additional-css = ["custom.css"]
It is important to note that **any** relative path specified in the in the configuration will 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. always be taken relative from the root of the book where the configuration file is located.
### General metadata ### General metadata
This is general information about your book.
- **title:** The title of the book - **title:** The title of the book
- **author:** The author 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 - **description:** A description for the book, which is added as meta
information in the html `<head>` of each page
**book.toml** - **src:** By default, the source directory is found in the directory named
```toml `src` directly under the root folder. But this is configurable with the `src`
title = "Example book" key in the configuration file.
author = "John Doe" - **build-dir:** The directory to put the rendered book in. By default this is
description = "The example book covers examples." `book/` in the book's root directory.
```
Some books may have multiple authors, there is an alternative key called `authors` plural that lets you specify an array
of authors.
**book.toml** **book.toml**
```toml ```toml
[book]
title = "Example book" title = "Example book"
authors = ["John Doe", "Jane Doe"] authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples." description = "The example book covers examples."
``` src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
build-dir = "build"
### 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.
**book.toml**
```toml
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`
``` ```
### HTML renderer options ### 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: 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 pub playpen: Playpen,
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. - **theme:** mdBook comes with a default theme and all the resource files
- **`curly-quotes`:** Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans. Defaults to `false`. needed for it. But if this option is set, mdBook will selectively overwrite
- **`google-analytics`:** If you use Google Analytics, this option lets you enable it by simply specifying your ID in the configuration file. the theme files with the ones found in the specified folder.
- **`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. - **curly-quotes:** Convert straight quotes to curly quotes, except for
- **`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. 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** **book.toml**
```toml ```toml
[book]
title = "Example book" title = "Example book"
authors = ["John Doe", "Jane Doe"] authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples." description = "The example book covers examples."
[output.html] [output.html]
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
theme = "my-theme" theme = "my-theme"
curly-quotes = true curly-quotes = true
google-analytics = "123456" google-analytics = "123456"
additional-css = ["custom.css", "custom2.css"] additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"] 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
```

View File

@ -1,4 +1,5 @@
use clap::{App, ArgMatches, SubCommand}; use std::path::PathBuf;
use clap::{ArgMatches, SubCommand, App};
use mdbook::MDBook; use mdbook::MDBook;
use mdbook::errors::Result; use mdbook::errors::Result;
use {get_book_dir, open}; use {get_book_dir, open};
@ -15,10 +16,6 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.arg_from_usage( .arg_from_usage(
"--no-create 'Will not create non-existent files linked from SUMMARY.md'", "--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( .arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory \ "[dir] 'A directory for your book{n}(Defaults to Current Directory \
when omitted)'", when omitted)'",
@ -28,21 +25,16 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
// Build command implementation // Build command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); 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") { if let Some(dest_dir) = args.value_of("dest-dir") {
Some(dest_dir) => book.with_destination(dest_dir), book.config.book.build_dir = PathBuf::from(dest_dir);
None => book, }
};
if args.is_present("no-create") { if args.is_present("no-create") {
book.create_missing = false; book.create_missing = false;
} }
if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
}
book.build()?; book.build()?;
if args.is_present("open") { if args.is_present("open") {

View File

@ -47,7 +47,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
} }
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root` // 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 { if !args.is_present("force") && is_dest_inside_root {
println!("\nDo you want a .gitignore to be created? (y/n)"); println!("\nDo you want a .gitignore to be created? (y/n)");

View File

@ -3,7 +3,7 @@ extern crate staticfile;
extern crate ws; extern crate ws;
use std; use std;
use std::path::Path; use std::path::PathBuf;
use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response,
Set}; Set};
use clap::{App, ArgMatches, SubCommand}; use clap::{App, ArgMatches, SubCommand};
@ -29,10 +29,6 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
"-d, --dest-dir=[dest-dir] 'The output directory for \ "-d, --dest-dir=[dest-dir] 'The output directory for \
your book{n}(Defaults to ./book when omitted)'", 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("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
.arg_from_usage( .arg_from_usage(
"-w, --websocket-port=[ws-port] 'Use another port for the \ "-w, --websocket-port=[ws-port] 'Use another port for the \
@ -53,15 +49,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
const RELOAD_COMMAND: &'static str = "reload"; const RELOAD_COMMAND: &'static str = "reload";
let book_dir = get_book_dir(args); 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") { if let Some(dest_dir) = args.value_of("dest-dir") {
Some(dest_dir) => book.with_destination(Path::new(dest_dir)), book.config.book.build_dir = PathBuf::from(dest_dir);
None => book,
};
if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
} }
let port = args.value_of("port").unwrap_or("3000"); let port = args.value_of("port").unwrap_or("3000");
@ -73,8 +64,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let address = format!("{}:{}", interface, port); let address = format!("{}:{}", interface, port);
let ws_address = format!("{}:{}", interface, ws_port); let ws_address = format!("{}:{}", interface, ws_port);
book.set_livereload(format!( book.livereload = Some(format!(r#"
r#"
<script type="text/javascript"> <script type="text/javascript">
var socket = new WebSocket("ws://{}:{}"); var socket = new WebSocket("ws://{}:{}");
socket.onmessage = function (event) {{ socket.onmessage = function (event) {{

View File

@ -1,6 +1,6 @@
extern crate notify; extern crate notify;
use std::path::Path; use std::path::{Path, PathBuf};
use self::notify::Watcher; use self::notify::Watcher;
use std::time::Duration; use std::time::Duration;
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
@ -18,10 +18,6 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
"-d, --dest-dir=[dest-dir] 'The output directory for \ "-d, --dest-dir=[dest-dir] 'The output directory for \
your book{n}(Defaults to ./book when omitted)'", 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( .arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to \ "[dir] 'A directory for your book{n}(Defaults to \
Current Directory when omitted)'", Current Directory when omitted)'",
@ -31,15 +27,10 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
// Watch command implementation // Watch command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); 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") { if let Some(dest_dir) = args.value_of("dest-dir") {
Some(dest_dir) => book.with_destination(dest_dir), book.config.book.build_dir = PathBuf::from(dest_dir);
None => book,
};
if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
} }
if args.is_present("open") { if args.is_present("open") {
@ -84,18 +75,17 @@ where
}; };
// Add the theme directory to the watcher // Add the theme directory to the watcher
watcher.watch(book.get_theme_path(), Recursive) watcher.watch(book.theme_dir(), Recursive)
.unwrap_or_default(); .unwrap_or_default();
// Add the book.{json,toml} file to the watcher if it exists, because it's not // Add the book.{json,toml} file to the watcher if it exists, because it's not
// located in the source directory // 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() .is_err()
{ {
// do nothing if book.json is not found // 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() .is_err()
{ {
// do nothing if book.toml is not found // do nothing if book.toml is not found

View File

@ -4,7 +4,7 @@ pub use self::bookitem::{BookItem, BookItems};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{Read, Write}; use std::io::Write;
use std::process::Command; use std::process::Command;
use tempdir::TempDir; use tempdir::TempDir;
@ -13,18 +13,16 @@ use renderer::{HtmlHandlebars, Renderer};
use preprocess; use preprocess;
use errors::*; use errors::*;
use config::BookConfig; use config::Config;
use config::tomlconfig::TomlConfig;
use config::htmlconfig::HtmlConfig;
use config::jsonconfig::JsonConfig;
pub struct MDBook { pub struct MDBook {
config: BookConfig, pub root: PathBuf,
pub config: Config,
pub content: Vec<BookItem>, pub content: Vec<BookItem>,
renderer: Box<Renderer>, renderer: Box<Renderer>,
livereload: Option<String>, pub livereload: Option<String>,
/// Should `mdbook build` create files referenced from SUMMARY.md if they /// Should `mdbook build` create files referenced from SUMMARY.md if they
/// don't exist /// don't exist
@ -66,7 +64,8 @@ impl MDBook {
} }
MDBook { MDBook {
config: BookConfig::new(root), root: root,
config: Config::default(),
content: vec![], content: vec![],
renderer: Box::new(HtmlHandlebars::new()), renderer: Box::new(HtmlHandlebars::new()),
@ -131,26 +130,26 @@ impl MDBook {
pub fn init(&mut self) -> Result<()> { pub fn init(&mut self) -> Result<()> {
debug!("[fn]: init"); debug!("[fn]: init");
if !self.config.get_root().exists() { if !self.root.exists() {
fs::create_dir_all(&self.config.get_root()).unwrap(); fs::create_dir_all(&self.root).unwrap();
info!("{:?} created", &self.config.get_root()); info!("{:?} created", self.root.display());
} }
{ {
if !self.get_destination().exists() { let dest = self.get_destination();
debug!("[*]: {:?} does not exist, trying to create directory", if !dest.exists() {
self.get_destination()); debug!("[*]: {} does not exist, trying to create directory", dest.display());
fs::create_dir_all(self.get_destination())?; fs::create_dir_all(dest)?;
} }
if !self.config.get_source().exists() { let src = self.get_source();
debug!("[*]: {:?} does not exist, trying to create directory", if !src.exists() {
self.config.get_source()); debug!("[*]: {} does not exist, trying to create directory", src.display());
fs::create_dir_all(self.config.get_source())?; fs::create_dir_all(&src)?;
} }
let summary = self.config.get_source().join("SUMMARY.md"); let summary = src.join("SUMMARY.md");
if !summary.exists() { if !summary.exists() {
// Summary does not exist, create it // Summary does not exist, create it
@ -177,12 +176,13 @@ impl MDBook {
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => ch, BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => ch,
}; };
if !ch.path.as_os_str().is_empty() { if !ch.path.as_os_str().is_empty() {
let path = self.config.get_source().join(&ch.path); let path = self.get_source().join(&ch.path);
if !path.exists() { if !path.exists() {
if !self.create_missing { if !self.create_missing {
return Err(format!("'{}' referenced from SUMMARY.md does not exist.", return Err(
path.to_string_lossy()).into()); format!("'{}' referenced from SUMMARY.md does not exist.", path.to_string_lossy()).into(),
);
} }
debug!("[*]: {:?} does not exist, trying to create file", path); debug!("[*]: {:?} does not exist, trying to create file", path);
::std::fs::create_dir_all(path.parent().unwrap())?; ::std::fs::create_dir_all(path.parent().unwrap())?;
@ -201,22 +201,22 @@ impl MDBook {
pub fn create_gitignore(&self) { pub fn create_gitignore(&self) {
let gitignore = self.get_gitignore(); let gitignore = self.get_gitignore();
let destination = self.config.get_html_config().get_destination(); let destination = self.get_destination();
// Check that the gitignore does not extist and // Check that the gitignore does not extist and that the destination path
// that the destination path begins with the root path // begins with the root path
// We assume tha if it does begin with the root path it is contained within. This assumption // We assume tha if it does begin with the root path it is contained within.
// will not hold true for paths containing double dots to go back up // This assumption
// e.g. `root/../destination` // will not hold true for paths containing double dots to go back up e.g.
if !gitignore.exists() && destination.starts_with(self.config.get_root()) { // `root/../destination`
let relative = destination.strip_prefix(self.config.get_root()) if !gitignore.exists() && destination.starts_with(&self.root) {
.expect("Could not strip the root prefix, path is not \ let relative = destination
relative to root") .strip_prefix(&self.root)
.to_str() .expect("Could not strip the root prefix, path is not relative to root")
.expect("Could not convert to &str"); .to_str()
.expect("Could not convert to &str");
debug!("[*]: {:?} does not exist, trying to create .gitignore", debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore);
gitignore);
let mut f = File::create(&gitignore).expect("Could not create file."); let mut f = File::create(&gitignore).expect("Could not create file.");
@ -238,20 +238,21 @@ impl MDBook {
self.init()?; self.init()?;
// Clean output directory // Clean output directory
utils::fs::remove_dir_content(self.config.get_html_config().get_destination())?; utils::fs::remove_dir_content(&self.get_destination())?;
self.renderer.render(self) self.renderer.render(self)
} }
pub fn get_gitignore(&self) -> PathBuf { pub fn get_gitignore(&self) -> PathBuf {
self.config.get_root().join(".gitignore") self.root.join(".gitignore")
} }
pub fn copy_theme(&self) -> Result<()> { pub fn copy_theme(&self) -> Result<()> {
debug!("[fn]: copy_theme"); debug!("[fn]: copy_theme");
let themedir = self.config.get_html_config().get_theme(); let themedir = self.theme_dir();
if !themedir.exists() { if !themedir.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", debug!("[*]: {:?} does not exist, trying to create directory",
themedir); themedir);
@ -259,27 +260,27 @@ impl MDBook {
} }
// index.hbs // index.hbs
let mut index = File::create(&themedir.join("index.hbs"))?; let mut index = File::create(themedir.join("index.hbs"))?;
index.write_all(theme::INDEX)?; index.write_all(theme::INDEX)?;
// book.css // book.css
let mut css = File::create(&themedir.join("book.css"))?; let mut css = File::create(themedir.join("book.css"))?;
css.write_all(theme::CSS)?; css.write_all(theme::CSS)?;
// favicon.png // favicon.png
let mut favicon = File::create(&themedir.join("favicon.png"))?; let mut favicon = File::create(themedir.join("favicon.png"))?;
favicon.write_all(theme::FAVICON)?; favicon.write_all(theme::FAVICON)?;
// book.js // book.js
let mut js = File::create(&themedir.join("book.js"))?; let mut js = File::create(themedir.join("book.js"))?;
js.write_all(theme::JS)?; js.write_all(theme::JS)?;
// highlight.css // highlight.css
let mut highlight_css = File::create(&themedir.join("highlight.css"))?; let mut highlight_css = File::create(themedir.join("highlight.css"))?;
highlight_css.write_all(theme::HIGHLIGHT_CSS)?; highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
// highlight.js // highlight.js
let mut highlight_js = File::create(&themedir.join("highlight.js"))?; let mut highlight_js = File::create(themedir.join("highlight.js"))?;
highlight_js.write_all(theme::HIGHLIGHT_JS)?; highlight_js.write_all(theme::HIGHLIGHT_JS)?;
Ok(()) Ok(())
@ -298,25 +299,9 @@ impl MDBook {
/// The root directory is the one specified when creating a new `MDBook` /// The root directory is the one specified when creating a new `MDBook`
pub fn read_config(mut self) -> Result<Self> { pub fn read_config(mut self) -> Result<Self> {
let toml = self.get_root().join("book.toml"); let config_path = self.root.join("book.toml");
let json = self.get_root().join("book.json"); debug!("[*] Loading the config from {}", config_path.display());
self.config = Config::from_disk(&config_path)?;
if toml.exists() {
let mut file = File::open(toml)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let parsed_config = TomlConfig::from_toml(&content)?;
self.config.fill_from_tomlconfig(parsed_config);
} else if json.exists() {
warn!("The JSON configuration file is deprecated, please use the TOML configuration.");
let mut file = File::open(json)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let parsed_config = JsonConfig::from_json(&content)?;
self.config.fill_from_jsonconfig(parsed_config);
}
Ok(self) Ok(self)
} }
@ -353,10 +338,11 @@ impl MDBook {
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> { pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
// read in the chapters // read in the chapters
self.parse_summary().chain_err(|| "Couldn't parse summary")?; self.parse_summary().chain_err(|| "Couldn't parse summary")?;
let library_args: Vec<&str> = (0..library_paths.len()).map(|_| "-L") let library_args: Vec<&str> = (0..library_paths.len())
.zip(library_paths.into_iter()) .map(|_| "-L")
.flat_map(|x| vec![x.0, x.1]) .zip(library_paths.into_iter())
.collect(); .flat_map(|x| vec![x.0, x.1])
.collect();
let temp_dir = TempDir::new("mdbook")?; let temp_dir = TempDir::new("mdbook")?;
for item in self.iter() { for item in self.iter() {
if let BookItem::Chapter(_, ref ch) = *item { if let BookItem::Chapter(_, ref ch) = *item {
@ -369,7 +355,7 @@ impl MDBook {
let content = preprocess::links::replace_all(&content, base)?; let content = preprocess::links::replace_all(&content, base)?;
println!("[*]: Testing file: {:?}", path); println!("[*]: Testing file: {:?}", path);
//write preprocessed file to tempdir // write preprocessed file to tempdir
let path = temp_dir.path().join(&ch.path); let path = temp_dir.path().join(&ch.path);
let mut tmpf = utils::fs::create_file(&path)?; let mut tmpf = utils::fs::create_file(&path)?;
tmpf.write_all(content.as_bytes())?; tmpf.write_all(content.as_bytes())?;
@ -389,127 +375,26 @@ impl MDBook {
Ok(()) Ok(())
} }
pub fn get_root(&self) -> &Path {
self.config.get_root()
}
pub fn with_destination<T: Into<PathBuf>>(mut self, destination: T) -> Self {
let root = self.config.get_root().to_owned();
self.config
.get_mut_html_config()
.set_destination(&root, &destination.into());
self
}
pub fn get_destination(&self) -> &Path {
self.config.get_html_config().get_destination()
}
pub fn with_source<T: Into<PathBuf>>(mut self, source: T) -> Self {
self.config.set_source(source);
self
}
pub fn get_source(&self) -> &Path {
self.config.get_source()
}
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
self.config.set_title(title);
self
}
pub fn get_title(&self) -> &str {
self.config.get_title()
}
pub fn with_description<T: Into<String>>(mut self, description: T) -> Self {
self.config.set_description(description);
self
}
pub fn get_description(&self) -> &str {
self.config.get_description()
}
pub fn set_livereload(&mut self, livereload: String) -> &mut Self {
self.livereload = Some(livereload);
self
}
pub fn unset_livereload(&mut self) -> &Self {
self.livereload = None;
self
}
pub fn get_livereload(&self) -> Option<&String> {
self.livereload.as_ref()
}
pub fn with_theme_path<T: Into<PathBuf>>(mut self, theme_path: T) -> Self {
let root = self.config.get_root().to_owned();
self.config
.get_mut_html_config()
.set_theme(&root, &theme_path.into());
self
}
pub fn get_theme_path(&self) -> &Path {
self.config.get_html_config().get_theme()
}
pub fn with_curly_quotes(mut self, curly_quotes: bool) -> Self {
self.config
.get_mut_html_config()
.set_curly_quotes(curly_quotes);
self
}
pub fn get_curly_quotes(&self) -> bool {
self.config.get_html_config().get_curly_quotes()
}
pub fn with_mathjax_support(mut self, mathjax_support: bool) -> Self {
self.config
.get_mut_html_config()
.set_mathjax_support(mathjax_support);
self
}
pub fn get_mathjax_support(&self) -> bool {
self.config.get_html_config().get_mathjax_support()
}
pub fn get_google_analytics_id(&self) -> Option<String> {
self.config.get_html_config().get_google_analytics_id()
}
pub fn has_additional_js(&self) -> bool {
self.config.get_html_config().has_additional_js()
}
pub fn get_additional_js(&self) -> &[PathBuf] {
self.config.get_html_config().get_additional_js()
}
pub fn has_additional_css(&self) -> bool {
self.config.get_html_config().has_additional_css()
}
pub fn get_additional_css(&self) -> &[PathBuf] {
self.config.get_html_config().get_additional_css()
}
pub fn get_html_config(&self) -> &HtmlConfig {
self.config.get_html_config()
}
// Construct book // Construct book
fn parse_summary(&mut self) -> Result<()> { fn parse_summary(&mut self) -> Result<()> {
// When append becomes stable, use self.content.append() ... // When append becomes stable, use self.content.append() ...
self.content = parse::construct_bookitems(&self.get_source().join("SUMMARY.md"))?; let summary = self.get_source().join("SUMMARY.md");
self.content = parse::construct_bookitems(&summary)?;
Ok(()) Ok(())
} }
pub fn get_destination(&self) -> PathBuf {
self.root.join(&self.config.book.build_dir)
}
pub fn get_source(&self) -> PathBuf {
self.root.join(&self.config.book.src)
}
pub fn theme_dir(&self) -> PathBuf {
match self.config.html_config().and_then(|h| h.theme) {
Some(d) => self.root.join(d),
None => self.root.join("theme"),
}
}
} }

387
src/config.rs Normal file
View File

@ -0,0 +1,387 @@
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Read;
use toml::{self, Value};
use toml::value::Table;
use serde::{Deserialize, Deserializer};
use errors::*;
/// The overall configuration object for MDBook.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Config {
/// Metadata about the book.
pub book: BookConfig,
rest: Table,
}
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();
File::open(config_file).chain_err(|| "Unable to open the configuration file")?
.read_to_string(&mut buffer)
.chain_err(|| "Couldn't read the file")?;
Config::from_str(&buffer)
}
/// 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)
}
/// 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.
pub fn html_config(&self) -> Option<HtmlConfig> {
self.get_deserialized("output.html").ok()
}
/// 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)
}
}
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
// "output.html.destination" to "book.build_dir" and parse it into a
// 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 {
cfg.book.build_dir = dest;
}
cfg.rest = table;
cfg
}
}
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]);
}
let first = key[0];
let rest = &key[1..];
if let Some(&Value::Table(ref nested)) = table.get(first) {
recursive_get(rest, nested)
} else {
None
}
}
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)?;
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.");
warn!("See the mdbook documentation for more details, although as a rule of thumb");
warn!("just move all top level configuration entries like `title`, `author` and ");
warn!("`description` under a table called `[book]` and it should all work.");
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
return Ok(Config::from_legacy(table));
}
let book: BookConfig = table.remove("book")
.and_then(|value| value.try_into().ok())
.unwrap_or_default();
Ok(Config {
book: book,
rest: table,
})
}
}
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()))
}
/// Configuration options which are specific to the book and required for
/// loading it from disk.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct BookConfig {
/// 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>,
/// Location of the book source relative to the book's root directory.
pub src: PathBuf,
/// Where to put built artefacts relative to the book's root directory.
pub build_dir: PathBuf,
/// Does this book support more than one language?
pub multilingual: bool,
}
impl Default for BookConfig {
fn default() -> BookConfig {
BookConfig {
title: None,
authors: Vec::new(),
description: None,
src: PathBuf::from("src"),
build_dir: PathBuf::from("book"),
multilingual: false,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct HtmlConfig {
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,
}
/// Configuration for tweaking how the the HTML renderer handles the playpen.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Playpen {
pub editor: PathBuf,
pub editable: bool,
}
#[cfg(test)]
mod tests {
use super::*;
const COMPLEX_CONFIG: &'static str = r#"
[book]
title = "Some Book"
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
description = "A completely useless book"
multilingual = true
src = "source"
build-dir = "outputs"
[output.html]
theme = "./themedir"
curly-quotes = true
google-analytics = "123456"
additional-css = ["./foo/bar/baz.css"]
[output.html.playpen]
editable = true
editor = "ace"
"#;
#[test]
fn load_a_complex_config_file() {
let src = COMPLEX_CONFIG;
let book_should_be = BookConfig {
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"),
build_dir: PathBuf::from("outputs"),
..Default::default()
};
let playpen_should_be = Playpen {
editable: true,
editor: PathBuf::from("ace"),
};
let html_should_be = HtmlConfig {
curly_quotes: true,
google_analytics: Some(String::from("123456")),
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
theme: Some(PathBuf::from("./themedir")),
playpen: playpen_should_be,
..Default::default()
};
let got = Config::from_str(src).unwrap();
assert_eq!(got.book, book_should_be);
assert_eq!(got.html_config().unwrap(), html_should_be);
}
#[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();
let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
assert_eq!(got, should_be);
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);
}
#[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")],
build_dir: PathBuf::from("my-book"),
src: PathBuf::from("./source"),
..Default::default()
};
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);
assert_eq!(got.html_config().unwrap(), html_should_be);
}
}

View File

@ -1,226 +0,0 @@
use std::path::{Path, PathBuf};
use super::HtmlConfig;
use super::tomlconfig::TomlConfig;
use super::jsonconfig::JsonConfig;
/// Configuration struct containing all the configuration options available in mdBook.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BookConfig {
root: PathBuf,
source: PathBuf,
title: String,
authors: Vec<String>,
description: String,
multilingual: bool,
indent_spaces: i32,
html_config: HtmlConfig,
}
impl BookConfig {
/// Creates a new `BookConfig` struct with as root path the path given as parameter.
/// The source directory is `root/src` and the destination for the rendered book is `root/book`.
///
/// ```
/// # use std::path::PathBuf;
/// # use mdbook::config::{BookConfig, HtmlConfig};
/// #
/// let root = PathBuf::from("directory/to/my/book");
/// let config = BookConfig::new(&root);
///
/// assert_eq!(config.get_root(), &root);
/// assert_eq!(config.get_source(), PathBuf::from("directory/to/my/book/src"));
/// assert_eq!(config.get_html_config(),
/// &HtmlConfig::new(PathBuf::from("directory/to/my/book")));
/// ```
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
let root: PathBuf = root.into();
let htmlconfig = HtmlConfig::new(&root);
BookConfig {
root: root.clone(),
source: root.join("src"),
title: String::new(),
authors: Vec::new(),
description: String::new(),
multilingual: false,
indent_spaces: 4,
html_config: htmlconfig,
}
}
/// Builder method to set the source directory
pub fn with_source<T: Into<PathBuf>>(mut self, source: T) -> Self {
self.source = source.into();
self
}
/// Builder method to set the book's title
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
self.title = title.into();
self
}
/// Builder method to set the book's description
pub fn with_description<T: Into<String>>(mut self, description: T) -> Self {
self.description = description.into();
self
}
/// Builder method to set the book's authors
pub fn with_authors<T: Into<Vec<String>>>(mut self, authors: T) -> Self {
self.authors = authors.into();
self
}
pub fn from_tomlconfig<T: Into<PathBuf>>(root: T, tomlconfig: TomlConfig) -> Self {
let root = root.into();
let mut config = BookConfig::new(&root);
config.fill_from_tomlconfig(tomlconfig);
config
}
pub fn fill_from_tomlconfig(&mut self, tomlconfig: TomlConfig) -> &mut Self {
if let Some(s) = tomlconfig.source {
self.set_source(s);
}
if let Some(t) = tomlconfig.title {
self.set_title(t);
}
if let Some(d) = tomlconfig.description {
self.set_description(d);
}
if let Some(a) = tomlconfig.authors {
self.set_authors(a);
}
if let Some(a) = tomlconfig.author {
self.set_authors(vec![a]);
}
if let Some(tomlhtmlconfig) = tomlconfig.output.and_then(|o| o.html) {
let root = self.root.clone();
self.get_mut_html_config()
.fill_from_tomlconfig(root, tomlhtmlconfig);
}
self
}
/// The JSON configuration file is **deprecated** and should not be used anymore.
/// Please, migrate to the TOML configuration file.
pub fn from_jsonconfig<T: Into<PathBuf>>(root: T, jsonconfig: JsonConfig) -> Self {
let root = root.into();
let mut config = BookConfig::new(&root);
config.fill_from_jsonconfig(jsonconfig);
config
}
/// The JSON configuration file is **deprecated** and should not be used anymore.
/// Please, migrate to the TOML configuration file.
pub fn fill_from_jsonconfig(&mut self, jsonconfig: JsonConfig) -> &mut Self {
if let Some(s) = jsonconfig.src {
self.set_source(s);
}
if let Some(t) = jsonconfig.title {
self.set_title(t);
}
if let Some(d) = jsonconfig.description {
self.set_description(d);
}
if let Some(a) = jsonconfig.author {
self.set_authors(vec![a]);
}
if let Some(d) = jsonconfig.dest {
let root = self.get_root().to_owned();
self.get_mut_html_config().set_destination(&root, &d);
}
if let Some(d) = jsonconfig.theme_path {
let root = self.get_root().to_owned();
self.get_mut_html_config().set_theme(&root, &d);
}
self
}
pub fn set_root<T: Into<PathBuf>>(&mut self, root: T) -> &mut Self {
self.root = root.into();
self
}
pub fn get_root(&self) -> &Path {
&self.root
}
pub fn set_source<T: Into<PathBuf>>(&mut self, source: T) -> &mut Self {
let mut source = source.into();
// If the source path is relative, start with the root path
if source.is_relative() {
source = self.root.join(source);
}
self.source = source;
self
}
pub fn get_source(&self) -> &Path {
&self.source
}
pub fn set_title<T: Into<String>>(&mut self, title: T) -> &mut Self {
self.title = title.into();
self
}
pub fn get_title(&self) -> &str {
&self.title
}
pub fn set_description<T: Into<String>>(&mut self, description: T) -> &mut Self {
self.description = description.into();
self
}
pub fn get_description(&self) -> &str {
&self.description
}
pub fn set_authors<T: Into<Vec<String>>>(&mut self, authors: T) -> &mut Self {
self.authors = authors.into();
self
}
/// Returns the authors of the book as specified in the configuration file
pub fn get_authors(&self) -> &[String] {
self.authors.as_slice()
}
pub fn set_html_config(&mut self, htmlconfig: HtmlConfig) -> &mut Self {
self.html_config = htmlconfig;
self
}
/// Returns the configuration for the HTML renderer or None of there isn't any
pub fn get_html_config(&self) -> &HtmlConfig {
&self.html_config
}
pub fn get_mut_html_config(&mut self) -> &mut HtmlConfig {
&mut self.html_config
}
}

View File

@ -1,177 +0,0 @@
use std::path::{Path, PathBuf};
use super::tomlconfig::TomlHtmlConfig;
use super::playpenconfig::PlaypenConfig;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HtmlConfig {
destination: PathBuf,
theme: PathBuf,
curly_quotes: bool,
mathjax_support: bool,
google_analytics: Option<String>,
additional_css: Vec<PathBuf>,
additional_js: Vec<PathBuf>,
playpen: PlaypenConfig,
}
impl HtmlConfig {
/// Creates a new `HtmlConfig` struct containing
/// the configuration parameters for the HTML renderer.
///
/// ```
/// # use std::path::PathBuf;
/// # use mdbook::config::HtmlConfig;
/// #
/// let output = PathBuf::from("root/book");
/// let config = HtmlConfig::new(PathBuf::from("root"));
///
/// assert_eq!(config.get_destination(), &output);
/// ```
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
let root = root.into();
let theme = root.join("theme");
HtmlConfig {
destination: root.clone().join("book"),
theme: theme.clone(),
curly_quotes: false,
mathjax_support: false,
google_analytics: None,
additional_css: Vec::new(),
additional_js: Vec::new(),
playpen: PlaypenConfig::new(theme),
}
}
pub fn fill_from_tomlconfig<T: Into<PathBuf>>(&mut self,
root: T,
tomlconfig: TomlHtmlConfig)
-> &mut Self {
let root = root.into();
if let Some(d) = tomlconfig.destination {
self.set_destination(&root, &d);
}
if let Some(t) = tomlconfig.theme {
self.set_theme(&root, &t);
}
if let Some(curly_quotes) = tomlconfig.curly_quotes {
self.curly_quotes = curly_quotes;
}
if let Some(mathjax_support) = tomlconfig.mathjax_support {
self.mathjax_support = mathjax_support;
}
if tomlconfig.google_analytics.is_some() {
self.google_analytics = tomlconfig.google_analytics;
}
if let Some(stylepaths) = tomlconfig.additional_css {
for path in stylepaths {
if path.is_relative() {
self.additional_css.push(root.join(path));
} else {
self.additional_css.push(path);
}
}
}
if let Some(scriptpaths) = tomlconfig.additional_js {
for path in scriptpaths {
if path.is_relative() {
self.additional_js.push(root.join(path));
} else {
self.additional_js.push(path);
}
}
}
if let Some(playpen) = tomlconfig.playpen {
self.playpen.fill_from_tomlconfig(&self.theme, playpen);
}
self
}
pub fn set_destination<T: Into<PathBuf>>(&mut self, root: T, destination: T) -> &mut Self {
let d = destination.into();
if d.is_relative() {
self.destination = root.into().join(d);
} else {
self.destination = d;
}
self
}
pub fn get_destination(&self) -> &Path {
&self.destination
}
pub fn get_theme(&self) -> &Path {
&self.theme
}
pub fn set_theme<T: Into<PathBuf>>(&mut self, root: T, theme: T) -> &mut Self {
let d = theme.into();
if d.is_relative() {
self.theme = root.into().join(d);
} else {
self.theme = d;
}
self
}
pub fn get_curly_quotes(&self) -> bool {
self.curly_quotes
}
pub fn set_curly_quotes(&mut self, curly_quotes: bool) {
self.curly_quotes = curly_quotes;
}
pub fn get_mathjax_support(&self) -> bool {
self.mathjax_support
}
pub fn set_mathjax_support(&mut self, mathjax_support: bool) {
self.mathjax_support = mathjax_support;
}
pub fn get_google_analytics_id(&self) -> Option<String> {
self.google_analytics.clone()
}
pub fn set_google_analytics_id(&mut self, id: Option<String>) -> &mut Self {
self.google_analytics = id;
self
}
pub fn has_additional_css(&self) -> bool {
!self.additional_css.is_empty()
}
pub fn get_additional_css(&self) -> &[PathBuf] {
&self.additional_css
}
pub fn has_additional_js(&self) -> bool {
!self.additional_js.is_empty()
}
pub fn get_additional_js(&self) -> &[PathBuf] {
&self.additional_js
}
pub fn get_playpen_config(&self) -> &PlaypenConfig {
&self.playpen
}
pub fn get_mut_playpen_config(&mut self) -> &mut PlaypenConfig {
&mut self.playpen
}
}

View File

@ -1,41 +0,0 @@
extern crate serde_json;
use std::path::PathBuf;
use errors::*;
/// The JSON configuration is **deprecated** and will be removed in the near future.
/// Please migrate to the TOML configuration.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct JsonConfig {
pub src: Option<PathBuf>,
pub dest: Option<PathBuf>,
pub title: Option<String>,
pub author: Option<String>,
pub description: Option<String>,
pub theme_path: Option<PathBuf>,
pub google_analytics: Option<String>,
}
/// Returns a `JsonConfig` from a JSON string
///
/// ```
/// # use mdbook::config::jsonconfig::JsonConfig;
/// # use std::path::PathBuf;
/// let json = r#"{
/// "title": "Some title",
/// "dest": "htmlbook"
/// }"#;
///
/// let config = JsonConfig::from_json(&json).expect("Should parse correctly");
/// assert_eq!(config.title, Some(String::from("Some title")));
/// assert_eq!(config.dest, Some(PathBuf::from("htmlbook")));
/// ```
impl JsonConfig {
pub fn from_json(input: &str) -> Result<Self> {
let config: JsonConfig = serde_json::from_str(input).chain_err(|| "Could not parse JSON")?;
Ok(config)
}
}

View File

@ -1,11 +0,0 @@
pub mod bookconfig;
pub mod htmlconfig;
pub mod playpenconfig;
pub mod tomlconfig;
pub mod jsonconfig;
// Re-export the config structs
pub use self::bookconfig::BookConfig;
pub use self::htmlconfig::HtmlConfig;
pub use self::playpenconfig::PlaypenConfig;
pub use self::tomlconfig::TomlConfig;

View File

@ -1,71 +0,0 @@
use std::path::{Path, PathBuf};
use super::tomlconfig::TomlPlaypenConfig;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PlaypenConfig {
editor: PathBuf,
editable: bool,
}
impl PlaypenConfig {
/// Creates a new `PlaypenConfig` for playpen configuration.
///
/// ```
/// # use std::path::PathBuf;
/// # use mdbook::config::PlaypenConfig;
/// #
/// let editor = PathBuf::from("root/editor");
/// let config = PlaypenConfig::new(PathBuf::from("root"));
///
/// assert_eq!(config.get_editor(), &editor);
/// assert_eq!(config.is_editable(), false);
/// ```
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
PlaypenConfig {
editor: root.into().join("editor"),
editable: false,
}
}
pub fn fill_from_tomlconfig<T: Into<PathBuf>>(&mut self,
root: T,
tomlplaypenconfig: TomlPlaypenConfig)
-> &mut Self {
let root = root.into();
if let Some(editor) = tomlplaypenconfig.editor {
if editor.is_relative() {
self.editor = root.join(editor);
} else {
self.editor = editor;
}
}
if let Some(editable) = tomlplaypenconfig.editable {
self.editable = editable;
}
self
}
pub fn is_editable(&self) -> bool {
self.editable
}
pub fn get_editor(&self) -> &Path {
&self.editor
}
pub fn set_editor<T: Into<PathBuf>>(&mut self, root: T, editor: T) -> &mut Self {
let editor = editor.into();
if editor.is_relative() {
self.editor = root.into().join(editor);
} else {
self.editor = editor;
}
self
}
}

View File

@ -1,60 +0,0 @@
extern crate toml;
use std::path::PathBuf;
use errors::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TomlConfig {
pub source: Option<PathBuf>,
pub title: Option<String>,
pub author: Option<String>,
pub authors: Option<Vec<String>>,
pub description: Option<String>,
pub output: Option<TomlOutputConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TomlOutputConfig {
pub html: Option<TomlHtmlConfig>,
}
#[serde(rename_all = "kebab-case")]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TomlHtmlConfig {
pub destination: Option<PathBuf>,
pub theme: Option<PathBuf>,
pub google_analytics: Option<String>,
pub curly_quotes: Option<bool>,
pub mathjax_support: Option<bool>,
pub additional_css: Option<Vec<PathBuf>>,
pub additional_js: Option<Vec<PathBuf>>,
pub playpen: Option<TomlPlaypenConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TomlPlaypenConfig {
pub editor: Option<PathBuf>,
pub editable: Option<bool>,
}
/// Returns a `TomlConfig` from a TOML string
///
/// ```
/// # use mdbook::config::tomlconfig::TomlConfig;
/// # use std::path::PathBuf;
/// let toml = r#"title="Some title"
/// [output.html]
/// destination = "htmlbook" "#;
///
/// let config = TomlConfig::from_toml(&toml).expect("Should parse correctly");
/// assert_eq!(config.title, Some(String::from("Some title")));
/// assert_eq!(config.output.unwrap().html.unwrap().destination, Some(PathBuf::from("htmlbook")));
/// ```
impl TomlConfig {
pub fn from_toml(input: &str) -> Result<Self> {
let config: TomlConfig = toml::from_str(input).chain_err(|| "Could not parse TOML")?;
Ok(config)
}
}

View File

@ -21,16 +21,17 @@
//! extern crate mdbook; //! extern crate mdbook;
//! //!
//! use mdbook::MDBook; //! use mdbook::MDBook;
//! use std::path::PathBuf;
//! //!
//! # #[allow(unused_variables)]
//! fn main() { //! fn main() {
//! let mut book = MDBook::new("my-book") // Path to root //! let mut md = MDBook::new("my-book");
//! .with_source("src") // Path from root to source directory
//! .with_destination("book") // Path from root to output directory
//! .read_config() // Parse book.toml configuration file
//! .expect("I don't handle configuration file errors, but you should!");
//! //!
//! book.build().unwrap(); // Render the book //! // tweak the book configuration a bit
//! md.config.book.src = PathBuf::from("source");
//! md.config.book.build_dir = PathBuf::from("book");
//!
//! // Render the book
//! md.build().unwrap();
//! } //! }
//! ``` //! ```
//! //!
@ -86,6 +87,7 @@ extern crate serde_derive;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
extern crate tempdir; extern crate tempdir;
extern crate toml;
mod parse; mod parse;
mod preprocess; mod preprocess;

View File

@ -3,9 +3,9 @@ use preprocess;
use renderer::Renderer; use renderer::Renderer;
use book::MDBook; use book::MDBook;
use book::bookitem::{BookItem, Chapter}; use book::bookitem::{BookItem, Chapter};
use config::PlaypenConfig; use config::{Config, Playpen, HtmlConfig};
use {theme, utils}; use {utils, theme};
use theme::{playpen_editor, Theme}; use theme::{Theme, playpen_editor};
use errors::*; use errors::*;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
@ -45,7 +45,7 @@ impl HtmlHandlebars {
// Parse and expand links // Parse and expand links
let content = preprocess::links::replace_all(&content, base)?; let content = preprocess::links::replace_all(&content, base)?;
let content = utils::render_markdown(&content, ctx.book.get_curly_quotes()); let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
print_content.push_str(&content); print_content.push_str(&content);
// Update the context with data for this file // Update the context with data for this file
@ -82,7 +82,7 @@ impl HtmlHandlebars {
&normalize_path(filepath.to_str().ok_or_else(|| Error::from( &normalize_path(filepath.to_str().ok_or_else(|| Error::from(
format!("Bad file name: {}", filepath.display()), format!("Bad file name: {}", filepath.display()),
))?), ))?),
ctx.book.get_html_config().get_playpen_config(), &ctx.book.config.html_config().unwrap_or_default().playpen,
); );
// Write to file // Write to file
@ -128,7 +128,7 @@ impl HtmlHandlebars {
fn post_process(&self, fn post_process(&self,
rendered: String, rendered: String,
filepath: &str, filepath: &str,
playpen_config: &PlaypenConfig) playpen_config: &Playpen)
-> String { -> String {
let rendered = build_header_links(&rendered, filepath); let rendered = build_header_links(&rendered, filepath);
let rendered = fix_anchor_links(&rendered, filepath); let rendered = fix_anchor_links(&rendered, filepath);
@ -138,7 +138,7 @@ impl HtmlHandlebars {
rendered rendered
} }
fn copy_static_files(&self, book: &MDBook, theme: &Theme) -> Result<()> { fn copy_static_files(&self, book: &MDBook, theme: &Theme, html_config: &HtmlConfig) -> Result<()> {
book.write_file("book.js", &theme.js)?; book.write_file("book.js", &theme.js)?;
book.write_file("book.css", &theme.css)?; book.write_file("book.css", &theme.css)?;
book.write_file("favicon.png", &theme.favicon)?; book.write_file("favicon.png", &theme.favicon)?;
@ -163,12 +163,12 @@ impl HtmlHandlebars {
book.write_file("_FontAwesome/fonts/FontAwesome.ttf", book.write_file("_FontAwesome/fonts/FontAwesome.ttf",
theme::FONT_AWESOME_TTF)?; theme::FONT_AWESOME_TTF)?;
let playpen_config = book.get_html_config().get_playpen_config(); let playpen_config = &html_config.playpen;
// Ace is a very large dependency, so only load it when requested // Ace is a very large dependency, so only load it when requested
if playpen_config.is_editable() { if playpen_config.editable {
// Load the editor // Load the editor
let editor = playpen_editor::PlaypenEditor::new(playpen_config.get_editor()); let editor = playpen_editor::PlaypenEditor::new(&playpen_config.editor);
book.write_file("editor.js", &editor.js)?; book.write_file("editor.js", &editor.js)?;
book.write_file("ace.js", &editor.ace_js)?; book.write_file("ace.js", &editor.ace_js)?;
book.write_file("mode-rust.js", &editor.mode_rust_js)?; book.write_file("mode-rust.js", &editor.mode_rust_js)?;
@ -186,7 +186,7 @@ impl HtmlHandlebars {
let mut f = File::open(custom_file)?; let mut f = File::open(custom_file)?;
f.read_to_end(&mut data)?; f.read_to_end(&mut data)?;
let name = match custom_file.strip_prefix(book.get_root()) { let name = match custom_file.strip_prefix(&book.root) {
Ok(p) => p.to_str().expect("Could not convert to str"), Ok(p) => p.to_str().expect("Could not convert to str"),
Err(_) => { Err(_) => {
custom_file.file_name() custom_file.file_name()
@ -224,9 +224,11 @@ impl HtmlHandlebars {
/// Copy across any additional CSS and JavaScript files which the book /// Copy across any additional CSS and JavaScript files which the book
/// has been configured to use. /// has been configured to use.
fn copy_additional_css_and_js(&self, book: &MDBook) -> Result<()> { fn copy_additional_css_and_js(&self, book: &MDBook) -> Result<()> {
let custom_files = book.get_additional_css() let html = book.config.html_config().unwrap_or_default();
let custom_files = html.additional_css
.iter() .iter()
.chain(book.get_additional_js().iter()); .chain(html.additional_js.iter());
for custom_file in custom_files { for custom_file in custom_files {
self.write_custom_file(custom_file, book)?; self.write_custom_file(custom_file, book)?;
@ -239,10 +241,17 @@ impl HtmlHandlebars {
impl Renderer for HtmlHandlebars { impl Renderer for HtmlHandlebars {
fn render(&self, book: &MDBook) -> Result<()> { fn render(&self, book: &MDBook) -> Result<()> {
let html_config = book.config.html_config().unwrap_or_default();
debug!("[fn]: render"); debug!("[fn]: render");
let mut handlebars = Handlebars::new(); let mut handlebars = Handlebars::new();
let theme = theme::Theme::new(book.get_theme_path()); let theme_dir = match html_config.theme {
Some(ref theme) => theme,
None => Path::new("theme"),
};
let theme = theme::Theme::new(theme_dir);
debug!("[*]: Register handlebars template"); debug!("[*]: Register handlebars template");
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?; handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
@ -250,17 +259,17 @@ impl Renderer for HtmlHandlebars {
debug!("[*]: Register handlebars helpers"); debug!("[*]: Register handlebars helpers");
self.register_hbs_helpers(&mut handlebars); self.register_hbs_helpers(&mut handlebars);
let mut data = make_data(book)?; let mut data = make_data(book, &book.config)?;
// Print version // Print version
let mut print_content = String::new(); let mut print_content = String::new();
// TODO: The Renderer trait should really pass in where it wants us to build to...
let destination = book.get_destination(); let destination = book.get_destination();
debug!("[*]: Check if destination directory exists"); debug!("[*]: Check if destination directory exists");
if fs::create_dir_all(&destination).is_err() { fs::create_dir_all(&destination)
bail!("Unexpected error when constructing destination path"); .chain_err(|| "Unexpected error when constructing destination path")?;
}
for (i, item) in book.iter().enumerate() { for (i, item) in book.iter().enumerate() {
let ctx = RenderItemContext { let ctx = RenderItemContext {
@ -269,13 +278,16 @@ impl Renderer for HtmlHandlebars {
destination: destination.to_path_buf(), destination: destination.to_path_buf(),
data: data.clone(), data: data.clone(),
is_index: i == 0, is_index: i == 0,
html_config: html_config.clone(),
}; };
self.render_item(item, ctx, &mut print_content)?; self.render_item(item, ctx, &mut print_content)?;
} }
// Print version // Print version
self.configure_print_version(&mut data, &print_content); self.configure_print_version(&mut data, &print_content);
data.insert("title".to_owned(), json!(book.get_title())); if let Some(ref title) = book.config.book.title {
data.insert("title".to_owned(), json!(title));
}
// Render the handlebars template with the data // Render the handlebars template with the data
debug!("[*]: Render template"); debug!("[*]: Render template");
@ -284,7 +296,7 @@ impl Renderer for HtmlHandlebars {
let rendered = self.post_process(rendered, let rendered = self.post_process(rendered,
"print.html", "print.html",
book.get_html_config().get_playpen_config()); &html_config.playpen);
book.write_file(Path::new("print").with_extension("html"), book.write_file(Path::new("print").with_extension("html"),
&rendered.into_bytes())?; &rendered.into_bytes())?;
@ -292,42 +304,44 @@ impl Renderer for HtmlHandlebars {
// Copy static files (js, css, images, ...) // Copy static files (js, css, images, ...)
debug!("[*] Copy static files"); debug!("[*] Copy static files");
self.copy_static_files(book, &theme)?; self.copy_static_files(book, &theme, &html_config)?;
self.copy_additional_css_and_js(book)?; self.copy_additional_css_and_js(book)?;
// Copy all remaining files // Copy all remaining files
utils::fs::copy_files_except_ext(book.get_source(), destination, true, &["md"])?; let src = book.get_source();
utils::fs::copy_files_except_ext(&src, &destination, true, &["md"])?;
Ok(()) Ok(())
} }
} }
fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>> { fn make_data(book: &MDBook, config: &Config) -> Result<serde_json::Map<String, serde_json::Value>> {
debug!("[fn]: make_data"); debug!("[fn]: make_data");
let html = config.html_config().unwrap_or_default();
let mut data = serde_json::Map::new(); let mut data = serde_json::Map::new();
data.insert("language".to_owned(), json!("en")); data.insert("language".to_owned(), json!("en"));
data.insert("book_title".to_owned(), json!(book.get_title())); data.insert("book_title".to_owned(), json!(config.book.title.clone().unwrap_or_default()));
data.insert("description".to_owned(), json!(book.get_description())); data.insert("description".to_owned(), json!(config.book.description.clone().unwrap_or_default()));
data.insert("favicon".to_owned(), json!("favicon.png")); data.insert("favicon".to_owned(), json!("favicon.png"));
if let Some(livereload) = book.get_livereload() { if let Some(ref livereload) = book.livereload {
data.insert("livereload".to_owned(), json!(livereload)); data.insert("livereload".to_owned(), json!(livereload));
} }
// Add google analytics tag // Add google analytics tag
if let Some(ref ga) = book.get_google_analytics_id() { if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
data.insert("google_analytics".to_owned(), json!(ga)); data.insert("google_analytics".to_owned(), json!(ga));
} }
if book.get_mathjax_support() { if html.mathjax_support {
data.insert("mathjax_support".to_owned(), json!(true)); data.insert("mathjax_support".to_owned(), json!(true));
} }
// Add check to see if there is an additional style // Add check to see if there is an additional style
if book.has_additional_css() { if !html.additional_css.is_empty() {
let mut css = Vec::new(); let mut css = Vec::new();
for style in book.get_additional_css() { for style in &html.additional_css {
match style.strip_prefix(book.get_root()) { match style.strip_prefix(&book.root) {
Ok(p) => css.push(p.to_str().expect("Could not convert to str")), Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
Err(_) => { Err(_) => {
css.push(style.file_name() css.push(style.file_name()
@ -341,10 +355,10 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
} }
// Add check to see if there is an additional script // Add check to see if there is an additional script
if book.has_additional_js() { if !html.additional_js.is_empty() {
let mut js = Vec::new(); let mut js = Vec::new();
for script in book.get_additional_js() { for script in &html.additional_js {
match script.strip_prefix(book.get_root()) { match script.strip_prefix(&book.root) {
Ok(p) => js.push(p.to_str().expect("Could not convert to str")), Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
Err(_) => { Err(_) => {
js.push(script.file_name() js.push(script.file_name()
@ -357,7 +371,7 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
data.insert("additional_js".to_owned(), json!(js)); data.insert("additional_js".to_owned(), json!(js));
} }
if book.get_html_config().get_playpen_config().is_editable() { if html.playpen.editable {
data.insert("playpens_editable".to_owned(), json!(true)); data.insert("playpens_editable".to_owned(), json!(true));
data.insert("editor_js".to_owned(), json!("editor.js")); data.insert("editor_js".to_owned(), json!("editor.js"));
data.insert("ace_js".to_owned(), json!("ace.js")); data.insert("ace_js".to_owned(), json!("ace.js"));
@ -519,7 +533,7 @@ fn fix_code_blocks(html: &str) -> String {
.into_owned() .into_owned()
} }
fn add_playpen_pre(html: &str, playpen_config: &PlaypenConfig) -> String { fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap(); let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
regex.replace_all(html, |caps: &Captures| { regex.replace_all(html, |caps: &Captures| {
let text = &caps[1]; let text = &caps[1];
@ -530,7 +544,7 @@ fn add_playpen_pre(html: &str, playpen_config: &PlaypenConfig) -> String {
classes.contains("mdbook-runnable") classes.contains("mdbook-runnable")
{ {
// wrap the contents in an external pre block // wrap the contents in an external pre block
if playpen_config.is_editable() && classes.contains("editable") || if playpen_config.editable && classes.contains("editable") ||
text.contains("fn main") || text.contains("quick_main!") text.contains("fn main") || text.contains("quick_main!")
{ {
format!("<pre class=\"playpen\">{}</pre>", text) format!("<pre class=\"playpen\">{}</pre>", text)
@ -583,6 +597,7 @@ struct RenderItemContext<'a> {
destination: PathBuf, destination: PathBuf,
data: serde_json::Map<String, serde_json::Value>, data: serde_json::Map<String, serde_json::Value>,
is_index: bool, is_index: bool,
html_config: HtmlConfig,
} }
pub fn normalize_path(path: &str) -> String { pub fn normalize_path(path: &str) -> String {

View File

@ -1,45 +0,0 @@
extern crate mdbook;
extern crate tempdir;
use std::fs::File;
use std::io::Write;
use mdbook::MDBook;
use tempdir::TempDir;
// Tests that config values unspecified in the configuration file do not overwrite
// values specified earlier.
#[test]
fn do_not_overwrite_unspecified_config_values() {
let dir = TempDir::new("mdbook").expect("Could not create a temp dir");
let book = MDBook::new(dir.path()).with_source("bar")
.with_destination("baz")
.with_mathjax_support(true);
assert_eq!(book.get_root(), dir.path());
assert_eq!(book.get_source(), dir.path().join("bar"));
assert_eq!(book.get_destination(), dir.path().join("baz"));
// Test when trying to read a config file that does not exist
let book = book.read_config().expect("Error reading the config file");
assert_eq!(book.get_root(), dir.path());
assert_eq!(book.get_source(), dir.path().join("bar"));
assert_eq!(book.get_destination(), dir.path().join("baz"));
assert_eq!(book.get_mathjax_support(), true);
// Try with a partial config file
let file_path = dir.path().join("book.toml");
let mut f = File::create(file_path).expect("Could not create config file");
f.write_all(br#"source = "barbaz""#)
.expect("Could not write to config file");
f.sync_all().expect("Could not sync the file");
let book = book.read_config().expect("Error reading the config file");
assert_eq!(book.get_root(), dir.path());
assert_eq!(book.get_source(), dir.path().join("barbaz"));
assert_eq!(book.get_destination(), dir.path().join("baz"));
assert_eq!(book.get_mathjax_support(), true);
}

View File

@ -1,8 +1,9 @@
extern crate mdbook; extern crate mdbook;
extern crate tempdir; extern crate tempdir;
use tempdir::TempDir; use std::path::PathBuf;
use mdbook::MDBook; use mdbook::MDBook;
use tempdir::TempDir;
/// Run `mdbook init` in an empty directory and make sure the default files /// Run `mdbook init` in an empty directory and make sure the default files
@ -20,7 +21,9 @@ fn base_mdbook_init_should_create_default_content() {
md.init().unwrap(); md.init().unwrap();
for file in &created_files { for file in &created_files {
assert!(temp.path().join(file).exists(), "{} doesn't exist", file); let target = temp.path().join(file);
println!("{}", target.display());
assert!(target.exists(), "{} doesn't exist", file);
} }
} }
@ -37,14 +40,14 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
file); file);
} }
let mut md = MDBook::new(temp.path()).with_source("in") let mut md = MDBook::new(temp.path());
.with_destination("out"); md.config.book.src = PathBuf::from("in");
md.config.book.build_dir = PathBuf::from("out");
md.init().unwrap(); md.init().unwrap();
for file in &created_files { for file in &created_files {
assert!(temp.path().join(file).exists(), let target = temp.path().join(file);
"{} should have been created by `mdbook init`", assert!(target.exists(), "{} should have been created by `mdbook init`", file);
file);
} }
} }

View File

@ -1,87 +0,0 @@
extern crate mdbook;
use mdbook::config::BookConfig;
use mdbook::config::jsonconfig::JsonConfig;
use std::path::PathBuf;
// Tests that the `src` key is correctly parsed in the JSON config
#[test]
fn from_json_source() {
let json = r#"{
"src": "source"
}"#;
let parsed = JsonConfig::from_json(json).expect("This should parse");
let config = BookConfig::from_jsonconfig("root", parsed);
assert_eq!(config.get_source(), PathBuf::from("root/source"));
}
// Tests that the `title` key is correctly parsed in the JSON config
#[test]
fn from_json_title() {
let json = r#"{
"title": "Some title"
}"#;
let parsed = JsonConfig::from_json(json).expect("This should parse");
let config = BookConfig::from_jsonconfig("root", parsed);
assert_eq!(config.get_title(), "Some title");
}
// Tests that the `description` key is correctly parsed in the JSON config
#[test]
fn from_json_description() {
let json = r#"{
"description": "This is a description"
}"#;
let parsed = JsonConfig::from_json(json).expect("This should parse");
let config = BookConfig::from_jsonconfig("root", parsed);
assert_eq!(config.get_description(), "This is a description");
}
// Tests that the `author` key is correctly parsed in the JSON config
#[test]
fn from_json_author() {
let json = r#"{
"author": "John Doe"
}"#;
let parsed = JsonConfig::from_json(json).expect("This should parse");
let config = BookConfig::from_jsonconfig("root", parsed);
assert_eq!(config.get_authors(), &[String::from("John Doe")]);
}
// Tests that the `dest` key is correctly parsed in the JSON config
#[test]
fn from_json_destination() {
let json = r#"{
"dest": "htmlbook"
}"#;
let parsed = JsonConfig::from_json(json).expect("This should parse");
let config = BookConfig::from_jsonconfig("root", parsed);
let htmlconfig = config.get_html_config();
assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook"));
}
// Tests that the `theme_path` key is correctly parsed in the JSON config
#[test]
fn from_json_output_html_theme() {
let json = r#"{
"theme_path": "theme"
}"#;
let parsed = JsonConfig::from_json(json).expect("This should parse");
let config = BookConfig::from_jsonconfig("root", parsed);
let htmlconfig = config.get_html_config();
assert_eq!(htmlconfig.get_theme(), &PathBuf::from("root/theme"));
}

View File

@ -1,209 +0,0 @@
extern crate mdbook;
use mdbook::config::BookConfig;
use mdbook::config::tomlconfig::TomlConfig;
use std::path::PathBuf;
// Tests that the `source` key is correctly parsed in the TOML config
#[test]
fn from_toml_source() {
let toml = r#"source = "source""#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
assert_eq!(config.get_source(), PathBuf::from("root/source"));
}
// Tests that the `title` key is correctly parsed in the TOML config
#[test]
fn from_toml_title() {
let toml = r#"title = "Some title""#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
assert_eq!(config.get_title(), "Some title");
}
// Tests that the `description` key is correctly parsed in the TOML config
#[test]
fn from_toml_description() {
let toml = r#"description = "This is a description""#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
assert_eq!(config.get_description(), "This is a description");
}
// Tests that the `author` key is correctly parsed in the TOML config
#[test]
fn from_toml_author() {
let toml = r#"author = "John Doe""#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
assert_eq!(config.get_authors(), &[String::from("John Doe")]);
}
// Tests that the `authors` key is correctly parsed in the TOML config
#[test]
fn from_toml_authors() {
let toml = r#"authors = ["John Doe", "Jane Doe"]"#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
assert_eq!(config.get_authors(),
&[String::from("John Doe"), String::from("Jane Doe")]);
}
// Tests that the default `playpen` config is correct in the TOML config
#[test]
fn from_toml_playpen_default() {
let toml = "";
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let playpenconfig = config.get_html_config().get_playpen_config();
assert_eq!(playpenconfig.get_editor(),
PathBuf::from("root/theme/editor"));
assert_eq!(playpenconfig.is_editable(), false);
}
// Tests that the `playpen.editor` key is correctly parsed in the TOML config
#[test]
fn from_toml_playpen_editor() {
let toml = r#"[output.html.playpen]
editor = "editordir""#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let playpenconfig = config.get_html_config().get_playpen_config();
assert_eq!(playpenconfig.get_editor(),
PathBuf::from("root/theme/editordir"));
}
// Tests that the `playpen.editable` key is correctly parsed in the TOML config
#[test]
fn from_toml_playpen_editable() {
let toml = r#"[output.html.playpen]
editable = true"#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let playpenconfig = config.get_html_config().get_playpen_config();
assert_eq!(playpenconfig.is_editable(), true);
}
// Tests that the `output.html.destination` key is correcly parsed in the TOML config
#[test]
fn from_toml_output_html_destination() {
let toml = r#"[output.html]
destination = "htmlbook""#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let htmlconfig = config.get_html_config();
assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook"));
}
// Tests that the `output.html.theme` key is correctly parsed in the TOML config
#[test]
fn from_toml_output_html_theme() {
let toml = r#"[output.html]
theme = "theme""#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let htmlconfig = config.get_html_config();
assert_eq!(htmlconfig.get_theme(), &PathBuf::from("root/theme"));
}
// Tests that the `output.html.curly-quotes` key is correctly parsed in the TOML config
#[test]
fn from_toml_output_html_curly_quotes() {
let toml = r#"[output.html]
curly-quotes = true"#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let htmlconfig = config.get_html_config();
assert_eq!(htmlconfig.get_curly_quotes(), true);
}
// Tests that the `output.html.mathjax-support` key is correctly parsed in the TOML config
#[test]
fn from_toml_output_html_mathjax_support() {
let toml = r#"[output.html]
mathjax-support = true"#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let htmlconfig = config.get_html_config();
assert_eq!(htmlconfig.get_mathjax_support(), true);
}
// Tests that the `output.html.google-analytics` key is correctly parsed in the TOML config
#[test]
fn from_toml_output_html_google_analytics() {
let toml = r#"[output.html]
google-analytics = "123456""#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let htmlconfig = config.get_html_config();
assert_eq!(htmlconfig.get_google_analytics_id()
.expect("the google-analytics key was provided"),
String::from("123456"));
}
// Tests that the `output.html.additional-css` key is correctly parsed in the TOML config
#[test]
fn from_toml_output_html_additional_stylesheet() {
let toml = r#"[output.html]
additional-css = ["custom.css", "two/custom.css"]"#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let htmlconfig = config.get_html_config();
assert_eq!(htmlconfig.get_additional_css(),
&[PathBuf::from("root/custom.css"),
PathBuf::from("root/two/custom.css")]);
}
// Tests that the `output.html.additional-js` key is correctly parsed in the TOML config
#[test]
fn from_toml_output_html_additional_scripts() {
let toml = r#"[output.html]
additional-js = ["custom.js", "two/custom.js"]"#;
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let htmlconfig = config.get_html_config();
assert_eq!(htmlconfig.get_additional_js(),
&[PathBuf::from("root/custom.js"),
PathBuf::from("root/two/custom.js")]);
}