From fdff29ec7bc3caf556d627a341c8aa7942f9e435 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 11 Dec 2017 23:43:38 +1100 Subject: [PATCH 01/24] Added a mechanism for creating alternate backends --- src/book/mod.rs | 4 +-- src/config.rs | 41 ++++++++++++++++----------- src/renderer/mod.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 20 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index bc8550e22f..362e45359e 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -33,8 +33,8 @@ pub struct MDBook { pub root: PathBuf, /// The configuration used to tweak now a book is built. pub config: Config, - - book: Book, + /// A representation of the book's contents in memory. + pub book: Book, renderer: Box, /// The URL used for live reloading when serving up the book. diff --git a/src/config.rs b/src/config.rs index 5b5974774f..e3cd1b1b45 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,9 +25,10 @@ impl Config { /// Load the configuration file from disk. pub fn from_disk>(config_file: P) -> Result { let mut buffer = String::new(); - File::open(config_file).chain_err(|| "Unable to open the configuration file")? - .read_to_string(&mut buffer) - .chain_err(|| "Couldn't read the file")?; + File::open(config_file) + .chain_err(|| "Unable to open the configuration file")? + .read_to_string(&mut buffer) + .chain_err(|| "Couldn't read the file")?; Config::from_str(&buffer) } @@ -64,9 +65,10 @@ impl Config { let name = name.as_ref(); if let Some(value) = self.get(name) { - value.clone() - .try_into() - .chain_err(|| "Couldn't deserialize the value") + value + .clone() + .try_into() + .chain_err(|| "Couldn't deserialize the value") } else { bail!("Key not found, {:?}", name) } @@ -91,10 +93,11 @@ impl Config { get_and_insert!(table, "source" => cfg.book.src); get_and_insert!(table, "description" => cfg.book.description); - // This complicated chain of and_then's is so we can move - // "output.html.destination" to "build.build_dir" and parse it into a + // This complicated chain of and_then's is so we can move + // "output.html.destination" to "build.build_dir" and parse it into a // PathBuf. - let destination: Option = table.get_mut("output") + let destination: Option = table + .get_mut("output") .and_then(|output| output.as_table_mut()) .and_then(|output| output.get_mut("html")) .and_then(|html| html.as_table_mut()) @@ -171,13 +174,15 @@ impl<'de> Deserialize<'de> for Config { return Ok(Config::from_legacy(table)); } - let book: BookConfig = table.remove("book") - .and_then(|value| value.try_into().ok()) - .unwrap_or_default(); + let book: BookConfig = table + .remove("book") + .and_then(|value| value.try_into().ok()) + .unwrap_or_default(); - let build: BuildConfig = table.remove("build") - .and_then(|value| value.try_into().ok()) - .unwrap_or_default(); + let build: BuildConfig = table + .remove("build") + .and_then(|value| value.try_into().ok()) + .unwrap_or_default(); Ok(Config { book: book, @@ -200,7 +205,7 @@ impl Serialize for Config { }; table.insert("book".to_string(), book_config); - + Value::Table(table).serialize(s) } } @@ -208,7 +213,9 @@ impl Serialize for Config { fn is_legacy_format(table: &Table) -> bool { let top_level_items = ["title", "author", "authors"]; - top_level_items.iter().any(|key| table.contains_key(&key.to_string())) + top_level_items + .iter() + .any(|key| table.contains_key(&key.to_string())) } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index fe32b3876c..6dbce5812c 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -2,8 +2,75 @@ pub use self::html_handlebars::HtmlHandlebars; mod html_handlebars; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use serde_json; + use errors::*; +use config::Config; +use book::{Book, MDBook}; + pub trait Renderer { - fn render(&self, book: &::book::MDBook) -> Result<()>; + fn render(&self, book: &MDBook) -> Result<()>; +} + + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RenderContext { + pub version: &'static str, + pub root: PathBuf, + pub book: Book, + pub config: Config, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct AlternateRenderer { + cmd: String, +} + +impl AlternateRenderer { + pub fn new>(cmd: S) -> AlternateRenderer { + AlternateRenderer { cmd: cmd.into() } + } +} + +impl Renderer for AlternateRenderer { + fn render(&self, book: &MDBook) -> Result<()> { + info!("Invoking the \"{}\" renderer", self.cmd); + let ctx = RenderContext::new(&book.root, book.book.clone(), book.config.clone()); + + let mut child = Command::new(&self.cmd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .chain_err(|| "Unable to start the renderer")?; + + serde_json::to_writer( + child.stdin.as_mut().expect("stdin is always attached"), + &ctx, + ).chain_err(|| "Error occurred while sending the render context to the renderer")?; + + let output = child.wait_with_output()?; + trace!("{} exited with output: {:?}", self.cmd, output); + + if !output.status.success() { + error!("Renderer exited with non-zero return code."); + bail!("Alternate renderer failed"); + } else { + Ok(()) + } + } +} + +impl RenderContext { + pub fn new>(root: P, book: Book, config: Config) -> RenderContext { + RenderContext { + book: book, + config: config, + version: env!("CARGO_PKG_VERSION"), + root: root.into(), + } + } } From 5e177a93511c7d0b08f41453e224a8e7f511b01a Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 12 Dec 2017 11:35:48 +1100 Subject: [PATCH 02/24] Added a CmdRenderer and the ability to have multiple renderers --- src/book/mod.rs | 14 +++++++++----- src/lib.rs | 2 +- src/renderer/mod.rs | 10 +++++----- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index 362e45359e..e4b3ed60ca 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -35,7 +35,7 @@ pub struct MDBook { pub config: Config, /// A representation of the book's contents in memory. pub book: Book, - renderer: Box, + renderers: Vec>, /// The URL used for live reloading when serving up the book. pub livereload: Option, @@ -84,7 +84,7 @@ impl MDBook { root: book_root, config: config, book: book, - renderer: Box::new(HtmlHandlebars::new()), + renderers: vec![Box::new(HtmlHandlebars::new())], livereload: None, }) } @@ -150,7 +150,11 @@ impl MDBook { utils::fs::remove_dir_content(&dest).chain_err(|| "Unable to clear output directory")?; } - self.renderer.render(self) + for renderer in &self.renderers { + renderer.render(self).chain_err(|| "Rendering failed")?; + } + + Ok(()) } // FIXME: This doesn't belong as part of `MDBook`. It is only used by the HTML renderer @@ -166,8 +170,8 @@ impl MDBook { /// You can change the default renderer to another one by using this method. /// The only requirement is for your renderer to implement the [Renderer /// trait](../../renderer/renderer/trait.Renderer.html) - pub fn set_renderer(mut self, renderer: R) -> Self { - self.renderer = Box::new(renderer); + pub fn with_renderer(&mut self, renderer: R) -> &mut Self { + self.renderers.push(Box::new(renderer)); self } diff --git a/src/lib.rs b/src/lib.rs index 3efef1d8d3..2f9c6a4aa3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,7 +69,7 @@ //! # let your_renderer = HtmlHandlebars::new(); //! # //! let mut book = MDBook::load("my-book").unwrap(); -//! book.set_renderer(your_renderer); +//! book.with_renderer(your_renderer); //! # } //! ``` //! diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 6dbce5812c..8d1182968b 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -25,17 +25,17 @@ pub struct RenderContext { } #[derive(Debug, Clone, PartialEq)] -pub struct AlternateRenderer { +pub struct CmdRenderer { cmd: String, } -impl AlternateRenderer { - pub fn new>(cmd: S) -> AlternateRenderer { - AlternateRenderer { cmd: cmd.into() } +impl CmdRenderer { + pub fn new>(cmd: S) -> CmdRenderer { + CmdRenderer { cmd: cmd.into() } } } -impl Renderer for AlternateRenderer { +impl Renderer for CmdRenderer { fn render(&self, book: &MDBook) -> Result<()> { info!("Invoking the \"{}\" renderer", self.cmd); let ctx = RenderContext::new(&book.root, book.book.clone(), book.config.clone()); From fea52b35eec8cc7bf2438448d0ab9078bffd9f8d Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 12 Dec 2017 11:49:29 +1100 Subject: [PATCH 03/24] Made MDBook::load() autodetect renderers --- src/book/mod.rs | 31 +++++++++++++++++++++++-------- src/config.rs | 8 ++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index e4b3ed60ca..417bffb2ae 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -21,7 +21,7 @@ use std::process::Command; use tempdir::TempDir; use utils; -use renderer::{HtmlHandlebars, Renderer}; +use renderer::{CmdRenderer, HtmlHandlebars, Renderer}; use preprocess; use errors::*; @@ -75,17 +75,32 @@ impl MDBook { /// Load a book from its root directory using a custom config. pub fn load_with_config>(book_root: P, config: Config) -> Result { - let book_root = book_root.into(); + let root = book_root.into(); - let src_dir = book_root.join(&config.book.src); + let src_dir = root.join(&config.book.src); let book = book::load_book(&src_dir, &config.build)?; + let livereload = None; + + let mut renderers: Vec> = Vec::new(); + + for name in config.renderers() { + if name == "html" { + renderers.push(Box::new(HtmlHandlebars::new())); + } else { + renderers.push(Box::new(CmdRenderer::new(name))); + } + } + + if renderers.is_empty() { + renderers.push(Box::new(HtmlHandlebars::new())); + } Ok(MDBook { - root: book_root, - config: config, - book: book, - renderers: vec![Box::new(HtmlHandlebars::new())], - livereload: None, + root, + config, + book, + renderers, + livereload, }) } diff --git a/src/config.rs b/src/config.rs index e3cd1b1b45..2c55436321 100644 --- a/src/config.rs +++ b/src/config.rs @@ -111,6 +111,14 @@ impl Config { cfg.rest = table; cfg } + + /// Get a list of the available renderers. + pub fn renderers(&self) -> Vec { + self.get("output") + .and_then(|o| o.as_table()) + .map(|t| t.keys().cloned().collect()) + .unwrap_or_default() + } } fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> { From 87253e6398bf889ac684a12eeb20525200e58e06 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 12 Dec 2017 11:58:31 +1100 Subject: [PATCH 04/24] Added a couple methods to RenderContext --- src/renderer/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 8d1182968b..4ebf4064a1 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -24,6 +24,16 @@ pub struct RenderContext { pub config: Config, } +impl RenderContext { + pub fn source_dir(&self) -> PathBuf { + self.root.join(&self.config.book.src) + } + + pub fn build_dir(&self) -> PathBuf { + self.root.join(&self.config.build.build_dir) + } +} + #[derive(Debug, Clone, PartialEq)] pub struct CmdRenderer { cmd: String, @@ -57,7 +67,7 @@ impl Renderer for CmdRenderer { if !output.status.success() { error!("Renderer exited with non-zero return code."); - bail!("Alternate renderer failed"); + bail!("The \"{}\" renderer failed", self.cmd); } else { Ok(()) } From 1ae03621c67d4abc047f8d8d5683e14ffa50f993 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 12 Dec 2017 12:04:25 +1100 Subject: [PATCH 05/24] Converted RenderContext.version to a String --- src/renderer/mod.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 4ebf4064a1..162ffd9e47 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -18,13 +18,22 @@ pub trait Renderer { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RenderContext { - pub version: &'static str, + pub version: String, pub root: PathBuf, pub book: Book, pub config: Config, } impl RenderContext { + pub fn new>(root: P, book: Book, config: Config) -> RenderContext { + RenderContext { + book: book, + config: config, + version: env!("CARGO_PKG_VERSION").to_string(), + root: root.into(), + } + } + pub fn source_dir(&self) -> PathBuf { self.root.join(&self.config.book.src) } @@ -73,14 +82,3 @@ impl Renderer for CmdRenderer { } } } - -impl RenderContext { - pub fn new>(root: P, book: Book, config: Config) -> RenderContext { - RenderContext { - book: book, - config: config, - version: env!("CARGO_PKG_VERSION"), - root: root.into(), - } - } -} From f606e49128354c1b1bf3cd63384b5efd09809454 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 12 Dec 2017 14:06:54 +1100 Subject: [PATCH 06/24] Made sure all alternate renderers are invoked as `mdbook-*` --- src/renderer/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 162ffd9e47..373705253b 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -58,8 +58,9 @@ impl Renderer for CmdRenderer { fn render(&self, book: &MDBook) -> Result<()> { info!("Invoking the \"{}\" renderer", self.cmd); let ctx = RenderContext::new(&book.root, book.book.clone(), book.config.clone()); + let cmd = format!("mdbook-{}", self.cmd); - let mut child = Command::new(&self.cmd) + let mut child = Command::new(cmd) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) From 89f80af0b774cd236b153e58d8c91c1a4a7deaf1 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 12 Dec 2017 20:00:42 +1100 Subject: [PATCH 07/24] Factored out the logic for determining which renderer to use --- src/book/mod.rs | 65 ++++++++++++++++---- src/config.rs | 8 --- src/renderer/html_handlebars/hbs_renderer.rs | 4 ++ src/renderer/mod.rs | 13 ++-- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index 417bffb2ae..f78d9c3934 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -81,19 +81,7 @@ impl MDBook { let book = book::load_book(&src_dir, &config.build)?; let livereload = None; - let mut renderers: Vec> = Vec::new(); - - for name in config.renderers() { - if name == "html" { - renderers.push(Box::new(HtmlHandlebars::new())); - } else { - renderers.push(Box::new(CmdRenderer::new(name))); - } - } - - if renderers.is_empty() { - renderers.push(Box::new(HtmlHandlebars::new())); - } + let renderers = determine_renderers(&config); Ok(MDBook { root, @@ -254,3 +242,54 @@ impl MDBook { } } } + +/// Look at the `Config` and try to figure out what renderers to use. +fn determine_renderers(config: &Config) -> Vec> { + let mut renderers: Vec> = Vec::new(); + + if let Some(output_table) = config.get("output").and_then(|o| o.as_table()) { + for (key, table) in output_table.iter() { + if key == "html" { + renderers.push(Box::new(HtmlHandlebars::new())); + } else { + // look for the `command` field, falling back to using the key + // prepended by "mdbook-" + let command = table + .get("command") + .and_then(|c| c.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("mdbook-{}", key)); + + renderers.push(Box::new(CmdRenderer::new( + key.to_string(), + command.to_string(), + ))); + } + } + } + + // if we couldn't find anything, add the HTML renderer as a default + if renderers.is_empty() { + renderers.push(Box::new(HtmlHandlebars::new())); + } + + renderers +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn config_defaults_to_html_renderer_if_empty() { + let cfg = Config::default(); + + // make sure we haven't got anything in the `output` table + assert!(cfg.get("output").is_none()); + + let got = determine_renderers(&cfg); + + assert_eq!(got.len(), 1); + assert_eq!(got[0].name(), "html"); + } +} diff --git a/src/config.rs b/src/config.rs index 2c55436321..e3cd1b1b45 100644 --- a/src/config.rs +++ b/src/config.rs @@ -111,14 +111,6 @@ impl Config { cfg.rest = table; cfg } - - /// Get a list of the available renderers. - pub fn renderers(&self) -> Vec { - self.get("output") - .and_then(|o| o.as_table()) - .map(|t| t.keys().cloned().collect()) - .unwrap_or_default() - } } fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> { diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 28b49376d5..70bf490467 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -245,6 +245,10 @@ impl HtmlHandlebars { impl Renderer for HtmlHandlebars { + fn name(&self) -> &str { + "html" + } + fn render(&self, book: &MDBook) -> Result<()> { let html_config = book.config.html_config().unwrap_or_default(); let src_dir = book.root.join(&book.config.book.src); diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 373705253b..c0c20b4b31 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -12,6 +12,7 @@ use book::{Book, MDBook}; pub trait Renderer { + fn name(&self) -> &str; fn render(&self, book: &MDBook) -> Result<()>; } @@ -45,22 +46,26 @@ impl RenderContext { #[derive(Debug, Clone, PartialEq)] pub struct CmdRenderer { + name: String, cmd: String, } impl CmdRenderer { - pub fn new>(cmd: S) -> CmdRenderer { - CmdRenderer { cmd: cmd.into() } + pub fn new(name: String, cmd: String) -> CmdRenderer { + CmdRenderer { name, cmd } } } impl Renderer for CmdRenderer { + fn name(&self) -> &str { + &self.name + } + fn render(&self, book: &MDBook) -> Result<()> { info!("Invoking the \"{}\" renderer", self.cmd); let ctx = RenderContext::new(&book.root, book.book.clone(), book.config.clone()); - let cmd = format!("mdbook-{}", self.cmd); - let mut child = Command::new(cmd) + let mut child = Command::new(&self.cmd) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) From ecccf9f46eb2f8d7e4acf7cfea28fbbd8c65db1f Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 12 Dec 2017 20:40:02 +1100 Subject: [PATCH 08/24] Added tests for renderer detection --- src/book/mod.rs | 26 ++++++++++++++++++++++++++ src/config.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/book/mod.rs b/src/book/mod.rs index f78d9c3934..c5bb1610f4 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -279,6 +279,7 @@ fn determine_renderers(config: &Config) -> Vec> { #[cfg(test)] mod tests { use super::*; + use toml::value::{Table, Value}; #[test] fn config_defaults_to_html_renderer_if_empty() { @@ -292,4 +293,29 @@ mod tests { assert_eq!(got.len(), 1); assert_eq!(got[0].name(), "html"); } + + #[test] + fn add_a_random_renderer_to_the_config() { + let mut cfg = Config::default(); + cfg.set("output.random", Table::new()).unwrap(); + + let got = determine_renderers(&cfg); + + assert_eq!(got.len(), 1); + assert_eq!(got[0].name(), "random"); + } + + #[test] + fn add_a_random_renderer_with_custom_command_to_the_config() { + let mut cfg = Config::default(); + + let mut table = Table::new(); + table.insert("command".to_string(), Value::String("false".to_string())); + cfg.set("output.random", table).unwrap(); + + let got = determine_renderers(&cfg); + + assert_eq!(got.len(), 1); + assert_eq!(got[0].name(), "random"); + } } diff --git a/src/config.rs b/src/config.rs index e3cd1b1b45..1ddb4a96fb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -74,6 +74,16 @@ impl Config { } } + /// Set a config key, clobbering any existing values along the way. + pub fn set>(&mut self, index: I, value: S) -> Result<()> { + let pieces: Vec<_> = index.as_ref().split(".").collect(); + let value = + Value::try_from(value).chain_err(|| "Unable to represent the item as a JSON Value")?; + recursive_set(&pieces, &mut self.rest, value); + + Ok(()) + } + fn from_legacy(mut table: Table) -> Config { let mut cfg = Config::default(); @@ -113,6 +123,26 @@ impl Config { } } +fn recursive_set(key: &[&str], table: &mut Table, value: Value) { + if key.is_empty() { + unreachable!(); + } else if key.len() == 1 { + table.insert(key[0].to_string(), value); + } else { + let first = key[0]; + let rest = &key[1..]; + + // if `table[first]` isn't a table, replace whatever is there with a + // new table. + if table.get(first).and_then(|t| t.as_table()).is_none() { + table.insert(first.to_string(), Value::Table(Table::new())); + } + + let nested = table.get_mut(first).and_then(|t| t.as_table_mut()).unwrap(); + recursive_set(rest, nested, value); + } +} + fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> { if key.is_empty() { return None; @@ -457,4 +487,17 @@ mod tests { assert_eq!(got.build, build_should_be); assert_eq!(got.html_config().unwrap(), html_should_be); } + + #[test] + fn set_a_config_item() { + let mut cfg = Config::default(); + let key = "foo.bar.baz"; + let value = "Something Interesting"; + + assert!(cfg.get(key).is_none()); + cfg.set(key, value).unwrap(); + + let got: String = cfg.get_deserialized(key).unwrap(); + assert_eq!(got, value); + } } From 0b0ae6e84383170e1d6c34cf5059cd92a45815f8 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 12 Dec 2017 21:26:05 +1100 Subject: [PATCH 09/24] Made it so `mdbook test` works on the book-example again --- book-example/book.toml | 2 ++ book-example/src/SUMMARY.md | 2 +- book-example/src/format/config.md | 36 ++++++++++++++++++++++--------- book-example/src/lib/index.md | 14 ++++++++++++ book-example/src/lib/lib.md | 24 --------------------- 5 files changed, 43 insertions(+), 35 deletions(-) create mode 100644 book-example/src/lib/index.md delete mode 100644 book-example/src/lib/lib.md diff --git a/book-example/book.toml b/book-example/book.toml index e730b43ef9..4fd264def1 100644 --- a/book-example/book.toml +++ b/book-example/book.toml @@ -5,3 +5,5 @@ author = "Mathieu David" [output.html] mathjax-support = true + +[output.epub] \ No newline at end of file diff --git a/book-example/src/SUMMARY.md b/book-example/src/SUMMARY.md index ff3911c72c..dd703380df 100644 --- a/book-example/src/SUMMARY.md +++ b/book-example/src/SUMMARY.md @@ -15,6 +15,6 @@ - [Syntax highlighting](format/theme/syntax-highlighting.md) - [MathJax Support](format/mathjax.md) - [Rust code specific features](format/rust.md) -- [Rust Library](lib/lib.md) +- [For Developers](lib/index.md) ----------- [Contributors](misc/contributors.md) diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md index d1980a21be..457fe51c6e 100644 --- a/book-example/src/format/config.md +++ b/book-example/src/format/config.md @@ -69,8 +69,6 @@ renderer need to be specified under the TOML table `[output.html]`. The following configuration options are available: - pub playpen: Playpen, - - **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. @@ -125,6 +123,15 @@ author can take advantage of serde to deserialize the generic `toml::Value` object retrieved from `Config` into a struct specific to its use case. ```rust +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate toml; +extern crate mdbook; + +use toml::Value; +use mdbook::config::Config; + #[derive(Debug, Deserialize, PartialEq)] struct RandomOutput { foo: u32, @@ -132,6 +139,7 @@ struct RandomOutput { baz: Vec, } +# fn run() -> Result<(), Box<::std::error::Error>> { let src = r#" [output.random] foo = 5 @@ -140,16 +148,24 @@ baz = [true, true, false] "#; let book_config = Config::from_str(src)?; // usually passed in by mdbook -let random: Value = book_config.get("output.random").unwrap_or_default(); -let got: RandomOutput = random.try_into()?; +let random = book_config.get("output.random") + .cloned() + .ok_or("output.random not found")?; +let got: RandomOutput = random.try_into()?; -assert_eq!(got, should_be); +let should_be = RandomOutput { + foo: 5, + bar: "Hello World".to_string(), + baz: vec![true, true, false] +}; -if let Some(baz) = book_config.get_deserialized::>("output.random.baz") { - println!("{:?}", baz); // prints [true, true, false] +assert_eq!(got, should_be); - // do something interesting with baz -} +let baz: Vec = book_config.get_deserialized("output.random.baz")?; +println!("{:?}", baz); // prints [true, true, false] -// start the rendering process +// do something interesting with baz +# Ok(()) +# } +# fn main() { run().unwrap() } ``` diff --git a/book-example/src/lib/index.md b/book-example/src/lib/index.md new file mode 100644 index 0000000000..2ee85cbc09 --- /dev/null +++ b/book-example/src/lib/index.md @@ -0,0 +1,14 @@ +# For Developers + +While `mdbook` is mainly used as a command line tool, you can also import the +underlying library directly and use that to manage a book. + +- Creating custom backends +- Automatically generating and reloading a book on the fly +- Integration with existing projects + +The best source for examples on using the `mdbook` crate from your own Rust +programs is the [API Docs]. + + +[API Docs]: https://docs.rs/mdbook \ No newline at end of file diff --git a/book-example/src/lib/lib.md b/book-example/src/lib/lib.md deleted file mode 100644 index 269e8c3199..0000000000 --- a/book-example/src/lib/lib.md +++ /dev/null @@ -1,24 +0,0 @@ -# Rust Library - -mdBook is not only a command line tool, it can be used as a crate. You can extend it, -integrate it in current projects. Here is a short example: - -```rust,ignore -extern crate mdbook; - -use mdbook::MDBook; -use std::path::Path; - -# #[allow(unused_variables)] -fn main() { - let mut book = MDBook::new("my-book") // Path to root - .with_source("src") // Path from root to source directory - .with_destination("book") // Path from root to output directory - .read_config() // Parse book.toml or book.json configuration file - .expect("I don't handle configuration file error, but you should!"); - - book.build().unwrap(); // Render the book -} -``` - -Check here for the [API docs](mdbook/index.html) generated by rustdoc. From e6db026d9d2df3322b46bb3743e41842723a3ae3 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 12 Dec 2017 21:42:12 +1100 Subject: [PATCH 10/24] Updated the "For Developers" docs --- book-example/src/lib/index.md | 39 ++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/book-example/src/lib/index.md b/book-example/src/lib/index.md index 2ee85cbc09..d2827d5450 100644 --- a/book-example/src/lib/index.md +++ b/book-example/src/lib/index.md @@ -11,4 +11,41 @@ The best source for examples on using the `mdbook` crate from your own Rust programs is the [API Docs]. -[API Docs]: https://docs.rs/mdbook \ No newline at end of file +## Alternate Backends + +The mechanism for using alternative backends is very simple, you add an extra +table to your `book.toml` and the `MDBook::load()` function will detect the + +For example, if you wanted to use a hypothetical `latex` backend you would add +an empty `output.latex` table to `book.toml`. + +```toml +# book.toml + +[book] +... + +[output.latex] +``` + +And then during the rendering stage `mdbook` will run the `mdbook-latex` +program, piping it a JSON serialized [RenderContext] via stdin. + +You can set the command used via the `command` key. + +```toml +# book.toml + +[book] +... + +[output.latex] +command = "python3 my_plugin.py" +``` + +If no backend is supplied (i.e. there are no `output.*` tables), `mdbook` will +fall back to the `html` backend. + + +[API Docs]: https://docs.rs/mdbook +[RenderContext]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html \ No newline at end of file From d93e62db73a1da6f37961b48c30e6eedc547414c Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 12 Dec 2017 23:16:12 +1100 Subject: [PATCH 11/24] Removed `[output.epub]` from the example book's book.toml --- book-example/book.toml | 2 -- tests/rendered_output.rs | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/book-example/book.toml b/book-example/book.toml index 4fd264def1..e730b43ef9 100644 --- a/book-example/book.toml +++ b/book-example/book.toml @@ -5,5 +5,3 @@ author = "Mathieu David" [output.html] mathjax-support = true - -[output.epub] \ No newline at end of file diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index dd630847fb..d1fb589936 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -304,8 +304,7 @@ fn example_book_can_build() { let mut md = MDBook::load(example_book_dir.path()).unwrap(); - let got = md.build(); - assert!(got.is_ok()); + md.build().unwrap(); } #[test] From 80850f3024e5874a39c5dca7448e362e8d279370 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 23 Dec 2017 03:13:24 +0800 Subject: [PATCH 12/24] Added a bit more info on how backends should work --- book-example/src/lib/index.md | 66 +++++++++++++++++++++++++++++++++-- src/renderer/mod.rs | 6 ++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/book-example/src/lib/index.md b/book-example/src/lib/index.md index d2827d5450..f01a488beb 100644 --- a/book-example/src/lib/index.md +++ b/book-example/src/lib/index.md @@ -11,10 +11,11 @@ The best source for examples on using the `mdbook` crate from your own Rust programs is the [API Docs]. -## Alternate Backends +## Configuration The mechanism for using alternative backends is very simple, you add an extra -table to your `book.toml` and the `MDBook::load()` function will detect the +table to your `book.toml` and the `MDBook::load()` function will automatically +detect the backends being used. For example, if you wanted to use a hypothetical `latex` backend you would add an empty `output.latex` table to `book.toml`. @@ -47,5 +48,64 @@ If no backend is supplied (i.e. there are no `output.*` tables), `mdbook` will fall back to the `html` backend. +## Render Context + +The `RenderContext` encapsulates all the information a backend needs to know +in order to generate output. Its Rust definition looks something like this: + +```rust +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RenderContext { + pub version: String, + pub root: PathBuf, + pub book: Book, + pub config: Config, + pub destination: PathBuf, +} +``` + +A backend will receive the `RenderContext` via `stdin` as one big JSON blob. If +possible, it is recommended to import the `mdbook` crate and use the +`RenderContext::from_json()` method. This way you should always be able to +deserialize the `RenderContext`, and as a bonus will also have access to the +methods already defined on the underlying types. + +Although backends are told the book's root directory on disk, it is *strongly +discouraged* to load chapter content from the filesystem. The `root` key is +provided as an escape hatch for certain plugins which may load additional, +non-markdown, files. + + +## Output Directory + +To make things more deterministic, a backend will be told where it should place +its generated artefacts. + +The general algorithm for deciding the output directory goes something like +this: + +- If there is only one backend: + - `destination` is `config.build.build_dir` (usually `book/`) +- Otherwise: + - `destination` is `config.build.build_dir` joined with the backend's name + (e.g. `build/latex/` for the "latex" backend) + + +## Output and Signalling Failure + +To signal that the plugin failed it just needs to exit with a non-zero return +code. + +All output from the plugin's subprocess is immediately passed through to the +user, so it is encouraged for plugins to follow the ["rule of silence"] and +by default only tell the user about things they directly need to respond to +(e.g. an error in generation or a warning). + +This "silent by default" behaviour can be overridden via the `RUST_LOG` +environment variable (which `mdbook` will pass through to the backend if set) +as is typical with Rust applications. + + [API Docs]: https://docs.rs/mdbook -[RenderContext]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html \ No newline at end of file +[RenderContext]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html +["rule of silence"]: http://www.linfo.org/rule_of_silence.html \ No newline at end of file diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index c0c20b4b31..9ec00731ec 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -2,6 +2,7 @@ pub use self::html_handlebars::HtmlHandlebars; mod html_handlebars; +use std::io::Read; use std::path::PathBuf; use std::process::{Command, Stdio}; use serde_json; @@ -42,6 +43,11 @@ impl RenderContext { pub fn build_dir(&self) -> PathBuf { self.root.join(&self.config.build.build_dir) } + + pub fn from_json(reader: R) -> Result { + serde_json::from_reader(reader) + .chain_err(|| "Unable to deserialize the `RenderContext`") + } } #[derive(Debug, Clone, PartialEq)] From 07f6c8e3fff19ca4d8362826d4d42b47cecb5d0c Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 25 Dec 2017 12:09:50 +0800 Subject: [PATCH 13/24] Added a `destination` key to the RenderContext --- src/book/mod.rs | 2 ++ src/renderer/mod.rs | 27 +++++++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index c5bb1610f4..5918a1bb42 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -260,9 +260,11 @@ fn determine_renderers(config: &Config) -> Vec> { .map(|s| s.to_string()) .unwrap_or_else(|| format!("mdbook-{}", key)); + // TODO: Actually calculate the destination directory renderers.push(Box::new(CmdRenderer::new( key.to_string(), command.to_string(), + &config.build.build_dir, ))); } } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 9ec00731ec..336842a6da 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -11,28 +11,32 @@ use errors::*; use config::Config; use book::{Book, MDBook}; - pub trait Renderer { fn name(&self) -> &str; fn render(&self, book: &MDBook) -> Result<()>; } - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RenderContext { pub version: String, pub root: PathBuf, pub book: Book, pub config: Config, + pub destination: PathBuf, } impl RenderContext { - pub fn new>(root: P, book: Book, config: Config) -> RenderContext { + pub fn new(root: P, book: Book, config: Config, destination: Q) -> RenderContext + where + P: Into, + Q: Into, + { RenderContext { book: book, config: config, version: env!("CARGO_PKG_VERSION").to_string(), root: root.into(), + destination: destination.into(), } } @@ -54,11 +58,17 @@ impl RenderContext { pub struct CmdRenderer { name: String, cmd: String, + destination: PathBuf, } impl CmdRenderer { - pub fn new(name: String, cmd: String) -> CmdRenderer { - CmdRenderer { name, cmd } + pub fn new>(name: String, cmd: String, destination: P) -> CmdRenderer { + let destination = destination.into(); + CmdRenderer { + name, + cmd, + destination, + } } } @@ -69,7 +79,12 @@ impl Renderer for CmdRenderer { fn render(&self, book: &MDBook) -> Result<()> { info!("Invoking the \"{}\" renderer", self.cmd); - let ctx = RenderContext::new(&book.root, book.book.clone(), book.config.clone()); + let ctx = RenderContext::new( + &book.root, + book.book.clone(), + book.config.clone(), + &self.destination, + ); let mut child = Command::new(&self.cmd) .stdin(Stdio::piped()) From 90328c7b26d52b7ca192efc47c90255130e80ca6 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 26 Dec 2017 21:26:37 +0800 Subject: [PATCH 14/24] Altered how we wait for an alternate backend to finish --- src/renderer/mod.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 336842a6da..678a0a5f0e 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -88,8 +88,6 @@ impl Renderer for CmdRenderer { let mut child = Command::new(&self.cmd) .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) .spawn() .chain_err(|| "Unable to start the renderer")?; @@ -98,10 +96,10 @@ impl Renderer for CmdRenderer { &ctx, ).chain_err(|| "Error occurred while sending the render context to the renderer")?; - let output = child.wait_with_output()?; - trace!("{} exited with output: {:?}", self.cmd, output); + let status = child.wait()?; + trace!("{} exited with output: {:?}", self.cmd, status); - if !output.status.success() { + if !status.success() { error!("Renderer exited with non-zero return code."); bail!("The \"{}\" renderer failed", self.cmd); } else { From dbba75e68bc89bf8d626f7995ed7300e732e7e2c Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Wed, 3 Jan 2018 17:39:21 +0800 Subject: [PATCH 15/24] Refactored the Renderer trait to not use MDBook and moved livereload to the template --- src/bin/build.rs | 2 +- src/bin/serve.rs | 40 +--- src/bin/watch.rs | 9 +- src/book/mod.rs | 79 +++++-- src/config.rs | 8 + src/renderer/html_handlebars/hbs_renderer.rs | 224 ++++++++++--------- src/renderer/mod.rs | 15 +- src/theme/index.hbs | 16 +- 8 files changed, 225 insertions(+), 168 deletions(-) diff --git a/src/bin/build.rs b/src/bin/build.rs index bc784ea983..adec879bab 100644 --- a/src/bin/build.rs +++ b/src/bin/build.rs @@ -30,7 +30,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { book.build()?; if args.is_present("open") { - open(book.get_destination().join("index.html")); + open(book.build_dir_for("html").join("index.html")); } Ok(()) diff --git a/src/bin/serve.rs b/src/bin/serve.rs index ac6a51e5e8..99f20e3ba7 100644 --- a/src/bin/serve.rs +++ b/src/bin/serve.rs @@ -38,8 +38,6 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { // Watch command implementation pub fn execute(args: &ArgMatches) -> Result<()> { - const RELOAD_COMMAND: &'static str = "reload"; - let book_dir = get_book_dir(args); let mut book = MDBook::load(&book_dir)?; @@ -52,29 +50,13 @@ pub fn execute(args: &ArgMatches) -> Result<()> { let address = format!("{}:{}", interface, port); let ws_address = format!("{}:{}", interface, ws_port); - let livereload = Some(format!( - r#" - -"#, - public_address, ws_port, RELOAD_COMMAND - )); - book.livereload = livereload.clone(); + let livereload_url = format!("ws://{}:{}", public_address, ws_port); + book.config + .set("output.html.livereload-url", &livereload_url)?; book.build()?; - let mut chain = Chain::new(staticfile::Static::new(book.get_destination())); + let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html"))); chain.link_after(ErrorRecover); let _iron = Iron::new(chain) .http(&*address) @@ -90,7 +72,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { }); let serving_url = format!("http://{}", address); - println!("\nServing on: {}", serving_url); + info!("Serving on: {}", serving_url); if open_browser { open(serving_url); @@ -100,13 +82,13 @@ pub fn execute(args: &ArgMatches) -> Result<()> { watch::trigger_on_change(&mut book, move |path, book_dir| { println!("File changed: {:?}\nBuilding book...\n", path); // FIXME: This area is really ugly because we need to re-set livereload :( - - let livereload = livereload.clone(); + + let livereload_url = livereload_url.clone(); let result = MDBook::load(&book_dir) - .map(move |mut b| { - b.livereload = livereload; - b + .and_then(move |mut b| { + b.config.set("output.html.livereload-url", &livereload_url)?; + Ok(b) }) .and_then(|mut b| b.build()); @@ -117,7 +99,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { error!("\tCaused By: {}", cause); } } else { - let _ = broadcaster.send(RELOAD_COMMAND); + let _ = broadcaster.send("reload"); } }); diff --git a/src/bin/watch.rs b/src/bin/watch.rs index 536e615019..94b51999e0 100644 --- a/src/bin/watch.rs +++ b/src/bin/watch.rs @@ -26,7 +26,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { if args.is_present("open") { book.build()?; - open(book.get_destination().join("index.html")); + open(book.build_dir_for("html").join("index.html")); } trigger_on_change(&book, |path, book_dir| { @@ -56,14 +56,14 @@ where let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) { Ok(w) => w, Err(e) => { - println!("Error while trying to watch the files:\n\n\t{:?}", e); + error!("Error while trying to watch the files:\n\n\t{:?}", e); ::std::process::exit(1) } }; // Add the source directory to the watcher if let Err(e) = watcher.watch(book.source_dir(), Recursive) { - println!("Error while watching {:?}:\n {:?}", book.source_dir(), e); + error!("Error while watching {:?}:\n {:?}", book.source_dir(), e); ::std::process::exit(1); }; @@ -72,9 +72,10 @@ where // Add the book.toml file to the watcher if it exists let _ = watcher.watch(book.root.join("book.toml"), NonRecursive); - println!("\nListening for changes...\n"); + info!("Listening for changes..."); for event in rx.iter() { + debug!("Received filesystem event: {:?}", event); match event { Create(path) | Write(path) | Remove(path) | Rename(_, path) => { closure(&path, &book.root); diff --git a/src/book/mod.rs b/src/book/mod.rs index 5918a1bb42..76c44ef1b3 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -15,13 +15,13 @@ pub use self::book::{load_book, Book, BookItem, BookItems, Chapter}; pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; pub use self::init::BookBuilder; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::io::Write; use std::process::Command; use tempdir::TempDir; use utils; -use renderer::{CmdRenderer, HtmlHandlebars, Renderer}; +use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer}; use preprocess; use errors::*; @@ -148,26 +148,33 @@ impl MDBook { pub fn build(&mut self) -> Result<()> { debug!("[fn]: build"); - let dest = self.get_destination(); - if dest.exists() { - utils::fs::remove_dir_content(&dest).chain_err(|| "Unable to clear output directory")?; - } - for renderer in &self.renderers { - renderer.render(self).chain_err(|| "Rendering failed")?; - } + let name = renderer.name(); + let build_dir = self.build_dir_for(name); + if build_dir.exists() { + debug!( + "Cleaning build dir for the \"{}\" renderer ({})", + name, + build_dir.display() + ); + + utils::fs::remove_dir_content(&build_dir) + .chain_err(|| "Unable to clear output directory")?; + } - Ok(()) - } + let render_context = RenderContext::new( + self.root.clone(), + self.book.clone(), + self.config.clone(), + build_dir, + ); - // FIXME: This doesn't belong as part of `MDBook`. It is only used by the HTML renderer - #[doc(hidden)] - pub fn write_file>(&self, filename: P, content: &[u8]) -> Result<()> { - let path = self.get_destination().join(filename); + renderer + .render(&render_context) + .chain_err(|| "Rendering failed")?; + } - utils::fs::create_file(&path)? - .write_all(content) - .map_err(|e| e.into()) + Ok(()) } /// You can change the default renderer to another one by using this method. @@ -222,10 +229,38 @@ impl MDBook { Ok(()) } - // FIXME: This doesn't belong under `MDBook`, it should really be passed to the renderer directly. - #[doc(hidden)] - pub fn get_destination(&self) -> PathBuf { - self.root.join(&self.config.build.build_dir) + /// The logic for determining where a backend should put its build + /// artefacts. + /// + /// If there is only 1 renderer, put it in the directory pointed to by the + /// `build.build_dir` key in `Config`. If there is more than one then the + /// renderer gets its own directory within the main build dir. + /// + /// i.e. If there were only one renderer (in this case, the HTML renderer): + /// + /// - build/ + /// - index.html + /// - ... + /// + /// Otherwise if there are multiple: + /// + /// - build/ + /// - epub/ + /// - my_awesome_book.epub + /// - html/ + /// - index.html + /// - ... + /// - latex/ + /// - my_awesome_book.tex + /// + pub fn build_dir_for(&self, backend_name: &str) -> PathBuf { + let build_dir = self.root.join(&self.config.build.build_dir); + + if self.renderers.len() <= 1 { + build_dir + } else { + build_dir.join(backend_name) + } } /// Get the directory containing this book's source files. diff --git a/src/config.rs b/src/config.rs index 1ddb4a96fb..50eb71f413 100644 --- a/src/config.rs +++ b/src/config.rs @@ -308,6 +308,14 @@ pub struct HtmlConfig { pub additional_css: Vec, pub additional_js: Vec, pub playpen: Playpen, + /// This is used as a bit of a workaround for the `mdbook serve` command. + /// Basically, because you set the websocket port from the command line, the + /// `mdbook serve` command needs a way to let the HTML renderer know where + /// to point livereloading at, if it has been enabled. + /// + /// This config item *should not be edited* by the end user. + #[doc(hidden)] + pub livereload_url: Option, } /// Configuration for tweaking how the the HTML renderer handles the playpen. diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 70bf490467..ac256eaf8d 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -1,18 +1,17 @@ use renderer::html_handlebars::helpers; use preprocess; -use renderer::Renderer; -use book::MDBook; -use book::{BookItem, Chapter}; -use config::{Config, Playpen, HtmlConfig}; -use {utils, theme}; -use theme::{Theme, playpen_editor}; +use renderer::{RenderContext, Renderer}; +use book::{Book, BookItem, Chapter}; +use config::{Config, HtmlConfig, Playpen}; +use {theme, utils}; +use theme::{playpen_editor, Theme}; use errors::*; use regex::{Captures, Regex}; #[allow(unused_imports)] use std::ascii::AsciiExt; use std::path::{Path, PathBuf}; use std::fs::{self, File}; -use std::io::{self, Read}; +use std::io::{self, Read, Write}; use std::collections::BTreeMap; use std::collections::HashMap; @@ -28,15 +27,28 @@ impl HtmlHandlebars { HtmlHandlebars } - fn render_item(&self, + fn write_file>( + &self, + build_dir: &Path, + filename: P, + content: &[u8], + ) -> Result<()> { + let path = build_dir.join(filename); + + utils::fs::create_file(&path)? + .write_all(content) + .map_err(|e| e.into()) + } + + fn render_item( + &self, item: &BookItem, mut ctx: RenderItemContext, - print_content: &mut String) - -> Result<()> { + print_content: &mut String, + ) -> Result<()> { // FIXME: This should be made DRY-er and rely less on mutable state match *item { - BookItem::Chapter(ref ch) => - { + BookItem::Chapter(ref ch) => { let content = ch.content.clone(); let base = ch.path.parent() .map(|dir| ctx.src_dir.join(dir)) @@ -83,18 +95,18 @@ impl HtmlHandlebars { let filepath = Path::new(&ch.path).with_extension("html"); let rendered = self.post_process( rendered, - &normalize_path(filepath.to_str().ok_or_else(|| Error::from( - format!("Bad file name: {}", filepath.display()), - ))?), - &ctx.book.config.html_config().unwrap_or_default().playpen, + &normalize_path(filepath.to_str().ok_or_else(|| { + Error::from(format!("Bad file name: {}", filepath.display())) + })?), + &ctx.html_config.playpen, ); // Write to file info!("[*] Creating {:?} ✓", filepath.display()); - ctx.book.write_file(filepath, &rendered.into_bytes())?; + self.write_file(&ctx.destination, filepath, &rendered.into_bytes())?; if ctx.is_index { - self.render_index(ctx.book, ch, &ctx.destination)?; + self.render_index(ch, &ctx.destination)?; } } _ => {} @@ -104,7 +116,7 @@ impl HtmlHandlebars { } /// Create an index.html from the first element in SUMMARY.md - fn render_index(&self, book: &MDBook, ch: &Chapter, destination: &Path) -> Result<()> { + fn render_index(&self, ch: &Chapter, destination: &Path) -> Result<()> { debug!("[*]: index.html"); let mut content = String::new(); @@ -120,10 +132,10 @@ impl HtmlHandlebars { .collect::>() .join("\n"); - book.write_file("index.html", content.as_bytes())?; + self.write_file(destination, "index.html", content.as_bytes())?; info!("[*] Creating index.html from {:?} ✓", - book.get_destination().join(&ch.path.with_extension("html"))); + destination.join(&ch.path.with_extension("html"))); Ok(()) } @@ -142,30 +154,57 @@ impl HtmlHandlebars { rendered } - fn copy_static_files(&self, book: &MDBook, theme: &Theme, html_config: &HtmlConfig) -> Result<()> { - book.write_file("book.js", &theme.js)?; - book.write_file("book.css", &theme.css)?; - book.write_file("favicon.png", &theme.favicon)?; - book.write_file("jquery.js", &theme.jquery)?; - book.write_file("highlight.css", &theme.highlight_css)?; - book.write_file("tomorrow-night.css", &theme.tomorrow_night_css)?; - book.write_file("ayu-highlight.css", &theme.ayu_highlight_css)?; - book.write_file("highlight.js", &theme.highlight_js)?; - book.write_file("clipboard.min.js", &theme.clipboard_js)?; - book.write_file("store.js", &theme.store_js)?; - book.write_file("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME)?; - book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot", - theme::FONT_AWESOME_EOT)?; - book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg", - theme::FONT_AWESOME_SVG)?; - book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf", - theme::FONT_AWESOME_TTF)?; - book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff", - theme::FONT_AWESOME_WOFF)?; - book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2", - theme::FONT_AWESOME_WOFF2)?; - book.write_file("_FontAwesome/fonts/FontAwesome.ttf", - theme::FONT_AWESOME_TTF)?; + fn copy_static_files( + &self, + destination: &Path, + theme: &Theme, + html_config: &HtmlConfig, + ) -> Result<()> { + self.write_file(destination, "book.js", &theme.js)?; + self.write_file(destination, "book.css", &theme.css)?; + self.write_file(destination, "favicon.png", &theme.favicon)?; + self.write_file(destination, "jquery.js", &theme.jquery)?; + self.write_file(destination, "highlight.css", &theme.highlight_css)?; + self.write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?; + self.write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?; + self.write_file(destination, "highlight.js", &theme.highlight_js)?; + self.write_file(destination, "clipboard.min.js", &theme.clipboard_js)?; + self.write_file(destination, "store.js", &theme.store_js)?; + self.write_file( + destination, + "_FontAwesome/css/font-awesome.css", + theme::FONT_AWESOME, + )?; + self.write_file( + destination, + "_FontAwesome/fonts/fontawesome-webfont.eot", + theme::FONT_AWESOME_EOT, + )?; + self.write_file( + destination, + "_FontAwesome/fonts/fontawesome-webfont.svg", + theme::FONT_AWESOME_SVG, + )?; + self.write_file( + destination, + "_FontAwesome/fonts/fontawesome-webfont.ttf", + theme::FONT_AWESOME_TTF, + )?; + self.write_file( + destination, + "_FontAwesome/fonts/fontawesome-webfont.woff", + theme::FONT_AWESOME_WOFF, + )?; + self.write_file( + destination, + "_FontAwesome/fonts/fontawesome-webfont.woff2", + theme::FONT_AWESOME_WOFF2, + )?; + self.write_file( + destination, + "_FontAwesome/fonts/FontAwesome.ttf", + theme::FONT_AWESOME_TTF, + )?; let playpen_config = &html_config.playpen; @@ -173,38 +212,19 @@ impl HtmlHandlebars { if playpen_config.editable { // Load the editor let editor = playpen_editor::PlaypenEditor::new(&playpen_config.editor); - book.write_file("editor.js", &editor.js)?; - book.write_file("ace.js", &editor.ace_js)?; - book.write_file("mode-rust.js", &editor.mode_rust_js)?; - book.write_file("theme-dawn.js", &editor.theme_dawn_js)?; - book.write_file("theme-tomorrow_night.js", &editor.theme_tomorrow_night_js)?; + self.write_file(destination, "editor.js", &editor.js)?; + self.write_file(destination, "ace.js", &editor.ace_js)?; + self.write_file(destination, "mode-rust.js", &editor.mode_rust_js)?; + self.write_file(destination, "theme-dawn.js", &editor.theme_dawn_js)?; + self.write_file(destination, + "theme-tomorrow_night.js", + &editor.theme_tomorrow_night_js, + )?; } Ok(()) } - /// Helper function to write a file to the build directory, normalizing - /// the path to be relative to the book root. - fn write_custom_file(&self, custom_file: &Path, book: &MDBook) -> Result<()> { - let mut data = Vec::new(); - let mut f = File::open(custom_file)?; - f.read_to_end(&mut data)?; - - let name = match custom_file.strip_prefix(&book.root) { - Ok(p) => p.to_str().expect("Could not convert to str"), - Err(_) => { - custom_file.file_name() - .expect("File has a file name") - .to_str() - .expect("Could not convert to str") - } - }; - - book.write_file(name, &data)?; - - Ok(()) - } - /// Update the context with data for this file fn configure_print_version(&self, data: &mut serde_json::Map, @@ -227,31 +247,42 @@ impl HtmlHandlebars { /// Copy across any additional CSS and JavaScript files which the book /// has been configured to use. - fn copy_additional_css_and_js(&self, book: &MDBook) -> Result<()> { - let html = book.config.html_config().unwrap_or_default(); + fn copy_additional_css_and_js(&self, html: &HtmlConfig, destination: &Path) -> Result<()> { + let custom_files = html.additional_css.iter().chain(html.additional_js.iter()); - let custom_files = html.additional_css - .iter() - .chain(html.additional_js.iter()); + debug!("Copying additional CSS and JS"); for custom_file in custom_files { - self.write_custom_file(&custom_file, book) - .chain_err(|| format!("Copying {} failed", custom_file.display()))?; + let output_location = destination.join(custom_file); + debug!( + "Copying {} -> {}", + custom_file.display(), + output_location.display() + ); + + fs::copy(custom_file, &output_location).chain_err(|| { + format!( + "Unable to copy {} to {}", + custom_file.display(), + output_location.display() + ) + })?; } Ok(()) } } - impl Renderer for HtmlHandlebars { fn name(&self) -> &str { "html" } - fn render(&self, book: &MDBook) -> Result<()> { - let html_config = book.config.html_config().unwrap_or_default(); - let src_dir = book.root.join(&book.config.book.src); + fn render(&self, ctx: &RenderContext) -> Result<()> { + let html_config = ctx.config.html_config().unwrap_or_default(); + let src_dir = ctx.root.join(&ctx.config.book.src); + let destination = &ctx.destination; + let book = &ctx.book; debug!("[fn]: render"); let mut handlebars = Handlebars::new(); @@ -278,21 +309,17 @@ impl Renderer for HtmlHandlebars { debug!("[*]: Register handlebars helpers"); self.register_hbs_helpers(&mut handlebars); - let mut data = make_data(book, &book.config)?; + let mut data = make_data(&ctx.root, &book, &ctx.config, &html_config)?; // Print version let mut print_content = String::new(); - // TODO: The Renderer trait should really pass in where it wants us to build to... - let destination = book.get_destination(); - debug!("[*]: Check if destination directory exists"); fs::create_dir_all(&destination) .chain_err(|| "Unexpected error when constructing destination path")?; for (i, item) in book.iter().enumerate() { let ctx = RenderItemContext { - book: book, handlebars: &handlebars, destination: destination.to_path_buf(), src_dir: src_dir.clone(), @@ -305,7 +332,7 @@ impl Renderer for HtmlHandlebars { // Print version self.configure_print_version(&mut data, &print_content); - if let Some(ref title) = book.config.book.title { + if let Some(ref title) = ctx.config.book.title { data.insert("title".to_owned(), json!(title)); } @@ -318,25 +345,23 @@ impl Renderer for HtmlHandlebars { "print.html", &html_config.playpen); - book.write_file(Path::new("print").with_extension("html"), - &rendered.into_bytes())?; + self.write_file(&destination, "print.html", &rendered.into_bytes())?; info!("[*] Creating print.html ✓"); debug!("[*] Copy static files"); - self.copy_static_files(book, &theme, &html_config) + self.copy_static_files(&destination, &theme, &html_config) .chain_err(|| "Unable to copy across static files")?; - self.copy_additional_css_and_js(book) + self.copy_additional_css_and_js(&html_config, &destination) .chain_err(|| "Unable to copy across additional CSS and JS")?; // Copy all remaining files - let src = book.source_dir(); - utils::fs::copy_files_except_ext(&src, &destination, true, &["md"])?; + utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?; Ok(()) } } -fn make_data(book: &MDBook, config: &Config) -> Result> { +fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig) -> Result> { debug!("[fn]: make_data"); let html = config.html_config().unwrap_or_default(); @@ -345,7 +370,7 @@ fn make_data(book: &MDBook, config: &Config) -> Result Result css.push(p.to_str().expect("Could not convert to str")), Err(_) => { css.push(style.file_name() @@ -379,7 +404,7 @@ fn make_data(book: &MDBook, config: &Config) -> Result js.push(p.to_str().expect("Could not convert to str")), Err(_) => { js.push(script.file_name() @@ -608,7 +633,6 @@ fn partition_source(s: &str) -> (String, String) { struct RenderItemContext<'a> { handlebars: &'a Handlebars, - book: &'a MDBook, destination: PathBuf, src_dir: PathBuf, data: serde_json::Map, diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 678a0a5f0e..fbceae723b 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -9,11 +9,11 @@ use serde_json; use errors::*; use config::Config; -use book::{Book, MDBook}; +use book::Book; pub trait Renderer { fn name(&self) -> &str; - fn render(&self, book: &MDBook) -> Result<()>; + fn render(&self, ctx: &RenderContext) -> Result<()>; } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -49,8 +49,7 @@ impl RenderContext { } pub fn from_json(reader: R) -> Result { - serde_json::from_reader(reader) - .chain_err(|| "Unable to deserialize the `RenderContext`") + serde_json::from_reader(reader).chain_err(|| "Unable to deserialize the `RenderContext`") } } @@ -77,14 +76,8 @@ impl Renderer for CmdRenderer { &self.name } - fn render(&self, book: &MDBook) -> Result<()> { + fn render(&self, ctx: &RenderContext) -> Result<()> { info!("Invoking the \"{}\" renderer", self.cmd); - let ctx = RenderContext::new( - &book.root, - book.book.clone(), - book.config.clone(), - &self.destination, - ); let mut child = Command::new(&self.cmd) .stdin(Stdio::piped()) diff --git a/src/theme/index.hbs b/src/theme/index.hbs index a64095a470..a44bd10ccd 100644 --- a/src/theme/index.hbs +++ b/src/theme/index.hbs @@ -139,8 +139,22 @@ } + {{#if livereload}} - {{{livereload}}} + + {{/if}} {{#if google_analytics}} From d919a480acfeaac92dcf1d26abb6e207c36d3676 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Wed, 3 Jan 2018 19:02:01 +0800 Subject: [PATCH 16/24] Moved info for developers out of the book.toml format chapter --- book-example/src/format/config.md | 66 ------------------------------- book-example/src/lib/index.md | 65 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 66 deletions(-) diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md index 457fe51c6e..786dce3c04 100644 --- a/book-example/src/format/config.md +++ b/book-example/src/format/config.md @@ -103,69 +103,3 @@ additional-js = ["custom.js"] editor = "./path/to/editor" editable = false ``` - - -## For Developers - -If you are developing a plugin or alternate backend then whenever your code is -called you will almost certainly be passed a reference to the book's `Config`. -This can be treated roughly as a nested hashmap which lets you call methods like -`get()` and `get_mut()` to get access to the config's contents. - -By convention, plugin developers will have their settings as a subtable inside -`plugins` (e.g. a link checker would put its settings in `plugins.link_check`) -and backends should put their configuration under `output`, like the HTML -renderer does in the previous examples. - -As an example, some hypothetical `random` renderer would typically want to load -its settings from the `Config` at the very start of its rendering process. The -author can take advantage of serde to deserialize the generic `toml::Value` -object retrieved from `Config` into a struct specific to its use case. - -```rust -extern crate serde; -#[macro_use] -extern crate serde_derive; -extern crate toml; -extern crate mdbook; - -use toml::Value; -use mdbook::config::Config; - -#[derive(Debug, Deserialize, PartialEq)] -struct RandomOutput { - foo: u32, - bar: String, - baz: Vec, -} - -# fn run() -> Result<(), Box<::std::error::Error>> { -let src = r#" -[output.random] -foo = 5 -bar = "Hello World" -baz = [true, true, false] -"#; - -let book_config = Config::from_str(src)?; // usually passed in by mdbook -let random = book_config.get("output.random") - .cloned() - .ok_or("output.random not found")?; -let got: RandomOutput = random.try_into()?; - -let should_be = RandomOutput { - foo: 5, - bar: "Hello World".to_string(), - baz: vec![true, true, false] -}; - -assert_eq!(got, should_be); - -let baz: Vec = book_config.get_deserialized("output.random.baz")?; -println!("{:?}", baz); // prints [true, true, false] - -// do something interesting with baz -# Ok(()) -# } -# fn main() { run().unwrap() } -``` diff --git a/book-example/src/lib/index.md b/book-example/src/lib/index.md index f01a488beb..23b96ec710 100644 --- a/book-example/src/lib/index.md +++ b/book-example/src/lib/index.md @@ -47,6 +47,71 @@ command = "python3 my_plugin.py" If no backend is supplied (i.e. there are no `output.*` tables), `mdbook` will fall back to the `html` backend. +### The `Config` Struct + +If you are developing a plugin or alternate backend then whenever your code is +called you will almost certainly be passed a reference to the book's `Config`. +This can be treated roughly as a nested hashmap which lets you call methods like +`get()` and `get_mut()` to get access to the config's contents. + +By convention, plugin developers will have their settings as a subtable inside +`plugins` (e.g. a link checker would put its settings in `plugins.link_check`) +and backends should put their configuration under `output`, like the HTML +renderer does in the previous examples. + +As an example, some hypothetical `random` renderer would typically want to load +its settings from the `Config` at the very start of its rendering process. The +author can take advantage of serde to deserialize the generic `toml::Value` +object retrieved from `Config` into a struct specific to its use case. + +```rust +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate toml; +extern crate mdbook; + +use toml::Value; +use mdbook::config::Config; + +#[derive(Debug, Deserialize, PartialEq)] +struct RandomOutput { + foo: u32, + bar: String, + baz: Vec, +} + +# fn run() -> Result<(), Box<::std::error::Error>> { +let src = r#" +[output.random] +foo = 5 +bar = "Hello World" +baz = [true, true, false] +"#; + +let book_config = Config::from_str(src)?; // usually passed in via the RenderContext +let random = book_config.get("output.random") + .cloned() + .ok_or("output.random not found")?; +let got: RandomOutput = random.try_into()?; + +let should_be = RandomOutput { + foo: 5, + bar: "Hello World".to_string(), + baz: vec![true, true, false] +}; + +assert_eq!(got, should_be); + +let baz: Vec = book_config.get_deserialized("output.random.baz")?; +println!("{:?}", baz); // prints [true, true, false] + +// do something interesting with baz +# Ok(()) +# } +# fn main() { run().unwrap() } +``` + ## Render Context From 1018f17025bd601b3293570f6210dba6c1ba51e2 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Wed, 3 Jan 2018 19:31:11 +0800 Subject: [PATCH 17/24] MOAR docs --- src/book/mod.rs | 1 - src/renderer/mod.rs | 82 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index 76c44ef1b3..c3436224d0 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -299,7 +299,6 @@ fn determine_renderers(config: &Config) -> Vec> { renderers.push(Box::new(CmdRenderer::new( key.to_string(), command.to_string(), - &config.build.build_dir, ))); } } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index fbceae723b..ff0730e6cc 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -1,3 +1,16 @@ +//! `mdbook`'s low level rendering interface. +//! +//! # Note +//! +//! You usually don't need to work with this module directly. If you want to +//! implement your own backend, then check out the [For Developers] section of +//! the user guide. +//! +//! The definition for [RenderContext] may be useful though. +//! +//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/lib/index.html +//! [RenderContext]: struct.RenderContext.html + pub use self::html_handlebars::HtmlHandlebars; mod html_handlebars; @@ -11,22 +24,49 @@ use errors::*; use config::Config; use book::Book; +/// An arbitrary `mdbook` backend. +/// +/// Although it's quite possible for you to import `mdbook` as a library and +/// provide your own renderer, there are two main renderer implementations that +/// 99% of users will ever use: +/// +/// - [HtmlHandlebars] - the built-in HTML renderer +/// - [CmdRenderer] - a generic renderer which shells out to a program to do the +/// actual rendering +/// +/// [HtmlHandlebars]: struct.HtmlHandlebars.html +/// [CmdRenderer]: struct.CmdRenderer.html pub trait Renderer { + /// The `Renderer`'s name. fn name(&self) -> &str; + + /// Invoke the `Renderer`, passing in all the necessary information for + /// describing a book. fn render(&self, ctx: &RenderContext) -> Result<()>; } +/// The context provided to all renderers. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RenderContext { + /// Which version of `mdbook` did this come from (as written in `mdbook`'s + /// `Cargo.toml`). Useful if you know the renderer is only compatible with + /// certain versions of `mdbook`. pub version: String, + /// The book's root directory. pub root: PathBuf, + /// A loaded representation of the book itself. pub book: Book, + /// The loaded configuration file. pub config: Config, + /// Where the renderer *must* put any build artefacts generated. To allow + /// renderers to cache intermediate results, this directory is not + /// guaranteed to be empty or even exist. pub destination: PathBuf, } impl RenderContext { - pub fn new(root: P, book: Book, config: Config, destination: Q) -> RenderContext + /// Create a new `RenderContext`. + pub(crate) fn new(root: P, book: Book, config: Config, destination: Q) -> RenderContext where P: Into, Q: Into, @@ -40,34 +80,50 @@ impl RenderContext { } } + /// Get the source directory's (absolute) path on disk. pub fn source_dir(&self) -> PathBuf { self.root.join(&self.config.book.src) } - pub fn build_dir(&self) -> PathBuf { - self.root.join(&self.config.build.build_dir) - } - + /// Load a `RenderContext` from its JSON representation. pub fn from_json(reader: R) -> Result { serde_json::from_reader(reader).chain_err(|| "Unable to deserialize the `RenderContext`") } } +/// A generic renderer which will shell out to an arbitrary executable. +/// +/// # Rendering Protocol +/// +/// When the renderer's `render()` method is invoked, `CmdRenderer` will spawn +/// the `cmd` as a subprocess. The `RenderContext` is passed to the subprocess +/// as a JSON string (using `serde_json`). +/// +/// > **Note:** The command used doesn't necessarily need to be a single +/// > executable (i.e. `/path/to/renderer`). The `cmd` string lets you pass +/// > in command line arguments, so there's no reason why it couldn't be +/// > `python /path/to/renderer --from mdbook --to epub`. +/// +/// Anything the subprocess writes to `stdin` or `stdout` will be passed through +/// to the user. While this gives the renderer maximum flexibility to output +/// whatever it wants, to avoid spamming users it is recommended to avoid +/// unnecessary output. +/// +/// To help choose the appropriate output level, the `RUST_LOG` environment +/// variable will be passed through to the subprocess, if set. +/// +/// If the subprocess wishes to indicate that rendering failed, it should exit +/// with a non-zero return code. #[derive(Debug, Clone, PartialEq)] pub struct CmdRenderer { name: String, cmd: String, - destination: PathBuf, } impl CmdRenderer { - pub fn new>(name: String, cmd: String, destination: P) -> CmdRenderer { - let destination = destination.into(); - CmdRenderer { - name, - cmd, - destination, - } + /// Create a new `CmdRenderer` which will invoke the provided `cmd` string. + pub fn new(name: String, cmd: String) -> CmdRenderer { + CmdRenderer { name, cmd } } } From e7693bccf616fe1a266b9207b44c47432328d33f Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Fri, 5 Jan 2018 11:45:15 +0800 Subject: [PATCH 18/24] MDBook::build() no longer takes &mut self --- src/bin/serve.rs | 2 +- src/bin/watch.rs | 4 ++-- src/book/mod.rs | 2 +- tests/init.rs | 2 +- tests/rendered_output.rs | 19 ++++++++----------- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/bin/serve.rs b/src/bin/serve.rs index 99f20e3ba7..7b33894d67 100644 --- a/src/bin/serve.rs +++ b/src/bin/serve.rs @@ -90,7 +90,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { b.config.set("output.html.livereload-url", &livereload_url)?; Ok(b) }) - .and_then(|mut b| b.build()); + .and_then(|b| b.build()); if let Err(e) = result { error!("Unable to load the book"); diff --git a/src/bin/watch.rs b/src/bin/watch.rs index 94b51999e0..9395bcb9a3 100644 --- a/src/bin/watch.rs +++ b/src/bin/watch.rs @@ -22,7 +22,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { // Watch command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); - let mut book = MDBook::load(&book_dir)?; + let book = MDBook::load(&book_dir)?; if args.is_present("open") { book.build()?; @@ -31,7 +31,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { trigger_on_change(&book, |path, book_dir| { println!("File changed: {:?}\nBuilding book...\n", path); - let result = MDBook::load(&book_dir).and_then(|mut b| b.build()); + let result = MDBook::load(&book_dir).and_then(|b| b.build()); if let Err(e) = result { println!("Error while building: {}", e); diff --git a/src/book/mod.rs b/src/book/mod.rs index c3436224d0..ec15bc9ec4 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -145,7 +145,7 @@ impl MDBook { } /// Tells the renderer to build our book and put it in the build directory. - pub fn build(&mut self) -> Result<()> { + pub fn build(&self) -> Result<()> { debug!("[fn]: build"); for renderer in &self.renderers { diff --git a/tests/init.rs b/tests/init.rs index 8659936a4d..8bea579233 100644 --- a/tests/init.rs +++ b/tests/init.rs @@ -62,7 +62,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() { #[test] fn book_toml_isnt_required() { let temp = TempDir::new("mdbook").unwrap(); - let mut md = MDBook::init(temp.path()).build().unwrap(); + let md = MDBook::init(temp.path()).build().unwrap(); let _ = fs::remove_file(temp.path().join("book.toml")); diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index d1fb589936..38b804b208 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -22,7 +22,6 @@ use mdbook::utils::fs::file_to_string; use mdbook::config::Config; use mdbook::MDBook; - const BOOK_ROOT: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book"); const TOC_TOP_LEVEL: &[&'static str] = &[ "1. First Chapter", @@ -36,7 +35,7 @@ const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter"]; #[test] fn build_the_dummy_book() { let temp = DummyBook::new().build().unwrap(); - let mut md = MDBook::load(temp.path()).unwrap(); + let md = MDBook::load(temp.path()).unwrap(); md.build().unwrap(); } @@ -44,7 +43,7 @@ fn build_the_dummy_book() { #[test] fn by_default_mdbook_generates_rendered_content_in_the_book_directory() { let temp = DummyBook::new().build().unwrap(); - let mut md = MDBook::load(temp.path()).unwrap(); + let md = MDBook::load(temp.path()).unwrap(); assert!(!temp.path().join("book").exists()); md.build().unwrap(); @@ -56,7 +55,7 @@ fn by_default_mdbook_generates_rendered_content_in_the_book_directory() { #[test] fn make_sure_bottom_level_files_contain_links_to_chapters() { let temp = DummyBook::new().build().unwrap(); - let mut md = MDBook::load(temp.path()).unwrap(); + let md = MDBook::load(temp.path()).unwrap(); md.build().unwrap(); let dest = temp.path().join("book"); @@ -78,7 +77,7 @@ fn make_sure_bottom_level_files_contain_links_to_chapters() { #[test] fn check_correct_cross_links_in_nested_dir() { let temp = DummyBook::new().build().unwrap(); - let mut md = MDBook::load(temp.path()).unwrap(); + let md = MDBook::load(temp.path()).unwrap(); md.build().unwrap(); let first = temp.path().join("book").join("first"); @@ -115,7 +114,7 @@ fn check_correct_cross_links_in_nested_dir() { #[test] fn rendered_code_has_playpen_stuff() { let temp = DummyBook::new().build().unwrap(); - let mut md = MDBook::load(temp.path()).unwrap(); + let md = MDBook::load(temp.path()).unwrap(); md.build().unwrap(); let nested = temp.path().join("book/first/nested.html"); @@ -138,7 +137,7 @@ fn chapter_content_appears_in_rendered_document() { ]; let temp = DummyBook::new().build().unwrap(); - let mut md = MDBook::load(temp.path()).unwrap(); + let md = MDBook::load(temp.path()).unwrap(); md.build().unwrap(); let destination = temp.path().join("book"); @@ -149,7 +148,6 @@ fn chapter_content_appears_in_rendered_document() { } } - /// Apply a series of predicates to some root predicate, where each /// successive predicate is the descendant of the last one. Similar to how you /// might do `ul.foo li a` in CSS to access all anchor tags in the `foo` list. @@ -162,7 +160,6 @@ macro_rules! descendants { }; } - /// Make sure that all `*.md` files (excluding `SUMMARY.md`) were rendered /// and placed in the `book` directory with their extensions set to `*.html`. #[test] @@ -286,7 +283,7 @@ fn create_missing_file_with_config() { #[test] fn able_to_include_rust_files_in_chapters() { let temp = DummyBook::new().build().unwrap(); - let mut md = MDBook::load(temp.path()).unwrap(); + let md = MDBook::load(temp.path()).unwrap(); md.build().unwrap(); let second = temp.path().join("book/second.html"); @@ -302,7 +299,7 @@ fn able_to_include_rust_files_in_chapters() { fn example_book_can_build() { let example_book_dir = dummy_book::new_copy_of_example_book().unwrap(); - let mut md = MDBook::load(example_book_dir.path()).unwrap(); + let md = MDBook::load(example_book_dir.path()).unwrap(); md.build().unwrap(); } From 69e26e625d1a424064ef766cac4d532e5cf68734 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 7 Jan 2018 19:29:47 +0800 Subject: [PATCH 19/24] Replaced a bunch of println!()'s with proper log macros --- src/bin/build.rs | 1 + src/bin/init.rs | 6 +++++- src/bin/mdbook.rs | 7 ++++--- src/bin/serve.rs | 10 +++++----- src/bin/watch.rs | 7 ++++--- src/utils/mod.rs | 10 ++++++++++ 6 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/bin/build.rs b/src/bin/build.rs index adec879bab..24d9a7fdb1 100644 --- a/src/bin/build.rs +++ b/src/bin/build.rs @@ -30,6 +30,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { book.build()?; if args.is_present("open") { + // FIXME: What's the right behaviour if we don't use the HTML renderer? open(book.build_dir_for("html").join("index.html")); } diff --git a/src/bin/init.rs b/src/bin/init.rs index 3a14d9c014..b0299059d8 100644 --- a/src/bin/init.rs +++ b/src/bin/init.rs @@ -26,7 +26,11 @@ pub fn execute(args: &ArgMatches) -> Result<()> { // Skip this if `--force` is present if !args.is_present("force") { // Print warning - print!("\nCopying the default theme to {}", builder.config().book.src.display()); + println!(); + println!( + "Copying the default theme to {}", + builder.config().book.src.display() + ); println!("This could potentially overwrite files already present in that directory."); print!("\nAre you sure you want to continue? (y/n) "); diff --git a/src/bin/mdbook.rs b/src/bin/mdbook.rs index 93794541da..1ceb7da963 100644 --- a/src/bin/mdbook.rs +++ b/src/bin/mdbook.rs @@ -17,6 +17,7 @@ use chrono::Local; use log::LevelFilter; use env_logger::Builder; use error_chain::ChainedError; +use mdbook::utils; pub mod build; pub mod init; @@ -64,7 +65,7 @@ fn main() { }; if let Err(e) = res { - eprintln!("{}", e.display_chain()); + utils::log_backtrace(&e); ::std::process::exit(101); } @@ -101,12 +102,12 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf { p.to_path_buf() } } else { - env::current_dir().unwrap() + env::current_dir().expect("Unable to determine the current directory") } } fn open>(path: P) { if let Err(e) = open::that(path) { - println!("Error opening web browser: {}", e); + error!("Error opening web browser: {}", e); } } diff --git a/src/bin/serve.rs b/src/bin/serve.rs index 7b33894d67..783282342e 100644 --- a/src/bin/serve.rs +++ b/src/bin/serve.rs @@ -7,6 +7,7 @@ use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Re Set}; use clap::{App, ArgMatches, SubCommand}; use mdbook::MDBook; +use mdbook::utils; use mdbook::errors::*; use {get_book_dir, open}; #[cfg(feature = "watch")] @@ -80,7 +81,9 @@ pub fn execute(args: &ArgMatches) -> Result<()> { #[cfg(feature = "watch")] watch::trigger_on_change(&mut book, move |path, book_dir| { - println!("File changed: {:?}\nBuilding book...\n", path); + info!("File changed: {:?}", path); + info!("Building book..."); + // FIXME: This area is really ugly because we need to re-set livereload :( let livereload_url = livereload_url.clone(); @@ -94,10 +97,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { if let Err(e) = result { error!("Unable to load the book"); - error!("Error: {}", e); - for cause in e.iter().skip(1) { - error!("\tCaused By: {}", cause); - } + utils::log_backtrace(&e); } else { let _ = broadcaster.send("reload"); } diff --git a/src/bin/watch.rs b/src/bin/watch.rs index 9395bcb9a3..5d39e71508 100644 --- a/src/bin/watch.rs +++ b/src/bin/watch.rs @@ -6,6 +6,7 @@ use std::time::Duration; use std::sync::mpsc::channel; use clap::{App, ArgMatches, SubCommand}; use mdbook::MDBook; +use mdbook::utils; use mdbook::errors::Result; use {get_book_dir, open}; @@ -30,13 +31,13 @@ pub fn execute(args: &ArgMatches) -> Result<()> { } trigger_on_change(&book, |path, book_dir| { - println!("File changed: {:?}\nBuilding book...\n", path); + info!("File changed: {:?}\nBuilding book...\n", path); let result = MDBook::load(&book_dir).and_then(|b| b.build()); if let Err(e) = result { - println!("Error while building: {}", e); + error!("Unable to build the book"); + utils::log_backtrace(&e); } - println!(); }); Ok(()) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index b541f84a32..d28aff6370 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,6 @@ pub mod fs; mod string; +use errors::Error; use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES, OPTION_ENABLE_TABLES}; @@ -105,6 +106,15 @@ fn convert_quotes_to_curly(original_text: &str) -> String { .collect() } +/// Prints a "backtrace" of some `Error`. +pub fn log_backtrace(e: &Error) { + error!("Error: {}", e); + + for cause in e.iter().skip(1) { + error!("\tCaused By: {}", cause); + } +} + #[cfg(test)] mod tests { mod render_markdown { From fe6c1c169bd55607c86ea6dc146e85e98090abb3 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 7 Jan 2018 19:58:27 +0800 Subject: [PATCH 20/24] Cleaned up the build() method and backend discovery --- src/bin/mdbook.rs | 1 - src/book/mod.rs | 76 ++++++++++++++++++++++++++--------------------- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/bin/mdbook.rs b/src/bin/mdbook.rs index 1ceb7da963..d8bd487ad3 100644 --- a/src/bin/mdbook.rs +++ b/src/bin/mdbook.rs @@ -16,7 +16,6 @@ use clap::{App, AppSettings, ArgMatches}; use chrono::Local; use log::LevelFilter; use env_logger::Builder; -use error_chain::ChainedError; use mdbook::utils; pub mod build; diff --git a/src/book/mod.rs b/src/book/mod.rs index ec15bc9ec4..cb8324fb6b 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -19,6 +19,7 @@ use std::path::PathBuf; use std::io::Write; use std::process::Command; use tempdir::TempDir; +use toml::Value; use utils; use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer}; @@ -149,32 +150,36 @@ impl MDBook { debug!("[fn]: build"); for renderer in &self.renderers { - let name = renderer.name(); - let build_dir = self.build_dir_for(name); - if build_dir.exists() { - debug!( - "Cleaning build dir for the \"{}\" renderer ({})", - name, - build_dir.display() - ); - - utils::fs::remove_dir_content(&build_dir) - .chain_err(|| "Unable to clear output directory")?; - } + self.run_renderer(renderer.as_ref())?; + } + + Ok(()) + } - let render_context = RenderContext::new( - self.root.clone(), - self.book.clone(), - self.config.clone(), - build_dir, + fn run_renderer(&self, renderer: &Renderer) -> Result<()> { + let name = renderer.name(); + let build_dir = self.build_dir_for(name); + if build_dir.exists() { + debug!( + "Cleaning build dir for the \"{}\" renderer ({})", + name, + build_dir.display() ); - renderer - .render(&render_context) - .chain_err(|| "Rendering failed")?; + utils::fs::remove_dir_content(&build_dir) + .chain_err(|| "Unable to clear output directory")?; } - Ok(()) + let render_context = RenderContext::new( + self.root.clone(), + self.book.clone(), + self.config.clone(), + build_dir, + ); + + renderer + .render(&render_context) + .chain_err(|| "Rendering failed") } /// You can change the default renderer to another one by using this method. @@ -284,22 +289,12 @@ fn determine_renderers(config: &Config) -> Vec> { if let Some(output_table) = config.get("output").and_then(|o| o.as_table()) { for (key, table) in output_table.iter() { + // the "html" backend has its own Renderer if key == "html" { renderers.push(Box::new(HtmlHandlebars::new())); } else { - // look for the `command` field, falling back to using the key - // prepended by "mdbook-" - let command = table - .get("command") - .and_then(|c| c.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| format!("mdbook-{}", key)); - - // TODO: Actually calculate the destination directory - renderers.push(Box::new(CmdRenderer::new( - key.to_string(), - command.to_string(), - ))); + let renderer = interpret_custom_renderer(key, table); + renderers.push(renderer); } } } @@ -312,6 +307,19 @@ fn determine_renderers(config: &Config) -> Vec> { renderers } +fn interpret_custom_renderer(key: &str, table: &Value) -> Box { + // look for the `command` field, falling back to using the key + // prepended by "mdbook-" + let table_dot_command = table + .get("command") + .and_then(|c| c.as_str()) + .map(|s| s.to_string()); + + let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key)); + + Box::new(CmdRenderer::new(key.to_string(), command.to_string())) +} + #[cfg(test)] mod tests { use super::*; From 38eecb896e33cf6db817cc20def482f8d372181a Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 7 Jan 2018 20:11:59 +0800 Subject: [PATCH 21/24] Added a couple notes and doc-comments --- src/book/mod.rs | 2 +- src/config.rs | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index cb8324fb6b..1d985caab8 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -273,7 +273,7 @@ impl MDBook { self.root.join(&self.config.book.src) } - // FIXME: This belongs as part of the `HtmlConfig`. + // FIXME: This really belongs as part of the `HtmlConfig`. #[doc(hidden)] pub fn theme_dir(&self) -> PathBuf { match self.config.html_config().and_then(|h| h.theme) { diff --git a/src/config.rs b/src/config.rs index 50eb71f413..b83293e293 100644 --- a/src/config.rs +++ b/src/config.rs @@ -54,7 +54,8 @@ impl Config { /// # Note /// /// This is for compatibility only. It will be removed completely once the - /// rendering and plugin system is established. + /// HTML renderer is refactored to be less coupled to `mdbook` internals. + #[doc(hidden)] pub fn html_config(&self) -> Option { self.get_deserialized("output.html").ok() } @@ -75,6 +76,9 @@ impl Config { } /// Set a config key, clobbering any existing values along the way. + /// + /// The only way this can fail is if we can't serialize `value` into a + /// `toml::Value`. pub fn set>(&mut self, index: I, value: S) -> Result<()> { let pieces: Vec<_> = index.as_ref().split(".").collect(); let value = @@ -123,6 +127,11 @@ impl Config { } } +/// Recursively walk down a table and try to set some `foo.bar.baz` value. +/// +/// If at any table along the way doesn't exist (or isn't itself a `Table`!) an +/// empty `Table` will be inserted. e.g. if the `foo` table didn't contain a +/// nested table called `bar`, we'd insert one and then keep recursing. fn recursive_set(key: &[&str], table: &mut Table, value: Value) { if key.is_empty() { unreachable!(); @@ -143,6 +152,7 @@ fn recursive_set(key: &[&str], table: &mut Table, value: Value) { } } +/// The "getter" version of `recursive_set()`. fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> { if key.is_empty() { return None; @@ -160,6 +170,7 @@ fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> { } } +/// The mutable version of `recursive_get()`. fn recursive_get_mut<'a>(key: &[&str], table: &'a mut Table) -> Option<&'a mut Value> { // TODO: Figure out how to abstract over mutability to reduce copy-pasta if key.is_empty() { @@ -248,7 +259,6 @@ fn is_legacy_format(table: &Table) -> bool { .any(|key| table.contains_key(&key.to_string())) } - /// Configuration options which are specific to the book and required for /// loading it from disk. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -308,7 +318,7 @@ pub struct HtmlConfig { pub additional_css: Vec, pub additional_js: Vec, pub playpen: Playpen, - /// This is used as a bit of a workaround for the `mdbook serve` command. + /// This is used as a bit of a workaround for the `mdbook serve` command. /// Basically, because you set the websocket port from the command line, the /// `mdbook serve` command needs a way to let the HTML renderer know where /// to point livereloading at, if it has been enabled. @@ -335,7 +345,6 @@ impl Default for Playpen { } } - #[cfg(test)] mod tests { use super::*; From 683f014bd2e0a9f96e6476c46177255c00c4c153 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 7 Jan 2018 20:35:29 +0800 Subject: [PATCH 22/24] Found a race condition when backends exit really quickly --- Cargo.toml | 1 + src/lib.rs | 1 + src/renderer/mod.rs | 24 +++++++++++--------- tests/alternate_backends.rs | 45 +++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 tests/alternate_backends.rs diff --git a/Cargo.toml b/Cargo.toml index 4c49432e1d..35d7dcc510 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ open = "1.1" regex = "0.2.1" tempdir = "0.3.4" itertools = "0.7.4" +tempfile = "2.2.0" # Watch feature notify = { version = "4.0", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 2f9c6a4aa3..c5573ed596 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,6 +110,7 @@ extern crate serde_derive; #[macro_use] extern crate serde_json; extern crate tempdir; +extern crate tempfile; extern crate toml; #[cfg(test)] diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index ff0730e6cc..da1df3905d 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -17,8 +17,9 @@ mod html_handlebars; use std::io::Read; use std::path::PathBuf; -use std::process::{Command, Stdio}; +use std::process::Command; use serde_json; +use tempfile; use errors::*; use config::Config; @@ -135,17 +136,20 @@ impl Renderer for CmdRenderer { fn render(&self, ctx: &RenderContext) -> Result<()> { info!("Invoking the \"{}\" renderer", self.cmd); - let mut child = Command::new(&self.cmd) - .stdin(Stdio::piped()) - .spawn() + // We need to write the RenderContext to a temporary file here instead + // of passing it in via a pipe. This prevents a race condition where + // some quickly executing command (e.g. `/bin/true`) may exit before we + // finish writing the render context (closing the stdin pipe and + // throwing a write error). + let mut temp = tempfile::tempfile().chain_err(|| "Unable to create a temporary file")?; + serde_json::to_writer(&mut temp, &ctx) + .chain_err(|| "Unable to serialize the RenderContext")?; + + let status = Command::new(&self.cmd) + .stdin(temp) + .status() .chain_err(|| "Unable to start the renderer")?; - serde_json::to_writer( - child.stdin.as_mut().expect("stdin is always attached"), - &ctx, - ).chain_err(|| "Error occurred while sending the render context to the renderer")?; - - let status = child.wait()?; trace!("{} exited with output: {:?}", self.cmd, status); if !status.success() { diff --git a/tests/alternate_backends.rs b/tests/alternate_backends.rs new file mode 100644 index 0000000000..6f69f0eace --- /dev/null +++ b/tests/alternate_backends.rs @@ -0,0 +1,45 @@ +//! Integration tests to make sure alternate backends work. + +extern crate mdbook; +extern crate tempdir; + +use tempdir::TempDir; +use mdbook::config::Config; +use mdbook::MDBook; + +#[test] +fn passing_alternate_backend() { + let (md, _temp) = dummy_book_with_backend("passing", "true"); + + md.build().unwrap(); +} + +#[test] +fn failing_alternate_backend() { + let (md, _temp) = dummy_book_with_backend("failing", "false"); + + md.build().unwrap_err(); +} + +#[test] +fn alternate_backend_with_arguments() { + let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!"); + + md.build().unwrap(); +} + +fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) { + let temp = TempDir::new("mdbook").unwrap(); + + let mut config = Config::default(); + config + .set(format!("output.{}.command", name), command) + .unwrap(); + + let md = MDBook::init(temp.path()) + .with_config(config) + .build() + .unwrap(); + + (md, temp) +} From 5ed8cbf363e34c2c0ba580fd5fa2a0327fd8baf1 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 7 Jan 2018 20:43:43 +0800 Subject: [PATCH 23/24] Added support for backends with arguments --- Cargo.toml | 1 + src/lib.rs | 1 + src/renderer/mod.rs | 20 +++++++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 35d7dcc510..061fd674a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ regex = "0.2.1" tempdir = "0.3.4" itertools = "0.7.4" tempfile = "2.2.0" +shlex = "0.1.1" # Watch feature notify = { version = "4.0", optional = true } diff --git a/src/lib.rs b/src/lib.rs index c5573ed596..cb4938fba0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,6 +109,7 @@ extern crate serde; extern crate serde_derive; #[macro_use] extern crate serde_json; +extern crate shlex; extern crate tempdir; extern crate tempfile; extern crate toml; diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index da1df3905d..0e145cce32 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -20,6 +20,7 @@ use std::path::PathBuf; use std::process::Command; use serde_json; use tempfile; +use shlex::Shlex; use errors::*; use config::Config; @@ -126,6 +127,22 @@ impl CmdRenderer { pub fn new(name: String, cmd: String) -> CmdRenderer { CmdRenderer { name, cmd } } + + fn compose_command(&self) -> Result { + let mut words = Shlex::new(&self.cmd); + let executable = match words.next() { + Some(e) => e, + None => bail!("Command string was empty"), + }; + + let mut cmd = Command::new(executable); + + for arg in words { + cmd.arg(arg); + } + + Ok(cmd) + } } impl Renderer for CmdRenderer { @@ -145,8 +162,9 @@ impl Renderer for CmdRenderer { serde_json::to_writer(&mut temp, &ctx) .chain_err(|| "Unable to serialize the RenderContext")?; - let status = Command::new(&self.cmd) + let status = self.compose_command()? .stdin(temp) + .current_dir(&ctx.destination) .status() .chain_err(|| "Unable to start the renderer")?; From 263a6248d50dddd880a88cd58345f570615d1d0b Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 7 Jan 2018 22:10:09 +0800 Subject: [PATCH 24/24] Fixed a funny doc-comment --- src/utils/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d28aff6370..255a7f776a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -8,10 +8,7 @@ use std::borrow::Cow; pub use self::string::{RangeArgument, take_lines}; -/// -/// -/// Wrapper around the pulldown-cmark parser and renderer to render markdown - +/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML. pub fn render_markdown(text: &str, curly_quotes: bool) -> String { let mut s = String::with_capacity(text.len() * 3 / 2);