From 170bf8b1eb22aacfbfff5b5f524cd61ee22fcd59 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Thu, 18 May 2017 19:32:08 +0200 Subject: [PATCH 1/9] New configuration struct + tests #285 --- Cargo.toml | 1 + src/config/bookconfig.rs | 179 +++++++++++++++++++++++++++++++++++++++ src/config/htmlconfig.rs | 64 ++++++++++++++ src/config/mod.rs | 7 ++ src/config/tomlconfig.rs | 50 +++++++++++ src/lib.rs | 7 +- tests/config.rs | 88 +++++++++++++++++++ 7 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 src/config/bookconfig.rs create mode 100644 src/config/htmlconfig.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/tomlconfig.rs create mode 100644 tests/config.rs diff --git a/Cargo.toml b/Cargo.toml index 97c2be12..098566da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ exclude = [ clap = "2.24" handlebars = "0.26" serde = "1.0" +serde_derive = "1.0" serde_json = "1.0" pulldown-cmark = "0.0.14" log = "0.3" diff --git a/src/config/bookconfig.rs b/src/config/bookconfig.rs new file mode 100644 index 00000000..2c2673a0 --- /dev/null +++ b/src/config/bookconfig.rs @@ -0,0 +1,179 @@ +use std::path::{PathBuf, Path}; + +use super::HtmlConfig; +use super::tomlconfig::TomlConfig; + +/// Configuration struct containing all the configuration options available in mdBook. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct BookConfig { + root: PathBuf, + source: PathBuf, + + title: String, + authors: Vec, + description: String, + + multilingual: bool, + indent_spaces: i32, + + html_config: Option, +} + +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(), Some(&HtmlConfig::new(PathBuf::from("directory/to/my/book")))); + /// ``` + pub fn new>(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: Some(htmlconfig), + } + } + + /// Builder method to set the source directory + pub fn with_source>(mut self, source: T) -> Self { + self.source = source.into(); + self + } + + /// Builder method to set the book's title + pub fn with_title>(mut self, title: T) -> Self { + self.title = title.into(); + self + } + + /// Builder method to set the book's description + pub fn with_description>(mut self, description: T) -> Self { + self.description = description.into(); + self + } + + /// Builder method to set the book's authors + pub fn with_authors>>(mut self, authors: T) -> Self { + self.authors = authors.into(); + self + } + + pub fn from_tomlconfig>(root: T, tomlconfig: TomlConfig) -> Self { + let root = root.into(); + let mut config = BookConfig::new(&root); + + if let Some(s) = tomlconfig.source { + config.set_source(s); + } + + if let Some(t) = tomlconfig.title { + config.set_title(t); + } + + if let Some(d) = tomlconfig.description { + config.set_description(d); + } + + if let Some(a) = tomlconfig.authors { + config.set_authors(a); + } + + if let Some(a) = tomlconfig.author { + config.set_authors(vec![a]); + } + + if let Some(tomlhtmlconfig) = tomlconfig.output.and_then(|o| o.html) { + let source = config.get_source().to_owned(); + let mut htmlconfig = config.get_mut_html_config().expect("We just created a new config and it creates a default HtmlConfig"); + htmlconfig.fill_from_tomlconfig(&root, &source, tomlhtmlconfig); + } + + config + } + + pub fn set_root>(&mut self, root: T) -> &mut Self { + self.root = root.into(); + self + } + + pub fn get_root(&self) -> &Path { + &self.root + } + + pub fn set_source>(&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>(&mut self, title: T) -> &mut Self { + self.title = title.into(); + self + } + + pub fn get_title(&self) -> &str { + &self.title + } + + pub fn set_description>(&mut self, description: T) -> &mut Self { + self.description = description.into(); + self + } + + pub fn get_description(&self) -> &str { + &self.description + } + + pub fn set_authors>>(&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 = Some(htmlconfig); + self + } + + /// Returns the configuration for the HTML renderer or None of there isn't any + pub fn get_html_config(&self) -> Option<&HtmlConfig> { + self.html_config.as_ref() + } + + pub fn get_mut_html_config(&mut self) -> Option<&mut HtmlConfig> { + self.html_config.as_mut() + } +} diff --git a/src/config/htmlconfig.rs b/src/config/htmlconfig.rs new file mode 100644 index 00000000..5f8d40ae --- /dev/null +++ b/src/config/htmlconfig.rs @@ -0,0 +1,64 @@ +use std::path::{PathBuf, Path}; + +use super::tomlconfig::TomlHtmlConfig; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct HtmlConfig { + destination: PathBuf, + theme: Option, + google_analytics: Option, +} + +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>(root: T) -> Self { + HtmlConfig { + destination: root.into().join("book"), + theme: None, + google_analytics: None, + } + } + + pub fn fill_from_tomlconfig>(&mut self, root: T, source: T, tomlconfig: TomlHtmlConfig) -> &mut Self { + if let Some(d) = tomlconfig.destination { + if d.is_relative() { + self.destination = root.into().join(d); + } else { + self.destination = d; + } + } + + if let Some(t) = tomlconfig.theme { + if t.is_relative() { + self.theme = Some(source.into().join(t)); + } else { + self.theme = Some(t); + } + } + + if tomlconfig.google_analytics.is_some() { + self.google_analytics = tomlconfig.google_analytics; + } + + self + } + + pub fn get_destination(&self) -> &Path { + &self.destination + } + + // FIXME: How to get a `Option<&Path>` ? + pub fn get_theme(&self) -> Option<&PathBuf> { + self.theme.as_ref() + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 00000000..ead9dcaa --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,7 @@ +pub mod bookconfig; +pub mod htmlconfig; +pub mod tomlconfig; + +// Re-export the config structs +pub use self::bookconfig::BookConfig; +pub use self::htmlconfig::HtmlConfig; diff --git a/src/config/tomlconfig.rs b/src/config/tomlconfig.rs new file mode 100644 index 00000000..cb5dad1b --- /dev/null +++ b/src/config/tomlconfig.rs @@ -0,0 +1,50 @@ +extern crate toml; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TomlConfig { + pub source: Option, + + pub title: Option, + pub author: Option, + pub authors: Option>, + pub description: Option, + + pub output: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TomlOutputConfig { + pub html: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TomlHtmlConfig { + pub destination: Option, + pub theme: Option, + pub google_analytics: Option, +} + +/// 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 { + let config: TomlConfig = toml::from_str(input) + .map_err(|e| format!("Could not parse TOML: {}", e))?; + + return Ok(config); + } +} + + diff --git a/src/lib.rs b/src/lib.rs index 303bb23b..882e22c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,9 +69,11 @@ //! //! Make sure to take a look at it. -extern crate serde; #[macro_use] -extern crate serde_json; +extern crate serde_derive; +extern crate serde; +#[macro_use] extern crate serde_json; + extern crate handlebars; extern crate pulldown_cmark; extern crate regex; @@ -79,6 +81,7 @@ extern crate regex; #[macro_use] extern crate log; pub mod book; +pub mod config; mod parse; pub mod renderer; pub mod theme; diff --git a/tests/config.rs b/tests/config.rs new file mode 100644 index 00000000..3f6e3143 --- /dev/null +++ b/tests/config.rs @@ -0,0 +1,88 @@ +extern crate mdbook; +use mdbook::config::BookConfig; +use mdbook::config::tomlconfig::TomlConfig; + +use std::path::PathBuf; + +// Tests that the `title` key is correcly 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 correcly 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 correcly 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 correcly 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 correcly 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 `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().expect("There should be an HtmlConfig"); + + assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook")); +} + +// Tests that the `output.html.theme` key is correcly 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().expect("There should be an HtmlConfig"); + + assert_eq!(htmlconfig.get_theme().expect("the theme key was provided"), &PathBuf::from("root/src/theme")); +} \ No newline at end of file From d3ae2eda56575fffd3c6c0d514e21ecf00368ccf Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Thu, 18 May 2017 23:52:38 +0200 Subject: [PATCH 2/9] Replace the old book structure with the new one --- src/bin/mdbook.rs | 41 +-- src/book/mod.rs | 264 ++++++++++--------- src/config/bookconfig.rs | 35 ++- src/config/htmlconfig.rs | 30 ++- src/lib.rs | 1 - src/renderer/html_handlebars/hbs_renderer.rs | 26 +- 6 files changed, 238 insertions(+), 159 deletions(-) diff --git a/src/bin/mdbook.rs b/src/bin/mdbook.rs index 56179ef1..78387ba1 100644 --- a/src/bin/mdbook.rs +++ b/src/bin/mdbook.rs @@ -129,7 +129,7 @@ fn init(args: &ArgMatches) -> Result<(), Box> { // Skip this if `--force` is present if !args.is_present("force") { // Print warning - print!("\nCopying the default theme to {:?}", book.get_src()); + print!("\nCopying the default theme to {:?}", book.get_source()); println!("could potentially overwrite files already present in that directory."); print!("\nAre you sure you want to continue? (y/n) "); @@ -148,7 +148,9 @@ fn init(args: &ArgMatches) -> Result<(), Box> { } // Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root` - let is_dest_inside_root = book.get_dest().starts_with(book.get_root()); + let is_dest_inside_root = book.get_destination() + .map(|p| p.starts_with(book.get_root())) + .unwrap_or(false); if !args.is_present("force") && is_dest_inside_root { println!("\nDo you want a .gitignore to be created? (y/n)"); @@ -168,10 +170,10 @@ fn init(args: &ArgMatches) -> Result<(), Box> { // Build command implementation fn build(args: &ArgMatches) -> Result<(), Box> { let book_dir = get_book_dir(args); - let book = MDBook::new(&book_dir).read_config(); + let book = MDBook::new(&book_dir).read_config()?; let mut book = match args.value_of("dest-dir") { - Some(dest_dir) => book.set_dest(Path::new(dest_dir)), + Some(dest_dir) => book.with_destination(Path::new(dest_dir)), None => book, }; @@ -181,8 +183,10 @@ fn build(args: &ArgMatches) -> Result<(), Box> { book.build()?; - if args.is_present("open") { - open(book.get_dest().join("index.html")); + if let Some(d) = book.get_destination() { + if args.is_present("open") { + open(d.join("index.html")); + } } Ok(()) @@ -193,16 +197,18 @@ fn build(args: &ArgMatches) -> Result<(), Box> { #[cfg(feature = "watch")] fn watch(args: &ArgMatches) -> Result<(), Box> { let book_dir = get_book_dir(args); - let book = MDBook::new(&book_dir).read_config(); + let book = MDBook::new(&book_dir).read_config()?; let mut book = match args.value_of("dest-dir") { - Some(dest_dir) => book.set_dest(Path::new(dest_dir)), + Some(dest_dir) => book.with_destination(Path::new(dest_dir)), None => book, }; if args.is_present("open") { book.build()?; - open(book.get_dest().join("index.html")); + if let Some(d) = book.get_destination() { + open(d.join("index.html")); + } } trigger_on_change(&mut book, |path, book| { @@ -223,13 +229,18 @@ fn serve(args: &ArgMatches) -> Result<(), Box> { const RELOAD_COMMAND: &'static str = "reload"; let book_dir = get_book_dir(args); - let book = MDBook::new(&book_dir).read_config(); + let book = MDBook::new(&book_dir).read_config()?; let mut book = match args.value_of("dest-dir") { - Some(dest_dir) => book.set_dest(Path::new(dest_dir)), + Some(dest_dir) => book.with_destination(Path::new(dest_dir)), None => book, }; + if let None = book.get_destination() { + println!("The HTML renderer is not set up, impossible to serve the files."); + std::process::exit(2); + } + let port = args.value_of("port").unwrap_or("3000"); let ws_port = args.value_of("websocket-port").unwrap_or("3001"); let interface = args.value_of("interface").unwrap_or("localhost"); @@ -260,7 +271,7 @@ fn serve(args: &ArgMatches) -> Result<(), Box> { book.build()?; - let staticfile = staticfile::Static::new(book.get_dest()); + let staticfile = staticfile::Static::new(book.get_destination().expect("destination is present, checked before")); let iron = iron::Iron::new(staticfile); let _iron = iron.http(&*address).unwrap(); @@ -292,7 +303,7 @@ fn serve(args: &ArgMatches) -> Result<(), Box> { fn test(args: &ArgMatches) -> Result<(), Box> { let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir).read_config(); + let mut book = MDBook::new(&book_dir).read_config()?; book.test()?; @@ -341,8 +352,8 @@ fn trigger_on_change(book: &mut MDBook, closure: F) -> () }; // Add the source directory to the watcher - if let Err(e) = watcher.watch(book.get_src(), Recursive) { - println!("Error while watching {:?}:\n {:?}", book.get_src(), e); + if let Err(e) = watcher.watch(book.get_source(), Recursive) { + println!("Error while watching {:?}:\n {:?}", book.get_source(), e); ::std::process::exit(0); }; diff --git a/src/book/mod.rs b/src/book/mod.rs index 0a0e1099..c23687d4 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -4,29 +4,24 @@ pub mod bookconfig; pub mod bookconfig_test; pub use self::bookitem::{BookItem, BookItems}; -pub use self::bookconfig::BookConfig; use std::path::{Path, PathBuf}; use std::fs::{self, File}; use std::error::Error; use std::io; -use std::io::Write; +use std::io::{Read, Write}; use std::io::ErrorKind; use std::process::Command; use {theme, parse, utils}; use renderer::{Renderer, HtmlHandlebars}; +use config::{BookConfig, HtmlConfig}; +use config::tomlconfig::TomlConfig; + pub struct MDBook { - root: PathBuf, - dest: PathBuf, - src: PathBuf, - theme_path: PathBuf, - - pub title: String, - pub author: String, - pub description: String, + config: BookConfig, pub content: Vec, renderer: Box, @@ -50,7 +45,7 @@ impl MDBook { /// # use mdbook::MDBook; /// # use std::path::Path; /// # fn main() { - /// let book = MDBook::new(Path::new("root_dir")); + /// let book = MDBook::new("root_dir"); /// # } /// ``` /// @@ -75,14 +70,7 @@ impl MDBook { } MDBook { - root: root.to_owned(), - dest: root.join("book"), - src: root.join("src"), - theme_path: root.join("theme"), - - title: String::new(), - author: String::new(), - description: String::new(), + config: BookConfig::new(root), content: vec![], renderer: Box::new(HtmlHandlebars::new()), @@ -149,31 +137,33 @@ impl MDBook { debug!("[fn]: init"); - if !self.root.exists() { - fs::create_dir_all(&self.root).unwrap(); - info!("{:?} created", &self.root); + if !self.config.get_root().exists() { + fs::create_dir_all(&self.config.get_root()).unwrap(); + info!("{:?} created", &self.config.get_root()); } { - if !self.dest.exists() { - debug!("[*]: {:?} does not exist, trying to create directory", self.dest); - fs::create_dir_all(&self.dest)?; + if let Some(htmlconfig) = self.config.get_html_config() { + if !htmlconfig.get_destination().exists() { + debug!("[*]: {:?} does not exist, trying to create directory", htmlconfig.get_destination()); + fs::create_dir_all(htmlconfig.get_destination())?; + } + } + + + if !self.config.get_source().exists() { + debug!("[*]: {:?} does not exist, trying to create directory", self.config.get_source()); + fs::create_dir_all(self.config.get_source())?; } - if !self.src.exists() { - debug!("[*]: {:?} does not exist, trying to create directory", self.src); - fs::create_dir_all(&self.src)?; - } - - let summary = self.src.join("SUMMARY.md"); + let summary = self.config.get_source().join("SUMMARY.md"); if !summary.exists() { // Summary does not exist, create it - - debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", self.src.join("SUMMARY.md")); - let mut f = File::create(&self.src.join("SUMMARY.md"))?; + debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", &summary); + let mut f = File::create(&summary)?; debug!("[*]: Writing to SUMMARY.md"); @@ -195,7 +185,7 @@ impl MDBook { BookItem::Affix(ref ch) => ch, }; if !ch.path.as_os_str().is_empty() { - let path = self.src.join(&ch.path); + let path = self.config.get_source().join(&ch.path); if !path.exists() { if !self.create_missing { @@ -219,22 +209,23 @@ impl MDBook { pub fn create_gitignore(&self) { let gitignore = self.get_gitignore(); - if !gitignore.exists() { - // Gitignore does not exist, create it + // If the HTML renderer is not set, return + if self.config.get_html_config().is_none() { return; } + + let destination = self.config.get_html_config() + .expect("The HtmlConfig does exist, checked just before") + .get_destination(); + + // Check that the gitignore does not extist and that the destination path begins with the root path + // We assume tha if it does begin with the root path it is contained within. This assumption + // will not hold true for paths containing double dots to go back up e.g. `root/../destination` + if !gitignore.exists() && destination.starts_with(self.config.get_root()) { - // Because of `src/book/mdbook.rs#L37-L39`, - // `dest` will always start with `root`. - // If it is not, `strip_prefix` will return an Error. - if !self.get_dest().starts_with(&self.root) { - return; - } - - let relative = self.get_dest() - .strip_prefix(&self.root) - .expect("Destination is not relative to root."); - let relative = relative + let relative = destination + .strip_prefix(self.config.get_root()) + .expect("Could not strip the root prefix, path is not relative to root") .to_str() - .expect("Path could not be yielded into a string slice."); + .expect("Could not convert to &str"); debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore); @@ -258,8 +249,10 @@ impl MDBook { self.init()?; // Clean output directory - utils::fs::remove_dir_content(&self.dest)?; - + if let Some(htmlconfig) = self.config.get_html_config() { + utils::fs::remove_dir_content(htmlconfig.get_destination())?; + } + self.renderer.render(&self)?; Ok(()) @@ -267,51 +260,56 @@ impl MDBook { pub fn get_gitignore(&self) -> PathBuf { - self.root.join(".gitignore") + self.config.get_root().join(".gitignore") } pub fn copy_theme(&self) -> Result<(), Box> { debug!("[fn]: copy_theme"); - let theme_dir = self.src.join("theme"); + if let Some(themedir) = self.config.get_html_config().and_then(HtmlConfig::get_theme) { - if !theme_dir.exists() { - debug!("[*]: {:?} does not exist, trying to create directory", theme_dir); - fs::create_dir(&theme_dir)?; + if !themedir.exists() { + debug!("[*]: {:?} does not exist, trying to create directory", themedir); + fs::create_dir(&themedir)?; + } + + // index.hbs + let mut index = File::create(&themedir.join("index.hbs"))?; + index.write_all(theme::INDEX)?; + + // book.css + let mut css = File::create(&themedir.join("book.css"))?; + css.write_all(theme::CSS)?; + + // favicon.png + let mut favicon = File::create(&themedir.join("favicon.png"))?; + favicon.write_all(theme::FAVICON)?; + + // book.js + let mut js = File::create(&themedir.join("book.js"))?; + js.write_all(theme::JS)?; + + // highlight.css + let mut highlight_css = File::create(&themedir.join("highlight.css"))?; + highlight_css.write_all(theme::HIGHLIGHT_CSS)?; + + // highlight.js + let mut highlight_js = File::create(&themedir.join("highlight.js"))?; + highlight_js.write_all(theme::HIGHLIGHT_JS)?; } - - // index.hbs - let mut index = File::create(&theme_dir.join("index.hbs"))?; - index.write_all(theme::INDEX)?; - - // book.css - let mut css = File::create(&theme_dir.join("book.css"))?; - css.write_all(theme::CSS)?; - - // favicon.png - let mut favicon = File::create(&theme_dir.join("favicon.png"))?; - favicon.write_all(theme::FAVICON)?; - - // book.js - let mut js = File::create(&theme_dir.join("book.js"))?; - js.write_all(theme::JS)?; - - // highlight.css - let mut highlight_css = File::create(&theme_dir.join("highlight.css"))?; - highlight_css.write_all(theme::HIGHLIGHT_CSS)?; - - // highlight.js - let mut highlight_js = File::create(&theme_dir.join("highlight.js"))?; - highlight_js.write_all(theme::HIGHLIGHT_JS)?; - + Ok(()) } pub fn write_file>(&self, filename: P, content: &[u8]) -> Result<(), Box> { - let path = self.get_dest().join(filename); + let path = self.get_destination() + .ok_or(String::from("HtmlConfig not set, could not find a destination"))? + .join(filename); + utils::fs::create_file(&path) .and_then(|mut file| file.write_all(content)) .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Could not create {}: {}", path.display(), e)))?; + Ok(()) } @@ -320,21 +318,23 @@ impl MDBook { /// The `book.json` file should be in the root directory of the book. /// The root directory is the one specified when creating a new `MDBook` - pub fn read_config(mut self) -> Self { + pub fn read_config(mut self) -> Result> { - let config = BookConfig::new(&self.root) - .read_config(&self.root) - .to_owned(); + let toml = self.get_root().join("book.toml"); + let json = self.get_root().join("book.json"); - self.title = config.title; - self.description = config.description; - self.author = config.author; + if toml.exists() { + let mut file = File::open(toml)?; + let mut content = String::new(); + file.read_to_string(&mut content)?; - self.dest = config.dest; - self.src = config.src; - self.theme_path = config.theme_path; + let parsed_config = TomlConfig::from_toml(&content)?; + self.config.fill_from_tomlconfig(parsed_config); + } else if json.exists() { + unimplemented!(); + } - self + Ok(self) } /// You can change the default renderer to another one @@ -374,7 +374,7 @@ impl MDBook { if let BookItem::Chapter(_, ref ch) = *item { if ch.path != PathBuf::new() { - let path = self.get_src().join(&ch.path); + let path = self.get_source().join(&ch.path); println!("[*]: Testing file: {:?}", path); @@ -395,52 +395,49 @@ impl MDBook { } pub fn get_root(&self) -> &Path { - &self.root + self.config.get_root() } - pub fn set_dest(mut self, dest: &Path) -> Self { - - // Handle absolute and relative paths - if dest.is_absolute() { - self.dest = dest.to_owned(); + + pub fn with_destination>(mut self, destination: T) -> Self { + let root = self.config.get_root().to_owned(); + if let Some(htmlconfig) = self.config.get_mut_html_config() { + htmlconfig.set_destination(&root, &destination.into()); } else { - let dest = self.root.join(dest).to_owned(); - self.dest = dest; + error!("There is no HTML renderer set..."); } self } + - pub fn get_dest(&self) -> &Path { - &self.dest - } - - pub fn set_src(mut self, src: &Path) -> Self { - - // Handle absolute and relative paths - if src.is_absolute() { - self.src = src.to_owned(); - } else { - let src = self.root.join(src).to_owned(); - self.src = src; + pub fn get_destination(&self) -> Option<&Path> { + if let Some(htmlconfig) = self.config.get_html_config() { + return Some(htmlconfig.get_destination()); } + None + } + + pub fn with_source>(mut self, source: T) -> Self { + self.config.set_source(source); self } - pub fn get_src(&self) -> &Path { - &self.src + pub fn get_source(&self) -> &Path { + self.config.get_source() } - pub fn set_title(mut self, title: &str) -> Self { - self.title = title.to_owned(); + pub fn with_title>(mut self, title: T) -> Self { + self.config.set_title(title); self } pub fn get_title(&self) -> &str { - &self.title + self.config.get_title() } +/* pub fn set_author(mut self, author: &str) -> Self { self.author = author.to_owned(); self @@ -449,14 +446,14 @@ impl MDBook { pub fn get_author(&self) -> &str { &self.author } - - pub fn set_description(mut self, description: &str) -> Self { - self.description = description.to_owned(); +*/ + pub fn with_description>(mut self, description: T) -> Self { + self.config.set_description(description); self } pub fn get_description(&self) -> &str { - &self.description + self.config.get_description() } pub fn set_livereload(&mut self, livereload: String) -> &mut Self { @@ -473,23 +470,28 @@ impl MDBook { self.livereload.as_ref() } - pub fn set_theme_path(mut self, theme_path: &Path) -> Self { - self.theme_path = if theme_path.is_absolute() { - theme_path.to_owned() + pub fn with_theme_path>(mut self, theme_path: T) -> Self { + let root = self.config.get_root().to_owned(); + if let Some(htmlconfig) = self.config.get_mut_html_config() { + htmlconfig.set_theme(&root, &theme_path.into()); } else { - self.root.join(theme_path).to_owned() - }; + error!("There is no HTML renderer set..."); + } self } - pub fn get_theme_path(&self) -> &Path { - &self.theme_path + pub fn get_theme_path(&self) -> Option<&PathBuf> { + if let Some(htmlconfig) = self.config.get_html_config() { + return htmlconfig.get_theme(); + } + + None } // Construct book fn parse_summary(&mut self) -> Result<(), Box> { // When append becomes stable, use self.content.append() ... - self.content = parse::construct_bookitems(&self.src.join("SUMMARY.md"))?; + self.content = parse::construct_bookitems(&self.get_source().join("SUMMARY.md"))?; Ok(()) } } diff --git a/src/config/bookconfig.rs b/src/config/bookconfig.rs index 2c2673a0..9f445b2b 100644 --- a/src/config/bookconfig.rs +++ b/src/config/bookconfig.rs @@ -102,14 +102,45 @@ impl BookConfig { } if let Some(tomlhtmlconfig) = tomlconfig.output.and_then(|o| o.html) { - let source = config.get_source().to_owned(); let mut htmlconfig = config.get_mut_html_config().expect("We just created a new config and it creates a default HtmlConfig"); - htmlconfig.fill_from_tomlconfig(&root, &source, tomlhtmlconfig); + htmlconfig.fill_from_tomlconfig(&root, tomlhtmlconfig); } 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(); + if let Some(htmlconfig) = self.get_mut_html_config() { + htmlconfig.fill_from_tomlconfig(root, tomlhtmlconfig); + } + } + + self + } + pub fn set_root>(&mut self, root: T) -> &mut Self { self.root = root.into(); self diff --git a/src/config/htmlconfig.rs b/src/config/htmlconfig.rs index 5f8d40ae..93bbc522 100644 --- a/src/config/htmlconfig.rs +++ b/src/config/htmlconfig.rs @@ -29,10 +29,12 @@ impl HtmlConfig { } } - pub fn fill_from_tomlconfig>(&mut self, root: T, source: T, tomlconfig: TomlHtmlConfig) -> &mut Self { + pub fn fill_from_tomlconfig>(&mut self, root: T, tomlconfig: TomlHtmlConfig) -> &mut Self { + let root = root.into(); + if let Some(d) = tomlconfig.destination { if d.is_relative() { - self.destination = root.into().join(d); + self.destination = root.join(d); } else { self.destination = d; } @@ -40,7 +42,7 @@ impl HtmlConfig { if let Some(t) = tomlconfig.theme { if t.is_relative() { - self.theme = Some(source.into().join(t)); + self.theme = Some(root.join(t)); } else { self.theme = Some(t); } @@ -53,6 +55,17 @@ impl HtmlConfig { self } + pub fn set_destination>(&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 } @@ -61,4 +74,15 @@ impl HtmlConfig { pub fn get_theme(&self) -> Option<&PathBuf> { self.theme.as_ref() } + + pub fn set_theme>(&mut self, root: T, theme: T) -> &mut Self { + let d = theme.into(); + if d.is_relative() { + self.theme = Some(root.into().join(d)); + } else { + self.theme = Some(d); + } + + self + } } diff --git a/src/lib.rs b/src/lib.rs index 882e22c4..ce6d4103 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,5 +89,4 @@ pub mod utils; pub use book::MDBook; pub use book::BookItem; -pub use book::BookConfig; pub use renderer::Renderer; diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index a97b8e66..07dd8c4a 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -33,7 +33,7 @@ impl Renderer for HtmlHandlebars { let mut handlebars = Handlebars::new(); // Load theme - let theme = theme::Theme::new(book.get_theme_path()); + let theme = theme::Theme::new(book.get_theme_path().expect("If the HTML renderer is called, one would assume the HtmlConfig is set...")); // Register template debug!("[*]: Register handlebars template"); @@ -53,7 +53,7 @@ impl Renderer for HtmlHandlebars { // Check if dest directory exists debug!("[*]: Check if destination directory exists"); - if fs::create_dir_all(book.get_dest()).is_err() { + if fs::create_dir_all(book.get_destination().expect("If the HTML renderer is called, one would assume the HtmlConfig is set...")).is_err() { return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Unexpected error when constructing destination path"))); } @@ -67,7 +67,7 @@ impl Renderer for HtmlHandlebars { BookItem::Affix(ref ch) => { if ch.path != PathBuf::new() { - let path = book.get_src().join(&ch.path); + let path = book.get_source().join(&ch.path); debug!("[*]: Opening file: {:?}", path); let mut f = File::open(&path)?; @@ -116,8 +116,12 @@ impl Renderer for HtmlHandlebars { debug!("[*]: index.html"); let mut content = String::new(); - let _source = File::open(book.get_dest().join(&ch.path.with_extension("html")))? - .read_to_string(&mut content); + + let _source = File::open( + book.get_destination() + .expect("If the HTML renderer is called, one would assume the HtmlConfig is set...") + .join(&ch.path.with_extension("html")) + )?.read_to_string(&mut content); // This could cause a problem when someone displays // code containing @@ -131,7 +135,10 @@ impl Renderer for HtmlHandlebars { book.write_file("index.html", content.as_bytes())?; info!("[*] Creating index.html from {:?} ✓", - book.get_dest().join(&ch.path.with_extension("html"))); + book.get_destination() + .expect("If the HTML renderer is called, one would assume the HtmlConfig is set...") + .join(&ch.path.with_extension("html")) + ); index = false; } } @@ -181,7 +188,12 @@ impl Renderer for HtmlHandlebars { book.write_file("_FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF)?; // Copy all remaining files - utils::fs::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"])?; + utils::fs::copy_files_except_ext( + book.get_source(), + book.get_destination() + .expect("If the HTML renderer is called, one would assume the HtmlConfig is set..."), true, &["md"] + )?; + Ok(()) } From 70383d0a256e3875d757af6bfae1a0b16dfa5646 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Fri, 19 May 2017 00:56:37 +0200 Subject: [PATCH 3/9] New config structs supports json again (the old style) for a little deprecation period --- src/book/bookconfig_test.rs | 371 ------------------- src/book/mod.rs | 14 +- src/config/bookconfig.rs | 74 ++-- src/config/htmlconfig.rs | 4 + src/config/jsonconfig.rs | 43 +++ src/config/mod.rs | 2 + src/config/tomlconfig.rs | 1 + src/lib.rs | 11 +- src/renderer/html_handlebars/hbs_renderer.rs | 11 +- src/theme/mod.rs | 8 +- tests/jsonconfig.rs | 87 +++++ tests/{config.rs => tomlconfig.rs} | 16 +- 12 files changed, 225 insertions(+), 417 deletions(-) delete mode 100644 src/book/bookconfig_test.rs create mode 100644 src/config/jsonconfig.rs create mode 100644 tests/jsonconfig.rs rename tests/{config.rs => tomlconfig.rs} (82%) diff --git a/src/book/bookconfig_test.rs b/src/book/bookconfig_test.rs deleted file mode 100644 index b3a24385..00000000 --- a/src/book/bookconfig_test.rs +++ /dev/null @@ -1,371 +0,0 @@ -#![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); -} - -#[test] -fn it_fetches_google_analytics_from_toml() { - let text = r#" -title = "mdBook Documentation" -description = "Create book from markdown files. Like Gitbook but implemented in Rust" -author = "Mathieu David" -google_analytics_id = "123456" -"#; - - 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(); - expected.google_analytics = Some("123456".to_string()); - - assert_eq!(format!("{:#?}", config), format!("{:#?}", expected)); -} diff --git a/src/book/mod.rs b/src/book/mod.rs index c23687d4..9125dc68 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -1,7 +1,4 @@ pub mod bookitem; -pub mod bookconfig; - -pub mod bookconfig_test; pub use self::bookitem::{BookItem, BookItems}; @@ -18,6 +15,7 @@ use renderer::{Renderer, HtmlHandlebars}; use config::{BookConfig, HtmlConfig}; use config::tomlconfig::TomlConfig; +use config::jsonconfig::JsonConfig; pub struct MDBook { @@ -45,7 +43,7 @@ impl MDBook { /// # use mdbook::MDBook; /// # use std::path::Path; /// # fn main() { - /// let book = MDBook::new("root_dir"); + /// let book = MDBook::new(Path::new("root_dir")); /// # } /// ``` /// @@ -331,7 +329,13 @@ impl MDBook { let parsed_config = TomlConfig::from_toml(&content)?; self.config.fill_from_tomlconfig(parsed_config); } else if json.exists() { - unimplemented!(); + 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) diff --git a/src/config/bookconfig.rs b/src/config/bookconfig.rs index 9f445b2b..94efe9c5 100644 --- a/src/config/bookconfig.rs +++ b/src/config/bookconfig.rs @@ -2,6 +2,7 @@ use std::path::{PathBuf, Path}; use super::HtmlConfig; use super::tomlconfig::TomlConfig; +use super::jsonconfig::JsonConfig; /// Configuration struct containing all the configuration options available in mdBook. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -80,32 +81,7 @@ impl BookConfig { pub fn from_tomlconfig>(root: T, tomlconfig: TomlConfig) -> Self { let root = root.into(); let mut config = BookConfig::new(&root); - - if let Some(s) = tomlconfig.source { - config.set_source(s); - } - - if let Some(t) = tomlconfig.title { - config.set_title(t); - } - - if let Some(d) = tomlconfig.description { - config.set_description(d); - } - - if let Some(a) = tomlconfig.authors { - config.set_authors(a); - } - - if let Some(a) = tomlconfig.author { - config.set_authors(vec![a]); - } - - if let Some(tomlhtmlconfig) = tomlconfig.output.and_then(|o| o.html) { - let mut htmlconfig = config.get_mut_html_config().expect("We just created a new config and it creates a default HtmlConfig"); - htmlconfig.fill_from_tomlconfig(&root, tomlhtmlconfig); - } - + config.fill_from_tomlconfig(tomlconfig); config } @@ -141,6 +117,52 @@ impl BookConfig { self } + /// The JSON configuration file is **deprecated** and should not be used anymore. + /// Please, migrate to the TOML configuration file. + pub fn from_jsonconfig>(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(); + if let Some(htmlconfig) = self.get_mut_html_config() { + htmlconfig.set_destination(&root, &d); + } + } + + if let Some(d) = jsonconfig.theme_path { + let root = self.get_root().to_owned(); + if let Some(htmlconfig) = self.get_mut_html_config() { + htmlconfig.set_theme(&root, &d); + } + } + + self + } + pub fn set_root>(&mut self, root: T) -> &mut Self { self.root = root.into(); self diff --git a/src/config/htmlconfig.rs b/src/config/htmlconfig.rs index 93bbc522..030782d1 100644 --- a/src/config/htmlconfig.rs +++ b/src/config/htmlconfig.rs @@ -85,4 +85,8 @@ impl HtmlConfig { self } + + pub fn get_google_analytics_id(&self) -> Option { + self.google_analytics.clone() + } } diff --git a/src/config/jsonconfig.rs b/src/config/jsonconfig.rs new file mode 100644 index 00000000..9d1e2f3f --- /dev/null +++ b/src/config/jsonconfig.rs @@ -0,0 +1,43 @@ +extern crate serde_json; +use std::path::PathBuf; + +/// 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, + pub dest: Option, + + pub title: Option, + pub author: Option, + pub description: Option, + + pub theme_path: Option, + pub google_analytics: Option, +} + + +/// 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 { + let config: JsonConfig = serde_json::from_str(input) + .map_err(|e| format!("Could not parse JSON: {}", e))?; + + return Ok(config); + } +} + + diff --git a/src/config/mod.rs b/src/config/mod.rs index ead9dcaa..90a8e2e4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,7 +1,9 @@ pub mod bookconfig; pub mod htmlconfig; pub mod tomlconfig; +pub mod jsonconfig; // Re-export the config structs pub use self::bookconfig::BookConfig; pub use self::htmlconfig::HtmlConfig; +pub use self::tomlconfig::TomlConfig; diff --git a/src/config/tomlconfig.rs b/src/config/tomlconfig.rs index cb5dad1b..ebb43068 100644 --- a/src/config/tomlconfig.rs +++ b/src/config/tomlconfig.rs @@ -18,6 +18,7 @@ pub struct TomlOutputConfig { pub html: Option, } +#[serde(rename_all = "kebab-case")] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TomlHtmlConfig { pub destination: Option, diff --git a/src/lib.rs b/src/lib.rs index ce6d4103..1b31b88d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,12 +24,13 @@ //! use std::path::Path; //! //! 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 +//! let mut book = MDBook::new(Path::new("my-book")) // Path to root +//! .with_source(Path::new("src")) // Path from root to source directory +//! .with_destination(Path::new("book")) // Path from root to output directory +//! .read_config() // Parse book.json file for configuration +//! .expect("I don't handle the error for the configuration file, but you should!"); //! -//! book.build().unwrap(); // Render the book +//! book.build().unwrap(); // Render the book //! } //! ``` //! diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 07dd8c4a..16f1ccd9 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -33,7 +33,7 @@ impl Renderer for HtmlHandlebars { let mut handlebars = Handlebars::new(); // Load theme - let theme = theme::Theme::new(book.get_theme_path().expect("If the HTML renderer is called, one would assume the HtmlConfig is set...")); + let theme = theme::Theme::new(book.get_theme_path()); // Register template debug!("[*]: Register handlebars template"); @@ -53,7 +53,7 @@ impl Renderer for HtmlHandlebars { // Check if dest directory exists debug!("[*]: Check if destination directory exists"); - if fs::create_dir_all(book.get_destination().expect("If the HTML renderer is called, one would assume the HtmlConfig is set...")).is_err() { + if fs::create_dir_all(book.get_destination().expect("If the HTML renderer is called, one would assume the HtmlConfig is set... (2)")).is_err() { return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Unexpected error when constructing destination path"))); } @@ -119,7 +119,7 @@ impl Renderer for HtmlHandlebars { let _source = File::open( book.get_destination() - .expect("If the HTML renderer is called, one would assume the HtmlConfig is set...") + .expect("If the HTML renderer is called, one would assume the HtmlConfig is set... (3)") .join(&ch.path.with_extension("html")) )?.read_to_string(&mut content); @@ -136,7 +136,7 @@ impl Renderer for HtmlHandlebars { info!("[*] Creating index.html from {:?} ✓", book.get_destination() - .expect("If the HTML renderer is called, one would assume the HtmlConfig is set...") + .expect("If the HTML renderer is called, one would assume the HtmlConfig is set... (4)") .join(&ch.path.with_extension("html")) ); index = false; @@ -191,10 +191,9 @@ impl Renderer for HtmlHandlebars { utils::fs::copy_files_except_ext( book.get_source(), book.get_destination() - .expect("If the HTML renderer is called, one would assume the HtmlConfig is set..."), true, &["md"] + .expect("If the HTML renderer is called, one would assume the HtmlConfig is set... (5)"), true, &["md"] )?; - Ok(()) } } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 3570cb24..0ac4ec42 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::PathBuf; use std::fs::File; use std::io::Read; @@ -42,7 +42,7 @@ pub struct Theme { } impl Theme { - pub fn new(src: &Path) -> Self { + pub fn new(src: Option<&PathBuf>) -> Self { // Default theme let mut theme = Theme { @@ -58,10 +58,12 @@ impl Theme { }; // Check if the given path exists - if !src.exists() || !src.is_dir() { + if src.is_none() || !src.unwrap().exists() || !src.unwrap().is_dir() { return theme; } + let src = src.unwrap(); + // Check for individual files if they exist // index.hbs diff --git a/tests/jsonconfig.rs b/tests/jsonconfig.rs new file mode 100644 index 00000000..ac82bcfd --- /dev/null +++ b/tests/jsonconfig.rs @@ -0,0 +1,87 @@ +extern crate mdbook; +use mdbook::config::BookConfig; +use mdbook::config::jsonconfig::JsonConfig; + +use std::path::PathBuf; + +// Tests that the `title` key is correcly parsed in the TOML 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 correcly parsed in the TOML 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 correcly parsed in the TOML 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 correcly parsed in the TOML 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 `output.html.destination` key is correcly parsed in the TOML 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().expect("There should be an HtmlConfig"); + + assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook")); +} + +// Tests that the `output.html.theme` key is correcly parsed in the TOML 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().expect("There should be an HtmlConfig"); + + assert_eq!(htmlconfig.get_theme().expect("the theme key was provided"), &PathBuf::from("root/theme")); +} \ No newline at end of file diff --git a/tests/config.rs b/tests/tomlconfig.rs similarity index 82% rename from tests/config.rs rename to tests/tomlconfig.rs index 3f6e3143..73e472a3 100644 --- a/tests/config.rs +++ b/tests/tomlconfig.rs @@ -84,5 +84,19 @@ fn from_toml_output_html_theme() { let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig"); - assert_eq!(htmlconfig.get_theme().expect("the theme key was provided"), &PathBuf::from("root/src/theme")); + assert_eq!(htmlconfig.get_theme().expect("the theme key was provided"), &PathBuf::from("root/theme")); +} + +// Tests that the `output.html.google-analytics` key is correcly 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().expect("There should be an HtmlConfig"); + + assert_eq!(htmlconfig.get_google_analytics_id().expect("the google-analytics key was provided"), String::from("123456")); } \ No newline at end of file From 2e812db13cc3757d41bfe011c8f82e8dae0b95e3 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Fri, 19 May 2017 01:13:45 +0200 Subject: [PATCH 4/9] Fix for google-analytics --- src/book/mod.rs | 11 ++++++++--- src/renderer/html_handlebars/hbs_renderer.rs | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index 9125dc68..c3765ff0 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -29,8 +29,6 @@ pub struct MDBook { /// Should `mdbook build` create files referenced from SUMMARY.md if they /// don't exist pub create_missing: bool, - - pub google_analytics: Option, } impl MDBook { @@ -75,7 +73,6 @@ impl MDBook { livereload: None, create_missing: true, - google_analytics: None, } } @@ -492,6 +489,14 @@ impl MDBook { None } + pub fn get_google_analytics_id(&self) -> Option { + if let Some(htmlconfig) = self.config.get_html_config() { + return htmlconfig.get_google_analytics_id(); + } + + None + } + // Construct book fn parse_summary(&mut self) -> Result<(), Box> { // When append becomes stable, use self.content.append() ... diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 16f1ccd9..29fca838 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -211,7 +211,7 @@ fn make_data(book: &MDBook) -> Result } // Add google analytics tag - if let Some(ref ga) = book.google_analytics { + if let Some(ref ga) = book.get_google_analytics_id() { data.insert("google_analytics".to_owned(), json!(ga)); } From c6bfe0b1d7a83c7461bcecf6a141759afd57ec94 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sat, 20 May 2017 13:00:47 +0200 Subject: [PATCH 5/9] Adds a test for #240 --- tests/config.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/config.rs diff --git a/tests/config.rs b/tests/config.rs new file mode 100644 index 00000000..4d9fd5bb --- /dev/null +++ b/tests/config.rs @@ -0,0 +1,44 @@ +extern crate mdbook; +extern crate tempdir; + +use std::path::Path; +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(Path::new("bar")) + .with_destination(Path::new("baz")); + + assert_eq!(book.get_root(), dir.path()); + assert_eq!(book.get_source(), dir.path().join("bar")); + assert_eq!(book.get_destination().unwrap(), 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().unwrap(), dir.path().join("baz")); + + // 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().unwrap(), dir.path().join("baz")); +} + From bb4ceb481f2e374da575b82c31c2c484000b7569 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sat, 20 May 2017 13:56:01 +0200 Subject: [PATCH 6/9] Allow an additional custom stylesheets, closes #178 --- src/book/bookconfig.rs | 230 ------------------- src/book/mod.rs | 16 ++ src/config/htmlconfig.rs | 25 ++ src/config/tomlconfig.rs | 1 + src/renderer/html_handlebars/hbs_renderer.rs | 25 ++ src/theme/index.hbs | 5 + tests/tomlconfig.rs | 15 ++ 7 files changed, 87 insertions(+), 230 deletions(-) delete mode 100644 src/book/bookconfig.rs diff --git a/src/book/bookconfig.rs b/src/book/bookconfig.rs deleted file mode 100644 index ac1f0605..00000000 --- a/src/book/bookconfig.rs +++ /dev/null @@ -1,230 +0,0 @@ -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 { - root: PathBuf, - pub dest: PathBuf, - pub src: PathBuf, - pub theme_path: PathBuf, - - pub title: String, - pub author: String, - pub description: String, - - pub indent_spaces: i32, - multilingual: bool, - pub google_analytics: Option, -} - -impl BookConfig { - pub fn new(root: &Path) -> Self { - BookConfig { - root: root.to_owned(), - dest: root.join("book"), - src: root.join("src"), - theme_path: root.join("theme"), - - title: String::new(), - author: String::new(), - description: String::new(), - - indent_spaces: 4, // indentation used for SUMMARY.md - multilingual: false, - google_analytics: None, - } - } - - pub fn read_config(&mut self, root: &Path) -> &mut Self { - - debug!("[fn]: read_config"); - - 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 f.read_to_string(&mut data).is_err() { - error!("[*]: Failed to read {:?}", &path); - exit(2); - } - data - }; - - // Read book.toml or book.json if exists - - if root.join("book.toml").exists() { - - debug!("[*]: Reading config"); - let data = read_file(root.join("book.toml")); - self.parse_from_toml_string(&data); - - } else if root.join("book.json").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."); - } - - self - } - - pub fn parse_from_toml_string(&mut self, data: &str) -> &mut Self { - let config = match toml::from_str(data) { - Ok(x) => x, - Err(e) => { - error!("[*]: Toml parse errors in book.toml: {:?}", e); - exit(2); - }, - }; - - self.parse_from_btreemap(&config); - - self - } - - /// Parses the string to JSON and converts it - /// to BTreeMap. - pub fn parse_from_json_string(&mut self, data: &str) -> &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); - }, - }; - - 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); - } - - // 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); - } - - // Google analytics code - if let Some(id) = config.get("google_analytics_id") { - self.google_analytics = Some(id.to_string().replace("\"", "")); - } - - self - } - - pub fn get_root(&self) -> &Path { - &self.root - } - - pub fn set_root(&mut self, root: &Path) -> &mut Self { - self.root = root.to_owned(); - self - } - - pub fn get_dest(&self) -> &Path { - &self.dest - } - - pub fn set_dest(&mut self, dest: &Path) -> &mut Self { - self.dest = dest.to_owned(); - self - } - - pub fn get_src(&self) -> &Path { - &self.src - } - - pub fn set_src(&mut self, src: &Path) -> &mut Self { - self.src = src.to_owned(); - self - } - - pub fn get_theme_path(&self) -> &Path { - &self.theme_path - } - - pub fn set_theme_path(&mut self, theme_path: &Path) -> &mut Self { - self.theme_path = theme_path.to_owned(); - 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::Number(ref x) if x.is_i64() => toml::Value::Integer(x.as_i64().unwrap()), - serde_json::Value::Number(ref x) if x.is_u64() => toml::Value::Integer(x.as_i64().unwrap()), - serde_json::Value::Number(x) => toml::Value::Float(x.as_f64().unwrap()), - 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/mod.rs b/src/book/mod.rs index c3765ff0..d79e7e14 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -497,6 +497,22 @@ impl MDBook { None } + pub fn has_additional_css(&self) -> bool { + if let Some(htmlconfig) = self.config.get_html_config() { + return htmlconfig.has_additional_css(); + } + + false + } + + pub fn get_additional_css(&self) -> &[PathBuf] { + if let Some(htmlconfig) = self.config.get_html_config() { + return htmlconfig.get_additional_css(); + } + + &[] + } + // Construct book fn parse_summary(&mut self) -> Result<(), Box> { // When append becomes stable, use self.content.append() ... diff --git a/src/config/htmlconfig.rs b/src/config/htmlconfig.rs index 030782d1..4beb14c4 100644 --- a/src/config/htmlconfig.rs +++ b/src/config/htmlconfig.rs @@ -7,6 +7,7 @@ pub struct HtmlConfig { destination: PathBuf, theme: Option, google_analytics: Option, + additional_css: Vec, } impl HtmlConfig { @@ -26,6 +27,7 @@ impl HtmlConfig { destination: root.into().join("book"), theme: None, google_analytics: None, + additional_css: Vec::new(), } } @@ -52,6 +54,16 @@ impl HtmlConfig { 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); + } + } + } + self } @@ -89,4 +101,17 @@ impl HtmlConfig { pub fn get_google_analytics_id(&self) -> Option { self.google_analytics.clone() } + + pub fn set_google_analytics_id(&mut self, id: Option) -> &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 + } } diff --git a/src/config/tomlconfig.rs b/src/config/tomlconfig.rs index ebb43068..2a08fa84 100644 --- a/src/config/tomlconfig.rs +++ b/src/config/tomlconfig.rs @@ -24,6 +24,7 @@ pub struct TomlHtmlConfig { pub destination: Option, pub theme: Option, pub google_analytics: Option, + pub additional_css: Option>, } /// Returns a TomlConfig from a TOML string diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 29fca838..3da67507 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -187,6 +187,19 @@ impl Renderer for HtmlHandlebars { book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2", theme::FONT_AWESOME_WOFF2)?; book.write_file("_FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF)?; + for style in book.get_additional_css() { + let mut data = Vec::new(); + let mut f = File::open(style)?; + f.read_to_end(&mut data)?; + + let name = match style.strip_prefix(book.get_root()) { + Ok(p) => p.to_str().expect("Could not convert to str"), + Err(_) => style.file_name().expect("File has a file name").to_str().expect("Could not convert to str"), + }; + + book.write_file(name, &data)?; + } + // Copy all remaining files utils::fs::copy_files_except_ext( book.get_source(), @@ -215,6 +228,18 @@ fn make_data(book: &MDBook) -> Result data.insert("google_analytics".to_owned(), json!(ga)); } + // Add check to see if there is an additional style + if book.has_additional_css() { + let mut css = Vec::new(); + for style in book.get_additional_css() { + match style.strip_prefix(book.get_root()) { + Ok(p) => css.push(p.to_str().expect("Could not convert to str")), + Err(_) => css.push(style.file_name().expect("File has a file name").to_str().expect("Could not convert to str")), + } + } + data.insert("additional_css".to_owned(), json!(css)); + } + let mut chapters = vec![]; for item in book.iter() { diff --git a/src/theme/index.hbs b/src/theme/index.hbs index ee66e167..a4705884 100644 --- a/src/theme/index.hbs +++ b/src/theme/index.hbs @@ -21,6 +21,11 @@ + + {{#each additional_css}} + + {{/each}} + diff --git a/tests/tomlconfig.rs b/tests/tomlconfig.rs index 73e472a3..91c40016 100644 --- a/tests/tomlconfig.rs +++ b/tests/tomlconfig.rs @@ -99,4 +99,19 @@ fn from_toml_output_html_google_analytics() { let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig"); 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 correcly 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().expect("There should be an HtmlConfig"); + + assert_eq!(htmlconfig.get_additional_css(), &[PathBuf::from("root/custom.css"), PathBuf::from("root/two/custom.css")]); } \ No newline at end of file From 23efa9e14619a3ea5a287e7334837338f560283d Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sat, 20 May 2017 15:03:10 +0200 Subject: [PATCH 7/9] Document the TOML configuration file --- book-example/src/format/config.md | 83 +++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 14 deletions(-) diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md index 009528b0..58beb2e7 100644 --- a/book-example/src/format/config.md +++ b/book-example/src/format/config.md @@ -2,28 +2,83 @@ You can configure the parameters for your book in the ***book.toml*** file. -We encourage using the TOML format, but JSON is also recognized and parsed. +**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: ```toml title = "Example book" -author = "Name" +author = "John Doe" description = "The example book covers examples." -dest = "output/my-book" + +[output.html] +destination = "my-example-book" +additional-css = ["custom.css"] ``` -#### Supported variables +## Supported configuration options -If relative paths are given, they will be relative to the book's root, i.e. the -parent directory of the source directory. +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. -- **title:** The title of the book. -- **author:** The author of the book. -- **description:** The description, which is added as meta in the html head of each page. -- **src:** The path to the book's source files (chapters in Markdown, SUMMARY.md, etc.). Defaults to `root/src`. -- **dest:** The path to the directory where you want your book to be rendered. Defaults to `root/book`. -- **theme_path:** The path to a custom theme directory. Defaults to `root/theme`. -- **google_analytics_id:** If included, google analytics will be added to each page and use the provided ID. +### General metadata + +- **title:** The title of the book +- **author:** The author of the book +- **description:** A description for the book, which is added as meta information in the html `` of each page + +**book.toml** +```toml +title = "Example book" +author = "John Doe" +description = "The example book covers examples." +``` + +Some books may have multiple authors, there is an alternative key called `authors` plural that lets you specify an array +of authors. + +**book.toml** +```toml +title = "Example book" +authors = ["John Doe", "Jane Doe"] +description = "The example book covers examples." +``` + +### 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 +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 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 + 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. +- **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. + +**book.toml** +```toml +title = "Example book" +authors = ["John Doe", "Jane Doe"] +description = "The example book covers examples." + +[output.html] +destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book` +theme = "my-theme" +google-analytics = "123456" +additional-css = ["custom.css", "custom2.css"] +``` -***note:*** *the supported configurable parameters are scarce at the moment, but more will be added in the future* From 1a8e54bb5204566a56fb11c54367e452f8ec8e60 Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sun, 4 Jun 2017 19:48:41 +0200 Subject: [PATCH 8/9] remove unused methods --- src/book/mod.rs | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index d79e7e14..cdcb3358 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -145,7 +145,7 @@ impl MDBook { fs::create_dir_all(htmlconfig.get_destination())?; } } - + if !self.config.get_source().exists() { debug!("[*]: {:?} does not exist, trying to create directory", self.config.get_source()); @@ -206,11 +206,11 @@ impl MDBook { // If the HTML renderer is not set, return if self.config.get_html_config().is_none() { return; } - + let destination = self.config.get_html_config() .expect("The HtmlConfig does exist, checked just before") .get_destination(); - + // Check that the gitignore does not extist and that the destination path begins with the root path // We assume tha if it does begin with the root path it is contained within. This assumption // will not hold true for paths containing double dots to go back up e.g. `root/../destination` @@ -247,7 +247,7 @@ impl MDBook { if let Some(htmlconfig) = self.config.get_html_config() { utils::fs::remove_dir_content(htmlconfig.get_destination())?; } - + self.renderer.render(&self)?; Ok(()) @@ -292,7 +292,7 @@ impl MDBook { let mut highlight_js = File::create(&themedir.join("highlight.js"))?; highlight_js.write_all(theme::HIGHLIGHT_JS)?; } - + Ok(()) } @@ -399,7 +399,7 @@ impl MDBook { self.config.get_root() } - + pub fn with_destination>(mut self, destination: T) -> Self { let root = self.config.get_root().to_owned(); if let Some(htmlconfig) = self.config.get_mut_html_config() { @@ -410,7 +410,7 @@ impl MDBook { self } - + pub fn get_destination(&self) -> Option<&Path> { if let Some(htmlconfig) = self.config.get_html_config() { @@ -438,16 +438,6 @@ impl MDBook { self.config.get_title() } -/* - pub fn set_author(mut self, author: &str) -> Self { - self.author = author.to_owned(); - self - } - - pub fn get_author(&self) -> &str { - &self.author - } -*/ pub fn with_description>(mut self, description: T) -> Self { self.config.set_description(description); self From f1121cf8c28370696e642b1a67f886a7b42fd50c Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sun, 4 Jun 2017 20:47:34 +0200 Subject: [PATCH 9/9] fix build failure --- src/bin/mdbook.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bin/mdbook.rs b/src/bin/mdbook.rs index 78387ba1..416aa081 100644 --- a/src/bin/mdbook.rs +++ b/src/bin/mdbook.rs @@ -358,7 +358,10 @@ fn trigger_on_change(book: &mut MDBook, closure: F) -> () }; // Add the theme directory to the watcher - watcher.watch(book.get_theme_path(), Recursive).unwrap_or_default(); + if let Some(t) = book.get_theme_path() { + watcher.watch(t, Recursive).unwrap_or_default(); + } + // Add the book.{json,toml} file to the watcher if it exists, because it's not // located in the source directory