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