diff --git a/Cargo.toml b/Cargo.toml
index b036bc55..b203f8fc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,13 +15,14 @@ exclude = [
]
[dependencies]
-clap = "2.2.1"
-handlebars = { version = "0.20.0", features = ["serde_type"] }
-serde = "0.8.17"
-serde_json = "0.8.3"
+clap = "2.19.2"
+handlebars = { version = "0.23.0", features = ["serde_type"] }
+serde = "0.8"
+serde_json = "0.8"
pulldown-cmark = "0.0.8"
log = "0.3"
-env_logger = "0.3.4"
+env_logger = "0.3"
+toml = { version = "0.2", features = ["serde"] }
# Watch feature
notify = { version = "2.5.5", optional = true }
@@ -33,12 +34,10 @@ iron = { version = "0.4", optional = true }
staticfile = { version = "0.3", optional = true }
ws = { version = "0.5.1", optional = true}
-
# Tests
[dev-dependencies]
tempdir = "0.3.4"
-
[features]
default = ["output", "watch", "serve"]
debug = []
diff --git a/book-example/book.json b/book-example/book.json
deleted file mode 100644
index aba0a400..00000000
--- a/book-example/book.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "title": "mdBook Documentation",
- "description": "Create book from markdown files. Like Gitbook but implemented in Rust",
- "author": "Mathieu David"
-}
diff --git a/book-example/book.toml b/book-example/book.toml
new file mode 100644
index 00000000..cac456db
--- /dev/null
+++ b/book-example/book.toml
@@ -0,0 +1,3 @@
+title = "mdBook Documentation"
+description = "Create book from markdown files. Like Gitbook but implemented in Rust"
+author = "Mathieu David"
diff --git a/book-example/src/SUMMARY.md b/book-example/src/SUMMARY.md
index ff3911c7..8d7bdcdd 100644
--- a/book-example/src/SUMMARY.md
+++ b/book-example/src/SUMMARY.md
@@ -1,5 +1,7 @@
# Summary
+[Introduction](misc/introduction.md)
+
- [mdBook](README.md)
- [Command Line Tool](cli/cli-tool.md)
- [init](cli/init.md)
diff --git a/book-example/src/cli/test.md b/book-example/src/cli/test.md
index a7979f01..feeb4e9d 100644
--- a/book-example/src/cli/test.md
+++ b/book-example/src/cli/test.md
@@ -10,7 +10,7 @@ mdBook supports a `test` command that will run all available tests in mdBook. At
- checking for unused files
- ...
-In the future I would like the user to be able to enable / disable test from the `book.json` configuration file and support custom tests.
+In the future I would like the user to be able to enable / disable test from the `book.toml` configuration file and support custom tests.
**How to use it:**
```bash
diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md
index f7c7769b..bbea9a59 100644
--- a/book-example/src/format/config.md
+++ b/book-example/src/format/config.md
@@ -1,16 +1,16 @@
# Configuration
-You can configure the parameters for your book in the ***book.json*** file.
+You can configure the parameters for your book in the ***book.toml*** file.
-Here is an example of what a ***book.json*** file might look like:
+We encourage using the TOML format, but JSON is also recognized and parsed.
-```json
-{
- "title": "Example book",
- "author": "Name",
- "description": "The example book covers examples.",
- "dest": "output/my-book"
-}
+Here is an example of what a ***book.toml*** file might look like:
+
+```toml
+title = "Example book"
+author = "Name"
+description = "The example book covers examples."
+dest = "output/my-book"
```
#### Supported variables
diff --git a/book-example/src/format/format.md b/book-example/src/format/format.md
index fe4cda8f..35757252 100644
--- a/book-example/src/format/format.md
+++ b/book-example/src/format/format.md
@@ -4,5 +4,5 @@ In this section you will learn how to:
- Structure your book correctly
- Format your `SUMMARY.md` file
-- Configure your book using `book.json`
+- Configure your book using `book.toml`
- Customize your theme
diff --git a/book-example/src/format/theme/index-hbs.md b/book-example/src/format/theme/index-hbs.md
index 3903da51..e509565a 100644
--- a/book-example/src/format/theme/index-hbs.md
+++ b/book-example/src/format/theme/index-hbs.md
@@ -19,7 +19,7 @@ Here is a list of the properties that are exposed:
- ***language*** Language of the book in the form `en`. To use in \
for example.
At the moment it is hardcoded.
-- ***title*** Title of the book, as specified in `book.json`
+- ***title*** Title of the book, as specified in `book.toml`
- ***path*** Relative path to the original markdown file from the source directory
- ***content*** This is the rendered markdown.
diff --git a/book-example/src/format/theme/syntax-highlighting.md b/book-example/src/format/theme/syntax-highlighting.md
index ec4490a6..fe6b3654 100644
--- a/book-example/src/format/theme/syntax-highlighting.md
+++ b/book-example/src/format/theme/syntax-highlighting.md
@@ -47,7 +47,7 @@ Will render as
# }
```
-**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.json` so that everyone can benefit from it.**
+**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.toml` so that everyone can benefit from it.**
## Improve default theme
diff --git a/book-example/src/lib/lib.md b/book-example/src/lib/lib.md
index 4a67ed06..0f7a643d 100644
--- a/book-example/src/lib/lib.md
+++ b/book-example/src/lib/lib.md
@@ -13,7 +13,7 @@ fn main() {
let mut book = MDBook::new(Path::new("my-book")) // Path to root
.set_src(Path::new("src")) // Path from root to source directory
.set_dest(Path::new("book")) // Path from root to output directory
- .read_config(); // Parse book.json file for configuration
+ .read_config(); // Parse book.toml or book.json file for configuration
book.build().unwrap(); // Render the book
}
diff --git a/book-example/src/misc/introduction.md b/book-example/src/misc/introduction.md
new file mode 100644
index 00000000..36495382
--- /dev/null
+++ b/book-example/src/misc/introduction.md
@@ -0,0 +1,3 @@
+# Introduction
+
+A frontmatter chapter.
diff --git a/src/book/bookconfig.rs b/src/book/bookconfig.rs
index 69f3340f..50bcb76d 100644
--- a/src/book/bookconfig.rs
+++ b/src/book/bookconfig.rs
@@ -1,7 +1,12 @@
-use serde_json;
+extern crate toml;
+
+use std::process::exit;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
+use std::collections::BTreeMap;
+use std::str::FromStr;
+use serde_json;
#[derive(Debug, Clone)]
pub struct BookConfig {
@@ -18,7 +23,6 @@ pub struct BookConfig {
multilingual: bool,
}
-
impl BookConfig {
pub fn new(root: &Path) -> Self {
BookConfig {
@@ -40,71 +44,117 @@ impl BookConfig {
debug!("[fn]: read_config");
- // If the file does not exist, return early
- let mut config_file = match File::open(root.join("book.json")) {
- Ok(f) => f,
- Err(_) => {
- debug!("[*]: Failed to open {:?}", root.join("book.json"));
- return self;
- },
+ let read_file = |path: PathBuf| -> String {
+ let mut data = String::new();
+ let mut f: File = match File::open(&path) {
+ Ok(x) => x,
+ Err(_) => {
+ error!("[*]: Failed to open {:?}", &path);
+ exit(2);
+ }
+ };
+ if let Err(_) = f.read_to_string(&mut data) {
+ error!("[*]: Failed to read {:?}", &path);
+ exit(2);
+ }
+ data
};
- debug!("[*]: Reading config");
- let mut data = String::new();
+ // Read book.toml or book.json if exists
- // Just return if an error occured.
- // I would like to propagate the error, but I have to return `&self`
- if let Err(_) = config_file.read_to_string(&mut data) {
- return self;
+ if Path::new(root.join("book.toml").as_os_str()).exists() {
+
+ debug!("[*]: Reading config");
+ let data = read_file(root.join("book.toml"));
+ self.parse_from_toml_string(&data);
+
+ } else if Path::new(root.join("book.json").as_os_str()).exists() {
+
+ debug!("[*]: Reading config");
+ let data = read_file(root.join("book.json"));
+ self.parse_from_json_string(&data);
+
+ } else {
+ debug!("[*]: No book.toml or book.json was found, using defaults.");
}
- // Convert to JSON
- if let Ok(config) = serde_json::from_str::(&data) {
- // Extract data
+ self
+ }
- let config = config.as_object().unwrap();
+ pub fn parse_from_toml_string(&mut self, data: &String) -> &mut Self {
- debug!("[*]: Extracting data from config");
- // Title, author, description
- if let Some(a) = config.get("title") {
- self.title = a.to_string().replace("\"", "")
- }
- if let Some(a) = config.get("author") {
- self.author = a.to_string().replace("\"", "")
- }
- if let Some(a) = config.get("description") {
- self.description = a.to_string().replace("\"", "")
+ let mut parser = toml::Parser::new(&data);
+
+ let config = match parser.parse() {
+ Some(x) => {x},
+ None => {
+ error!("[*]: Toml parse errors in book.toml: {:?}", parser.errors);
+ exit(2);
}
+ };
- // Destination folder
- if let Some(a) = config.get("dest") {
- let mut dest = PathBuf::from(&a.to_string().replace("\"", ""));
+ self.parse_from_btreemap(&config);
- // If path is relative make it absolute from the parent directory of src
- if dest.is_relative() {
- dest = self.get_root().join(&dest);
- }
- self.set_dest(&dest);
+ self
+ }
+
+ /// Parses the string to JSON and converts it to BTreeMap.
+ pub fn parse_from_json_string(&mut self, data: &String) -> &mut Self {
+
+ let c: serde_json::Value = match serde_json::from_str(&data) {
+ Ok(x) => x,
+ Err(e) => {
+ error!("[*]: JSON parse errors in book.json: {:?}", e);
+ exit(2);
}
+ };
- // Source folder
- if let Some(a) = config.get("src") {
- let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
- if src.is_relative() {
- src = self.get_root().join(&src);
- }
- self.set_src(&src);
+ let config = json_object_to_btreemap(&c.as_object().unwrap());
+ self.parse_from_btreemap(&config);
+
+ self
+ }
+
+ pub fn parse_from_btreemap(&mut self, config: &BTreeMap) -> &mut Self {
+
+ // Title, author, description
+ if let Some(a) = config.get("title") {
+ self.title = a.to_string().replace("\"", "");
+ }
+ if let Some(a) = config.get("author") {
+ self.author = a.to_string().replace("\"", "");
+ }
+ if let Some(a) = config.get("description") {
+ self.description = a.to_string().replace("\"", "");
+ }
+
+ // Destination folder
+ if let Some(a) = config.get("dest") {
+ let mut dest = PathBuf::from(&a.to_string().replace("\"", ""));
+
+ // If path is relative make it absolute from the parent directory of src
+ if dest.is_relative() {
+ dest = self.get_root().join(&dest);
}
+ self.set_dest(&dest);
+ }
- // Theme path folder
- if let Some(a) = config.get("theme_path") {
- let mut theme_path = PathBuf::from(&a.to_string().replace("\"", ""));
- if theme_path.is_relative() {
- theme_path = self.get_root().join(&theme_path);
- }
- self.set_theme_path(&theme_path);
+ // Source folder
+ if let Some(a) = config.get("src") {
+ let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
+ if src.is_relative() {
+ src = self.get_root().join(&src);
}
+ self.set_src(&src);
+ }
+ // Theme path folder
+ if let Some(a) = config.get("theme_path") {
+ let mut theme_path = PathBuf::from(&a.to_string().replace("\"", ""));
+ if theme_path.is_relative() {
+ theme_path = self.get_root().join(&theme_path);
+ }
+ self.set_theme_path(&theme_path);
}
self
@@ -146,3 +196,33 @@ impl BookConfig {
self
}
}
+
+pub fn json_object_to_btreemap(json: &serde_json::Map) -> BTreeMap {
+ let mut config: BTreeMap = BTreeMap::new();
+
+ for (key, value) in json.iter() {
+ config.insert(
+ String::from_str(key).unwrap(),
+ json_value_to_toml_value(value.to_owned())
+ );
+ }
+
+ config
+}
+
+pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value {
+ match json {
+ serde_json::Value::Null => toml::Value::String("".to_string()),
+ serde_json::Value::Bool(x) => toml::Value::Boolean(x),
+ serde_json::Value::I64(x) => toml::Value::Integer(x),
+ serde_json::Value::U64(x) => toml::Value::Integer(x as i64),
+ serde_json::Value::F64(x) => toml::Value::Float(x),
+ serde_json::Value::String(x) => toml::Value::String(x),
+ serde_json::Value::Array(x) => {
+ toml::Value::Array(x.iter().map(|v| json_value_to_toml_value(v.to_owned())).collect())
+ },
+ serde_json::Value::Object(x) => {
+ toml::Value::Table(json_object_to_btreemap(&x))
+ },
+ }
+}
diff --git a/src/book/bookconfig_test.rs b/src/book/bookconfig_test.rs
new file mode 100644
index 00000000..34122628
--- /dev/null
+++ b/src/book/bookconfig_test.rs
@@ -0,0 +1,349 @@
+#[cfg(test)]
+
+use std::path::Path;
+use serde_json;
+use book::bookconfig::*;
+
+#[test]
+fn it_parses_json_config() {
+ let text = r#"
+{
+ "title": "mdBook Documentation",
+ "description": "Create book from markdown files. Like Gitbook but implemented in Rust",
+ "author": "Mathieu David"
+}"#;
+
+ // TODO don't require path argument, take pwd
+ let mut config = BookConfig::new(Path::new("."));
+
+ config.parse_from_json_string(&text.to_string());
+
+ let mut expected = BookConfig::new(Path::new("."));
+ expected.title = "mdBook Documentation".to_string();
+ expected.author = "Mathieu David".to_string();
+ expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
+
+ assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
+}
+
+#[test]
+fn it_parses_toml_config() {
+ let text = r#"
+title = "mdBook Documentation"
+description = "Create book from markdown files. Like Gitbook but implemented in Rust"
+author = "Mathieu David"
+"#;
+
+ // TODO don't require path argument, take pwd
+ let mut config = BookConfig::new(Path::new("."));
+
+ config.parse_from_toml_string(&text.to_string());
+
+ let mut expected = BookConfig::new(Path::new("."));
+ expected.title = "mdBook Documentation".to_string();
+ expected.author = "Mathieu David".to_string();
+ expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
+
+ assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
+}
+
+#[test]
+fn it_parses_json_nested_array_to_toml() {
+
+ // Example from:
+ // toml-0.2.1/tests/valid/arrays-nested.json
+
+ let text = r#"
+{
+ "nest": {
+ "type": "array",
+ "value": [
+ {"type": "array", "value": [
+ {"type": "string", "value": "a"}
+ ]},
+ {"type": "array", "value": [
+ {"type": "string", "value": "b"}
+ ]}
+ ]
+ }
+}"#;
+
+ let c: serde_json::Value = serde_json::from_str(&text).unwrap();
+
+ let result = json_object_to_btreemap(&c.as_object().unwrap());
+
+ let expected = r#"{
+ "nest": Table(
+ {
+ "type": String(
+ "array"
+ ),
+ "value": Array(
+ [
+ Table(
+ {
+ "type": String(
+ "array"
+ ),
+ "value": Array(
+ [
+ Table(
+ {
+ "type": String(
+ "string"
+ ),
+ "value": String(
+ "a"
+ )
+ }
+ )
+ ]
+ )
+ }
+ ),
+ Table(
+ {
+ "type": String(
+ "array"
+ ),
+ "value": Array(
+ [
+ Table(
+ {
+ "type": String(
+ "string"
+ ),
+ "value": String(
+ "b"
+ )
+ }
+ )
+ ]
+ )
+ }
+ )
+ ]
+ )
+ }
+ )
+}"#;
+
+ assert_eq!(format!("{:#?}", result), expected);
+}
+
+
+#[test]
+fn it_parses_json_arrays_to_toml() {
+
+ // Example from:
+ // toml-0.2.1/tests/valid/arrays.json
+
+ let text = r#"
+{
+ "ints": {
+ "type": "array",
+ "value": [
+ {"type": "integer", "value": "1"},
+ {"type": "integer", "value": "2"},
+ {"type": "integer", "value": "3"}
+ ]
+ },
+ "floats": {
+ "type": "array",
+ "value": [
+ {"type": "float", "value": "1.1"},
+ {"type": "float", "value": "2.1"},
+ {"type": "float", "value": "3.1"}
+ ]
+ },
+ "strings": {
+ "type": "array",
+ "value": [
+ {"type": "string", "value": "a"},
+ {"type": "string", "value": "b"},
+ {"type": "string", "value": "c"}
+ ]
+ },
+ "dates": {
+ "type": "array",
+ "value": [
+ {"type": "datetime", "value": "1987-07-05T17:45:00Z"},
+ {"type": "datetime", "value": "1979-05-27T07:32:00Z"},
+ {"type": "datetime", "value": "2006-06-01T11:00:00Z"}
+ ]
+ }
+}"#;
+
+ let c: serde_json::Value = serde_json::from_str(&text).unwrap();
+
+ let result = json_object_to_btreemap(&c.as_object().unwrap());
+
+ let expected = r#"{
+ "dates": Table(
+ {
+ "type": String(
+ "array"
+ ),
+ "value": Array(
+ [
+ Table(
+ {
+ "type": String(
+ "datetime"
+ ),
+ "value": String(
+ "1987-07-05T17:45:00Z"
+ )
+ }
+ ),
+ Table(
+ {
+ "type": String(
+ "datetime"
+ ),
+ "value": String(
+ "1979-05-27T07:32:00Z"
+ )
+ }
+ ),
+ Table(
+ {
+ "type": String(
+ "datetime"
+ ),
+ "value": String(
+ "2006-06-01T11:00:00Z"
+ )
+ }
+ )
+ ]
+ )
+ }
+ ),
+ "floats": Table(
+ {
+ "type": String(
+ "array"
+ ),
+ "value": Array(
+ [
+ Table(
+ {
+ "type": String(
+ "float"
+ ),
+ "value": String(
+ "1.1"
+ )
+ }
+ ),
+ Table(
+ {
+ "type": String(
+ "float"
+ ),
+ "value": String(
+ "2.1"
+ )
+ }
+ ),
+ Table(
+ {
+ "type": String(
+ "float"
+ ),
+ "value": String(
+ "3.1"
+ )
+ }
+ )
+ ]
+ )
+ }
+ ),
+ "ints": Table(
+ {
+ "type": String(
+ "array"
+ ),
+ "value": Array(
+ [
+ Table(
+ {
+ "type": String(
+ "integer"
+ ),
+ "value": String(
+ "1"
+ )
+ }
+ ),
+ Table(
+ {
+ "type": String(
+ "integer"
+ ),
+ "value": String(
+ "2"
+ )
+ }
+ ),
+ Table(
+ {
+ "type": String(
+ "integer"
+ ),
+ "value": String(
+ "3"
+ )
+ }
+ )
+ ]
+ )
+ }
+ ),
+ "strings": Table(
+ {
+ "type": String(
+ "array"
+ ),
+ "value": Array(
+ [
+ Table(
+ {
+ "type": String(
+ "string"
+ ),
+ "value": String(
+ "a"
+ )
+ }
+ ),
+ Table(
+ {
+ "type": String(
+ "string"
+ ),
+ "value": String(
+ "b"
+ )
+ }
+ ),
+ Table(
+ {
+ "type": String(
+ "string"
+ ),
+ "value": String(
+ "c"
+ )
+ }
+ )
+ ]
+ )
+ }
+ )
+}"#;
+
+ assert_eq!(format!("{:#?}", result), expected);
+}
diff --git a/src/book/mod.rs b/src/book/mod.rs
index c3594300..25d59ca1 100644
--- a/src/book/mod.rs
+++ b/src/book/mod.rs
@@ -1,6 +1,8 @@
pub mod bookitem;
pub mod bookconfig;
+pub mod bookconfig_test;
+
pub use self::bookitem::{BookItem, BookItems};
pub use self::bookconfig::BookConfig;
diff --git a/src/renderer/html_handlebars/helpers/navigation.rs b/src/renderer/html_handlebars/helpers/navigation.rs
index 5c135861..58c90100 100644
--- a/src/renderer/html_handlebars/helpers/navigation.rs
+++ b/src/renderer/html_handlebars/helpers/navigation.rs
@@ -1,10 +1,11 @@
use std::path::Path;
-use std::collections::BTreeMap;
+use std::collections::{VecDeque, BTreeMap};
use serde_json;
use serde_json::value::ToJson;
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Context, Renderable};
+
// Handlebars helper for navigation
pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
@@ -14,9 +15,9 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
// get value from context data
// rc.get_path() is current json parent path, you should always use it like this
// param is the key of value you want to display
- let chapters = c.navigate(rc.get_path(), "chapters");
+ let chapters = c.navigate(rc.get_path(), &VecDeque::new(), "chapters");
- let current = c.navigate(rc.get_path(), "path")
+ let current = c.navigate(rc.get_path(), &VecDeque::new(), "path")
.to_string()
.replace("\"", "");
@@ -114,9 +115,9 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
// get value from context data
// rc.get_path() is current json parent path, you should always use it like this
// param is the key of value you want to display
- let chapters = c.navigate(rc.get_path(), "chapters");
+ let chapters = c.navigate(rc.get_path(), &VecDeque::new(), "chapters");
- let current = c.navigate(rc.get_path(), "path")
+ let current = c.navigate(rc.get_path(), &VecDeque::new(), "path")
.to_string()
.replace("\"", "");
diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs
index f5ec6c46..07a6b36d 100644
--- a/src/renderer/html_handlebars/helpers/toc.rs
+++ b/src/renderer/html_handlebars/helpers/toc.rs
@@ -1,5 +1,5 @@
use std::path::Path;
-use std::collections::BTreeMap;
+use std::collections::{VecDeque, BTreeMap};
use serde_json;
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
@@ -15,8 +15,8 @@ impl HelperDef for RenderToc {
// get value from context data
// rc.get_path() is current json parent path, you should always use it like this
// param is the key of value you want to display
- let chapters = c.navigate(rc.get_path(), "chapters");
- let current = c.navigate(rc.get_path(), "path").to_string().replace("\"", "");
+ let chapters = c.navigate(rc.get_path(), &VecDeque::new(), "chapters");
+ let current = c.navigate(rc.get_path(), &VecDeque::new(), "path").to_string().replace("\"", "");
try!(rc.writer.write("".as_bytes()));
// Decode json format