Merge pull request #190 from gambhiro/parse-toml

Parse toml
This commit is contained in:
Mathieu David 2016-12-26 13:36:16 +01:00 committed by GitHub
commit f1df53a4bb
16 changed files with 519 additions and 85 deletions

View File

@ -15,13 +15,14 @@ exclude = [
] ]
[dependencies] [dependencies]
clap = "2.2.1" clap = "2.19.2"
handlebars = { version = "0.20.0", features = ["serde_type"] } handlebars = { version = "0.23.0", features = ["serde_type"] }
serde = "0.8.17" serde = "0.8"
serde_json = "0.8.3" serde_json = "0.8"
pulldown-cmark = "0.0.8" pulldown-cmark = "0.0.8"
log = "0.3" log = "0.3"
env_logger = "0.3.4" env_logger = "0.3"
toml = { version = "0.2", features = ["serde"] }
# Watch feature # Watch feature
notify = { version = "2.5.5", optional = true } notify = { version = "2.5.5", optional = true }
@ -33,12 +34,10 @@ iron = { version = "0.4", optional = true }
staticfile = { version = "0.3", optional = true } staticfile = { version = "0.3", optional = true }
ws = { version = "0.5.1", optional = true} ws = { version = "0.5.1", optional = true}
# Tests # Tests
[dev-dependencies] [dev-dependencies]
tempdir = "0.3.4" tempdir = "0.3.4"
[features] [features]
default = ["output", "watch", "serve"] default = ["output", "watch", "serve"]
debug = [] debug = []

View File

@ -1,5 +0,0 @@
{
"title": "mdBook Documentation",
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
"author": "Mathieu David"
}

3
book-example/book.toml Normal file
View File

@ -0,0 +1,3 @@
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
author = "Mathieu David"

View File

@ -1,5 +1,7 @@
# Summary # Summary
[Introduction](misc/introduction.md)
- [mdBook](README.md) - [mdBook](README.md)
- [Command Line Tool](cli/cli-tool.md) - [Command Line Tool](cli/cli-tool.md)
- [init](cli/init.md) - [init](cli/init.md)

View File

@ -10,7 +10,7 @@ mdBook supports a `test` command that will run all available tests in mdBook. At
- checking for unused files - 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:** **How to use it:**
```bash ```bash

View File

@ -1,16 +1,16 @@
# Configuration # 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 Here is an example of what a ***book.toml*** file might look like:
{
"title": "Example book", ```toml
"author": "Name", title = "Example book"
"description": "The example book covers examples.", author = "Name"
"dest": "output/my-book" description = "The example book covers examples."
} dest = "output/my-book"
``` ```
#### Supported variables #### Supported variables

View File

@ -4,5 +4,5 @@ In this section you will learn how to:
- Structure your book correctly - Structure your book correctly
- Format your `SUMMARY.md` file - Format your `SUMMARY.md` file
- Configure your book using `book.json` - Configure your book using `book.toml`
- Customize your theme - Customize your theme

View File

@ -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 <code class="language-html">\<html lang="{{ language }}"></code> for example. - ***language*** Language of the book in the form `en`. To use in <code class="language-html">\<html lang="{{ language }}"></code> for example.
At the moment it is hardcoded. 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 - ***path*** Relative path to the original markdown file from the source directory
- ***content*** This is the rendered markdown. - ***content*** This is the rendered markdown.

View File

@ -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 ## Improve default theme

View File

@ -13,7 +13,7 @@ fn main() {
let mut book = MDBook::new(Path::new("my-book")) // Path to root let mut book = MDBook::new(Path::new("my-book")) // Path to root
.set_src(Path::new("src")) // Path from root to source directory .set_src(Path::new("src")) // Path from root to source directory
.set_dest(Path::new("book")) // Path from root to output 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 book.build().unwrap(); // Render the book
} }

View File

@ -0,0 +1,3 @@
# Introduction
A frontmatter chapter.

View File

@ -1,7 +1,12 @@
use serde_json; extern crate toml;
use std::process::exit;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::collections::BTreeMap;
use std::str::FromStr;
use serde_json;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BookConfig { pub struct BookConfig {
@ -18,7 +23,6 @@ pub struct BookConfig {
multilingual: bool, multilingual: bool,
} }
impl BookConfig { impl BookConfig {
pub fn new(root: &Path) -> Self { pub fn new(root: &Path) -> Self {
BookConfig { BookConfig {
@ -40,71 +44,117 @@ impl BookConfig {
debug!("[fn]: read_config"); debug!("[fn]: read_config");
// If the file does not exist, return early let read_file = |path: PathBuf| -> String {
let mut config_file = match File::open(root.join("book.json")) { let mut data = String::new();
Ok(f) => f, let mut f: File = match File::open(&path) {
Err(_) => { Ok(x) => x,
debug!("[*]: Failed to open {:?}", root.join("book.json")); Err(_) => {
return self; 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"); // Read book.toml or book.json if exists
let mut data = String::new();
// Just return if an error occured. if Path::new(root.join("book.toml").as_os_str()).exists() {
// I would like to propagate the error, but I have to return `&self`
if let Err(_) = config_file.read_to_string(&mut data) { debug!("[*]: Reading config");
return self; 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 self
if let Ok(config) = serde_json::from_str::<serde_json::Value>(&data) { }
// Extract data
let config = config.as_object().unwrap(); pub fn parse_from_toml_string(&mut self, data: &String) -> &mut Self {
debug!("[*]: Extracting data from config"); let mut parser = toml::Parser::new(&data);
// Title, author, description
if let Some(a) = config.get("title") { let config = match parser.parse() {
self.title = a.to_string().replace("\"", "") Some(x) => {x},
} None => {
if let Some(a) = config.get("author") { error!("[*]: Toml parse errors in book.toml: {:?}", parser.errors);
self.author = a.to_string().replace("\"", "") exit(2);
}
if let Some(a) = config.get("description") {
self.description = a.to_string().replace("\"", "")
} }
};
// Destination folder self.parse_from_btreemap(&config);
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 self
if dest.is_relative() { }
dest = self.get_root().join(&dest);
} /// Parses the string to JSON and converts it to BTreeMap<String, toml::Value>.
self.set_dest(&dest); 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 let config = json_object_to_btreemap(&c.as_object().unwrap());
if let Some(a) = config.get("src") { self.parse_from_btreemap(&config);
let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
if src.is_relative() { self
src = self.get_root().join(&src); }
}
self.set_src(&src); pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &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 // Source folder
if let Some(a) = config.get("theme_path") { if let Some(a) = config.get("src") {
let mut theme_path = PathBuf::from(&a.to_string().replace("\"", "")); let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
if theme_path.is_relative() { if src.is_relative() {
theme_path = self.get_root().join(&theme_path); src = self.get_root().join(&src);
}
self.set_theme_path(&theme_path);
} }
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 self
@ -146,3 +196,33 @@ impl BookConfig {
self self
} }
} }
pub fn json_object_to_btreemap(json: &serde_json::Map<String, serde_json::Value>) -> BTreeMap<String, toml::Value> {
let mut config: BTreeMap<String, toml::Value> = 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))
},
}
}

349
src/book/bookconfig_test.rs Normal file
View File

@ -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);
}

View File

@ -1,6 +1,8 @@
pub mod bookitem; pub mod bookitem;
pub mod bookconfig; pub mod bookconfig;
pub mod bookconfig_test;
pub use self::bookitem::{BookItem, BookItems}; pub use self::bookitem::{BookItem, BookItems};
pub use self::bookconfig::BookConfig; pub use self::bookconfig::BookConfig;

View File

@ -1,10 +1,11 @@
use std::path::Path; use std::path::Path;
use std::collections::BTreeMap; use std::collections::{VecDeque, BTreeMap};
use serde_json; use serde_json;
use serde_json::value::ToJson; use serde_json::value::ToJson;
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Context, Renderable}; use handlebars::{Handlebars, RenderError, RenderContext, Helper, Context, Renderable};
// Handlebars helper for navigation // Handlebars helper for navigation
pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { 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 // get value from context data
// rc.get_path() is current json parent path, you should always use it like this // 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 // 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() .to_string()
.replace("\"", ""); .replace("\"", "");
@ -114,9 +115,9 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
// get value from context data // get value from context data
// rc.get_path() is current json parent path, you should always use it like this // 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 // 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() .to_string()
.replace("\"", ""); .replace("\"", "");

View File

@ -1,5 +1,5 @@
use std::path::Path; use std::path::Path;
use std::collections::BTreeMap; use std::collections::{VecDeque, BTreeMap};
use serde_json; use serde_json;
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context}; use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
@ -15,8 +15,8 @@ impl HelperDef for RenderToc {
// get value from context data // get value from context data
// rc.get_path() is current json parent path, you should always use it like this // 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 // 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").to_string().replace("\"", ""); let current = c.navigate(rc.get_path(), &VecDeque::new(), "path").to_string().replace("\"", "");
try!(rc.writer.write("<ul class=\"chapter\">".as_bytes())); try!(rc.writer.write("<ul class=\"chapter\">".as_bytes()));
// Decode json format // Decode json format