diff --git a/src/book/mod.rs b/src/book/mod.rs index 4101f6a9..4666e271 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -23,7 +23,7 @@ use toml::Value; use utils; use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer}; -use preprocess; +use preprocess::{Preprocessor, LinkPreprocessor, PreprocessorContext}; use errors::*; use config::Config; @@ -40,6 +40,9 @@ pub struct MDBook { /// The URL used for live reloading when serving up the book. pub livereload: Option, + + /// List of pre-processors to be run on the book + preprocessors: Vec> } impl MDBook { @@ -85,6 +88,7 @@ impl MDBook { let livereload = None; let renderers = determine_renderers(&config); + let preprocessors = determine_preprocessors(&config)?; Ok(MDBook { root, @@ -92,6 +96,7 @@ impl MDBook { book, renderers, livereload, + preprocessors, }) } @@ -151,14 +156,22 @@ impl MDBook { pub fn build(&self) -> Result<()> { debug!("[fn]: build"); + let mut preprocessed_book = self.book.clone(); + let preprocess_ctx = PreprocessorContext::new(self.root.clone(), self.config.clone()); + + for preprocessor in &self.preprocessors { + debug!("Running the {} preprocessor.", preprocessor.name()); + preprocessor.run(&preprocess_ctx, &mut preprocessed_book)?; + } + for renderer in &self.renderers { - self.run_renderer(renderer.as_ref())?; + self.run_renderer(&preprocessed_book, renderer.as_ref())?; } Ok(()) } - fn run_renderer(&self, renderer: &Renderer) -> Result<()> { + fn run_renderer(&self, preprocessed_book: &Book, renderer: &Renderer) -> Result<()> { let name = renderer.name(); let build_dir = self.build_dir_for(name); if build_dir.exists() { @@ -174,7 +187,7 @@ impl MDBook { let render_context = RenderContext::new( self.root.clone(), - self.book.clone(), + preprocessed_book.clone(), self.config.clone(), build_dir, ); @@ -185,13 +198,19 @@ 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) + /// The only requirement is for your renderer to implement the [`Renderer` + /// trait](../renderer/trait.Renderer.html) pub fn with_renderer(&mut self, renderer: R) -> &mut Self { self.renderers.push(Box::new(renderer)); self } + /// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book. + pub fn with_preprecessor(&mut self, preprocessor: P) -> &mut Self { + self.preprocessors.push(Box::new(preprocessor)); + self + } + /// Run `rustdoc` tests on the book, linking against the provided libraries. pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> { let library_args: Vec<&str> = (0..library_paths.len()) @@ -202,15 +221,15 @@ impl MDBook { let temp_dir = TempDir::new("mdbook")?; + let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone()); + + LinkPreprocessor::new().run(&preprocess_context, &mut self.book)?; + for item in self.iter() { if let BookItem::Chapter(ref ch) = *item { if !ch.path.as_os_str().is_empty() { let path = self.source_dir().join(&ch.path); - let base = path.parent() - .ok_or_else(|| String::from("Invalid bookitem path!"))?; let content = utils::fs::file_to_string(&path)?; - // Parse and expand links - let content = preprocess::links::replace_all(&content, base)?; println!("[*]: Testing file: {:?}", path); // write preprocessed file to tempdir @@ -309,6 +328,34 @@ fn determine_renderers(config: &Config) -> Vec> { renderers } +fn default_preprocessors() -> Vec> { + vec![Box::new(LinkPreprocessor::new())] +} + +/// Look at the `MDBook` and try to figure out what preprocessors to run. +fn determine_preprocessors(config: &Config) -> Result>> { + + let preprocess_list = match config.build.preprocess { + Some(ref p) => p, + // If no preprocessor field is set, default to the LinkPreprocessor. This allows you + // to disable the LinkPreprocessor by setting "preprocess" to an empty list. + None => return Ok(default_preprocessors()) + }; + + let mut preprocessors: Vec> = Vec::new(); + + for key in preprocess_list { + match key.as_ref() { + "links" => { + preprocessors.push(Box::new(LinkPreprocessor::new())) + } + _ => bail!("{:?} is not a recognised preprocessor", key), + } + } + + Ok(preprocessors) +} + fn interpret_custom_renderer(key: &str, table: &Value) -> Box { // look for the `command` field, falling back to using the key // prepended by "mdbook-" @@ -364,4 +411,65 @@ mod tests { assert_eq!(got.len(), 1); assert_eq!(got[0].name(), "random"); } + + #[test] + fn config_defaults_to_link_preprocessor_if_not_set() { + let cfg = Config::default(); + + // make sure we haven't got anything in the `output` table + assert!(cfg.build.preprocess.is_none()); + + let got = determine_preprocessors(&cfg); + + assert!(got.is_ok()); + assert_eq!(got.as_ref().unwrap().len(), 1); + assert_eq!(got.as_ref().unwrap()[0].name(), "links"); + } + + #[test] + fn config_doesnt_default_if_empty() { + let cfg_str: &'static str = r#" + [book] + title = "Some Book" + + [build] + build-dir = "outputs" + create-missing = false + preprocess = [] + "#; + + + let cfg = Config::from_str(cfg_str).unwrap(); + + // make sure we have something in the `output` table + assert!(cfg.build.preprocess.is_some()); + + let got = determine_preprocessors(&cfg); + + assert!(got.is_ok()); + assert!(got.unwrap().is_empty()); + } + + #[test] + fn config_complains_if_unimplemented_preprocessor() { + let cfg_str: &'static str = r#" + [book] + title = "Some Book" + + [build] + build-dir = "outputs" + create-missing = false + preprocess = ["random"] + "#; + + + let cfg = Config::from_str(cfg_str).unwrap(); + + // make sure we have something in the `output` table + assert!(cfg.build.preprocess.is_some()); + + let got = determine_preprocessors(&cfg); + + assert!(got.is_err()); + } } diff --git a/src/config.rs b/src/config.rs index 24c3f96e..e74f1917 100644 --- a/src/config.rs +++ b/src/config.rs @@ -329,6 +329,9 @@ pub struct BuildConfig { /// Should non-existent markdown files specified in `SETTINGS.md` be created /// if they don't exist? pub create_missing: bool, + /// Which preprocessors should be applied + pub preprocess: Option>, + } impl Default for BuildConfig { @@ -336,6 +339,7 @@ impl Default for BuildConfig { BuildConfig { build_dir: PathBuf::from("book"), create_missing: true, + preprocess: None, } } } @@ -422,6 +426,7 @@ mod tests { [build] build-dir = "outputs" create-missing = false + preprocess = ["first_preprocessor", "second_preprocessor"] [output.html] theme = "./themedir" @@ -449,6 +454,8 @@ mod tests { let build_should_be = BuildConfig { build_dir: PathBuf::from("outputs"), create_missing: false, + preprocess: Some(vec!["first_preprocessor".to_string(), + "second_preprocessor".to_string()]), }; let playpen_should_be = Playpen { editable: true, @@ -550,6 +557,7 @@ mod tests { let build_should_be = BuildConfig { build_dir: PathBuf::from("my-book"), create_missing: true, + preprocess: None, }; let html_should_be = HtmlConfig { diff --git a/src/lib.rs b/src/lib.rs index 163f3619..08c4c37e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,7 +118,7 @@ extern crate toml_query; #[macro_use] extern crate pretty_assertions; -mod preprocess; +pub mod preprocess; pub mod book; pub mod config; pub mod renderer; diff --git a/src/preprocess/links.rs b/src/preprocess/links.rs index fbc1a2f5..c3cab8bf 100644 --- a/src/preprocess/links.rs +++ b/src/preprocess/links.rs @@ -5,9 +5,45 @@ use utils::fs::file_to_string; use utils::take_lines; use errors::*; +use super::{Preprocessor, PreprocessorContext}; +use book::{Book, BookItem}; + const ESCAPE_CHAR: char = '\\'; -pub fn replace_all>(s: &str, path: P) -> Result { +pub struct LinkPreprocessor; + +impl LinkPreprocessor { + pub fn new() -> Self { + LinkPreprocessor + } +} + +impl Preprocessor for LinkPreprocessor { + fn name(&self) -> &str { + "links" + } + + fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()> { + let src_dir = ctx.root.join(&ctx.config.book.src); + + for section in &mut book.sections { + match *section { + BookItem::Chapter(ref mut ch) => { + let base = ch.path.parent() + .map(|dir| src_dir.join(dir)) + .ok_or_else(|| String::from("Invalid bookitem path!"))?; + let content = replace_all(&ch.content, base)?; + ch.content = content + } + _ => {} + } + } + + Ok(()) + } +} + +fn replace_all>(s: &str, path: P) -> Result { // When replacing one thing in a string by something with a different length, // the indices after that will not correspond, // we therefore have to store the difference to correct this diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs index 19b2f4d4..091604d6 100644 --- a/src/preprocess/mod.rs +++ b/src/preprocess/mod.rs @@ -1 +1,25 @@ -pub mod links; +pub use self::links::LinkPreprocessor; + +mod links; + +use book::Book; +use config::Config; +use errors::*; + +use std::path::PathBuf; + +pub struct PreprocessorContext { + pub root: PathBuf, + pub config: Config, +} + +impl PreprocessorContext { + pub fn new(root: PathBuf, config: Config) -> Self { + PreprocessorContext { root, config } + } +} + +pub trait Preprocessor { + fn name(&self) -> &str; + fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>; +} \ No newline at end of file diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 694186ee..d0ec14f7 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -1,5 +1,4 @@ use renderer::html_handlebars::helpers; -use preprocess; use renderer::{RenderContext, Renderer}; use book::{Book, BookItem, Chapter}; use config::{Config, HtmlConfig, Playpen}; @@ -50,12 +49,6 @@ impl HtmlHandlebars { match *item { BookItem::Chapter(ref ch) => { let content = ch.content.clone(); - let base = ch.path.parent() - .map(|dir| ctx.src_dir.join(dir)) - .expect("All chapters must have a parent directory"); - - // Parse and expand links - let content = preprocess::links::replace_all(&content, base)?; let content = utils::render_markdown(&content, ctx.html_config.curly_quotes); print_content.push_str(&content); @@ -322,7 +315,6 @@ impl Renderer for HtmlHandlebars { let ctx = RenderItemContext { handlebars: &handlebars, destination: destination.to_path_buf(), - src_dir: src_dir.clone(), data: data.clone(), is_index: i == 0, html_config: html_config.clone(), @@ -634,7 +626,6 @@ fn partition_source(s: &str) -> (String, String) { struct RenderItemContext<'a> { handlebars: &'a Handlebars, destination: PathBuf, - src_dir: PathBuf, data: serde_json::Map, is_index: bool, html_config: HtmlConfig, diff --git a/tests/testing.rs b/tests/testing.rs index 8e060eb7..fb6a5e6a 100644 --- a/tests/testing.rs +++ b/tests/testing.rs @@ -3,8 +3,14 @@ extern crate mdbook; mod dummy_book; use dummy_book::DummyBook; -use mdbook::MDBook; +use mdbook::MDBook; +use mdbook::preprocess::{Preprocessor, PreprocessorContext}; +use mdbook::book::Book; +use mdbook::config::Config; +use mdbook::errors::*; + +use std::sync::{Arc, Mutex}; #[test] fn mdbook_can_correctly_test_a_passing_book() { @@ -21,3 +27,31 @@ fn mdbook_detects_book_with_failing_tests() { assert!(md.test(vec![]).is_err()); } + +#[test] +fn mdbook_runs_preprocessors() { + + let has_run: Arc> = Arc::new(Mutex::new(false)); + + struct DummyPreprocessor(Arc>); + + impl Preprocessor for DummyPreprocessor { + fn name(&self) -> &str { + "dummy" + } + + fn run(&self, _ctx: &PreprocessorContext, _book: &mut Book) -> Result<()> { + *self.0.lock().unwrap() = true; + Ok(()) + } + } + + let temp = DummyBook::new().build().unwrap(); + let cfg = Config::default(); + + let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap(); + book.with_preprecessor(DummyPreprocessor(Arc::clone(&has_run))); + book.build().unwrap(); + + assert!(*has_run.lock().unwrap()) +}