From 206a00915b350fdb5f0fd9dd6107e743b6338612 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 16 Sep 2018 14:22:50 +0800 Subject: [PATCH 01/10] Export the mdbook version from the crate root --- src/lib.rs | 6 ++++++ src/renderer/mod.rs | 4 +--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2c91e283af..028a2ba87b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,6 +114,12 @@ pub mod renderer; pub mod theme; pub mod utils; +/// The current version of `mdbook`. +/// +/// This is provided as a way for custom preprocessors and renderers to do +/// compatibility checks. +pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION"); + pub use book::BookItem; pub use book::MDBook; pub use config::Config; diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 906f7e2745..78d208e222 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -26,8 +26,6 @@ use book::Book; use config::Config; use errors::*; -const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION"); - /// An arbitrary `mdbook` backend. /// /// Although it's quite possible for you to import `mdbook` as a library and @@ -78,7 +76,7 @@ impl RenderContext { RenderContext { book: book, config: config, - version: MDBOOK_VERSION.to_string(), + version: ::MDBOOK_VERSION.to_string(), root: root.into(), destination: destination.into(), } From 5dce5399289cdaa8d3a95fd00ff79f27968e3dce Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 16 Sep 2018 14:27:37 +0800 Subject: [PATCH 02/10] Notify preprocessors of the mdbook version and add __non_exhaustive elements --- src/preprocess/mod.rs | 11 ++++++++++- src/renderer/mod.rs | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs index 5f59c5bf71..87a135db83 100644 --- a/src/preprocess/mod.rs +++ b/src/preprocess/mod.rs @@ -21,12 +21,21 @@ pub struct PreprocessorContext { pub config: Config, /// The `Renderer` this preprocessor is being used with. pub renderer: String, + /// The calling `mdbook` version. + pub mdbook_version: String, + __non_exhaustive: (), } impl PreprocessorContext { /// Create a new `PreprocessorContext`. pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self { - PreprocessorContext { root, config, renderer } + PreprocessorContext { + root, + config, + renderer, + mdbook_version: ::MDBOOK_VERSION.to_string(), + __non_exhaustive: (), + } } } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 78d208e222..41176ae0dc 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -64,6 +64,7 @@ pub struct RenderContext { /// renderers to cache intermediate results, this directory is not /// guaranteed to be empty or even exist. pub destination: PathBuf, + __non_exhaustive: (), } impl RenderContext { @@ -79,6 +80,7 @@ impl RenderContext { version: ::MDBOOK_VERSION.to_string(), root: root.into(), destination: destination.into(), + __non_exhaustive: (), } } From 729c94a7e418a4652e58cf556101384539d20581 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 16 Sep 2018 22:49:52 +0800 Subject: [PATCH 03/10] Started working on a custom preprocessor --- examples/nop-preprocessor.rs | 40 ++++++++++++ src/preprocess/cmd.rs | 111 ++++++++++++++++++++++++++++++++++ src/preprocess/mod.rs | 3 + tests/custom_preprocessors.rs | 16 +++++ 4 files changed, 170 insertions(+) create mode 100644 examples/nop-preprocessor.rs create mode 100644 src/preprocess/cmd.rs create mode 100644 tests/custom_preprocessors.rs diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs new file mode 100644 index 0000000000..1349a591c7 --- /dev/null +++ b/examples/nop-preprocessor.rs @@ -0,0 +1,40 @@ +extern crate mdbook; +#[macro_use] +extern crate clap; + +use clap::{App, Arg, SubCommand, ArgMatches}; +use mdbook::preprocess::{Preprocessor, PreprocessorContext}; + +fn main() { + let matches = app().get_matches(); + + if let Some(sub_args) = matches.subcommand_matches("supports") { + handle_supports(sub_args); + } else { + handle_preprocessing(&matches); + } +} + +fn handle_preprocessing(args: &ArgMatches) { + +} + +fn handle_supports(sub_args: &ArgMatches) { + let renderer = sub_args.value_of("renderer").expect("Required argument"); + let supported = renderer_is_supported(&renderer); +} + +fn renderer_is_supported(renderer: &str) -> bool { + true +} + +fn app() -> App<'static, 'static> { + app_from_crate!().subcommand( + SubCommand::with_name("supports") + .arg(Arg::with_name("renderer").required(true)) + .about( + "Check whether a renderer is supported by this preprocessor", + ), + ) +} + diff --git a/src/preprocess/cmd.rs b/src/preprocess/cmd.rs new file mode 100644 index 0000000000..2047cc43ba --- /dev/null +++ b/src/preprocess/cmd.rs @@ -0,0 +1,111 @@ +use super::{Preprocessor, PreprocessorContext}; +use book::Book; +use errors::*; +use shlex::Shlex; +use std::io::{self, Read}; +use serde_json; +use std::process::{Command, Stdio, Child}; + +/// A custom preprocessor which will shell out to a 3rd-party program. +/// +/// # Preprocessing +/// +/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will +/// execute the shell command `$cmd supports $renderer`. If the renderer is +/// supported, custom preprocessors should exit with a exit code of `0`, +/// any other exit code be considered as unsupported. +#[derive(Debug, Clone, PartialEq)] +pub struct CmdPreprocessor { + name: String, + cmd: String, +} + +impl CmdPreprocessor { + /// Create a new `CmdPreprocessor`. + pub fn new(name: String, cmd: String) -> CmdPreprocessor { + CmdPreprocessor { name, cmd } + } + + /// A convenience function custom preprocessors can use to parse the input + /// written to `stdin` by a `CmdRenderer`. + pub fn parse_input( + reader: R, + ) -> Result<(PreprocessorContext, Book)> { + serde_json::from_reader(reader).chain_err(|| "Unable to parse the input") + } + + fn write_input(&self, child: &mut Child, book: Book, ctx: PreprocessorContext) { + let mut stdin = child.stdin.take().expect("Child has stdin"); + let input = (ctx, book); + + if let Err(e) = serde_json::to_writer(&mut stdin, &input) { + // Looks like the backend hung up before we could finish + // sending it the render context. Log the error and keep going + warn!("Error writing the RenderContext to the backend, {}", e); + } + + // explicitly close the `stdin` file handle + drop(stdin); + } + + fn 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 Preprocessor for CmdPreprocessor { + fn name(&self) -> &str { + &self.name + } + + fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result { + unimplemented!() + } + + fn supports_renderer(&self, renderer: &str) -> bool { + let mut cmd = match self.command() { + Ok(c) => c, + Err(e) => { + warn!("Unable to create the command for the \"{}\" preprocessor, {}", self.name(), e); + return true; + } + }; + + cmd + .arg("supports") + .arg(renderer) + .stdin(Stdio::piped()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + + let child = match cmd.spawn() { + Ok(c) => c, + Err(e) => { + if e.kind() == io::ErrorKind::NotFound { + warn!( + "The command wasn't found, is the \"{}\" preprocessor installed?", + self.name + ); + warn!("\tCommand: {}", self.cmd); + } + + // give it the benefit of the doubt + return true; + } + }; + + unimplemented!() + } +} diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs index 87a135db83..b7ab1986b5 100644 --- a/src/preprocess/mod.rs +++ b/src/preprocess/mod.rs @@ -2,9 +2,11 @@ pub use self::index::IndexPreprocessor; pub use self::links::LinkPreprocessor; +pub use self::cmd::CmdPreprocessor; mod index; mod links; +mod cmd; use book::Book; use config::Config; @@ -14,6 +16,7 @@ use std::path::PathBuf; /// Extra information for a `Preprocessor` to give them more context when /// processing a book. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PreprocessorContext { /// The location of the book directory on disk. pub root: PathBuf, diff --git a/tests/custom_preprocessors.rs b/tests/custom_preprocessors.rs new file mode 100644 index 0000000000..bad64f98e1 --- /dev/null +++ b/tests/custom_preprocessors.rs @@ -0,0 +1,16 @@ +extern crate mdbook; + +use mdbook::preprocess::{CmdPreprocessor, Preprocessor}; + +fn example() -> CmdPreprocessor { + CmdPreprocessor::new("nop-preprocessor".to_string(), "cargo run --example nop-preprocessor --".to_string()) +} + +#[test] +fn check_if_renderer_is_supported() { + let cmd = example(); + + let got = cmd.supports_renderer("whatever"); + + assert_eq!(got, true); +} From 304234c122ec15051a2ed4379bd3a21b2dccb4fa Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 16 Sep 2018 23:00:19 +0800 Subject: [PATCH 04/10] The example can now tell mdbook if renderers are supported --- examples/nop-preprocessor.rs | 19 +++++++---- src/preprocess/cmd.rs | 62 +++++++++++++++++++---------------- tests/custom_preprocessors.rs | 11 ++++++- 3 files changed, 56 insertions(+), 36 deletions(-) diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs index 1349a591c7..29b397ee81 100644 --- a/examples/nop-preprocessor.rs +++ b/examples/nop-preprocessor.rs @@ -2,8 +2,9 @@ extern crate mdbook; #[macro_use] extern crate clap; -use clap::{App, Arg, SubCommand, ArgMatches}; +use clap::{App, Arg, ArgMatches, SubCommand}; use mdbook::preprocess::{Preprocessor, PreprocessorContext}; +use std::process; fn main() { let matches = app().get_matches(); @@ -16,16 +17,23 @@ fn main() { } fn handle_preprocessing(args: &ArgMatches) { - + unimplemented!() } fn handle_supports(sub_args: &ArgMatches) { - let renderer = sub_args.value_of("renderer").expect("Required argument"); - let supported = renderer_is_supported(&renderer); + let renderer = sub_args.value_of("renderer").expect("Required argument"); + let supported = renderer_is_supported(&renderer); + + if supported { + process::exit(0); + } else { + process::exit(1); + } } fn renderer_is_supported(renderer: &str) -> bool { - true + // We support everything except the `not-supported` renderer + renderer != "not-supported" } fn app() -> App<'static, 'static> { @@ -37,4 +45,3 @@ fn app() -> App<'static, 'static> { ), ) } - diff --git a/src/preprocess/cmd.rs b/src/preprocess/cmd.rs index 2047cc43ba..84a78e29d5 100644 --- a/src/preprocess/cmd.rs +++ b/src/preprocess/cmd.rs @@ -1,10 +1,10 @@ use super::{Preprocessor, PreprocessorContext}; use book::Book; use errors::*; +use serde_json; use shlex::Shlex; use std::io::{self, Read}; -use serde_json; -use std::process::{Command, Stdio, Child}; +use std::process::{Child, Command, Stdio}; /// A custom preprocessor which will shell out to a 3rd-party program. /// @@ -31,21 +31,27 @@ impl CmdPreprocessor { pub fn parse_input( reader: R, ) -> Result<(PreprocessorContext, Book)> { - serde_json::from_reader(reader).chain_err(|| "Unable to parse the input") + serde_json::from_reader(reader) + .chain_err(|| "Unable to parse the input") } - fn write_input(&self, child: &mut Child, book: Book, ctx: PreprocessorContext) { - let mut stdin = child.stdin.take().expect("Child has stdin"); - let input = (ctx, book); - - if let Err(e) = serde_json::to_writer(&mut stdin, &input) { - // Looks like the backend hung up before we could finish - // sending it the render context. Log the error and keep going - warn!("Error writing the RenderContext to the backend, {}", e); - } + fn write_input( + &self, + child: &mut Child, + book: Book, + ctx: PreprocessorContext, + ) { + let mut stdin = child.stdin.take().expect("Child has stdin"); + let input = (ctx, book); + + if let Err(e) = serde_json::to_writer(&mut stdin, &input) { + // Looks like the backend hung up before we could finish + // sending it the render context. Log the error and keep going + warn!("Error writing the RenderContext to the backend, {}", e); + } - // explicitly close the `stdin` file handle - drop(stdin); + // explicitly close the `stdin` file handle + drop(stdin); } fn command(&self) -> Result { @@ -75,6 +81,8 @@ impl Preprocessor for CmdPreprocessor { } fn supports_renderer(&self, renderer: &str) -> bool { + debug!("Checking if the \"{}\" preprocessor supports \"{}\"", self.name(), renderer); + let mut cmd = match self.command() { Ok(c) => c, Err(e) => { @@ -83,29 +91,25 @@ impl Preprocessor for CmdPreprocessor { } }; - cmd + let outcome = cmd .arg("supports") .arg(renderer) - .stdin(Stdio::piped()) + .stdin(Stdio::null()) .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()); + .stderr(Stdio::inherit()) + .status() + .map(|status| status.code() == Some(0)); - let child = match cmd.spawn() { - Ok(c) => c, - Err(e) => { - if e.kind() == io::ErrorKind::NotFound { - warn!( + if let Err(ref e) = outcome { + if e.kind() == io::ErrorKind::NotFound { + warn!( "The command wasn't found, is the \"{}\" preprocessor installed?", self.name ); - warn!("\tCommand: {}", self.cmd); - } - - // give it the benefit of the doubt - return true; + warn!("\tCommand: {}", self.cmd); } - }; + } - unimplemented!() + outcome.unwrap_or(false) } } diff --git a/tests/custom_preprocessors.rs b/tests/custom_preprocessors.rs index bad64f98e1..03d2689ada 100644 --- a/tests/custom_preprocessors.rs +++ b/tests/custom_preprocessors.rs @@ -7,10 +7,19 @@ fn example() -> CmdPreprocessor { } #[test] -fn check_if_renderer_is_supported() { +fn example_supports_whatever() { let cmd = example(); let got = cmd.supports_renderer("whatever"); assert_eq!(got, true); } + +#[test] +fn example_doesnt_support_not_supported() { + let cmd = example(); + + let got = cmd.supports_renderer("not-supported"); + + assert_eq!(got, false); +} From 1aa1194d798b1e8ab5788bb527f968981e7b9ca2 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 16 Sep 2018 23:23:03 +0800 Subject: [PATCH 05/10] We can shell out to the preprocessor --- examples/nop-preprocessor.rs | 16 ++++++++++++++-- src/preprocess/cmd.rs | 22 ++++++++++++++++++++-- tests/custom_preprocessors.rs | 24 ++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs index 29b397ee81..df8bd7577e 100644 --- a/examples/nop-preprocessor.rs +++ b/examples/nop-preprocessor.rs @@ -3,8 +3,9 @@ extern crate mdbook; extern crate clap; use clap::{App, Arg, ArgMatches, SubCommand}; -use mdbook::preprocess::{Preprocessor, PreprocessorContext}; +use mdbook::preprocess::{Preprocessor, PreprocessorContext, CmdPreprocessor}; use std::process; +use std::io; fn main() { let matches = app().get_matches(); @@ -17,7 +18,18 @@ fn main() { } fn handle_preprocessing(args: &ArgMatches) { - unimplemented!() + let (ctx, book) = CmdPreprocessor::parse_input(io::stdin()) + .expect("Couldn't parse the input"); + + // You can tell the preprocessor to blow up by setting a particular + // config value + if let Some(table) = ctx.config.get_preprocessor("nop-preprocessor") { + let should_blow_up = table.get("blow-up").is_some(); + + if should_blow_up { + panic!("Boom!!!1!"); + } + } } fn handle_supports(sub_args: &ArgMatches) { diff --git a/src/preprocess/cmd.rs b/src/preprocess/cmd.rs index 84a78e29d5..2fc14ffca3 100644 --- a/src/preprocess/cmd.rs +++ b/src/preprocess/cmd.rs @@ -39,7 +39,7 @@ impl CmdPreprocessor { &self, child: &mut Child, book: Book, - ctx: PreprocessorContext, + ctx: &PreprocessorContext, ) { let mut stdin = child.stdin.take().expect("Child has stdin"); let input = (ctx, book); @@ -77,7 +77,25 @@ impl Preprocessor for CmdPreprocessor { } fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result { - unimplemented!() + let mut cmd = self.command()?; + + let mut child = cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .chain_err(|| format!("Unable to start the \"{}\" preprocessor. Is it installed?", self.name()))?; + + self.write_input(&mut child, book, ctx); + + let output = child + .wait_with_output() + .chain_err(|| "Error waiting for the preprocessor to complete")?; + + trace!("{} exited with output: {:?}", self.cmd, output); + ensure!(output.status.success(), "The preprocessor exited unsuccessfully"); + + serde_json::from_slice(&output.stdout).chain_err(|| "Unable to parse the preprocessed book") } fn supports_renderer(&self, renderer: &str) -> bool { diff --git a/tests/custom_preprocessors.rs b/tests/custom_preprocessors.rs index 03d2689ada..d0182e7e38 100644 --- a/tests/custom_preprocessors.rs +++ b/tests/custom_preprocessors.rs @@ -1,6 +1,8 @@ extern crate mdbook; use mdbook::preprocess::{CmdPreprocessor, Preprocessor}; +use mdbook::MDBook; +use std::path::Path; fn example() -> CmdPreprocessor { CmdPreprocessor::new("nop-preprocessor".to_string(), "cargo run --example nop-preprocessor --".to_string()) @@ -23,3 +25,25 @@ fn example_doesnt_support_not_supported() { assert_eq!(got, false); } + +#[test] +fn ask_the_preprocessor_to_blow_up() { + let dummy_book = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join("dummy_book"); + let mut md = MDBook::load(&dummy_book).unwrap(); + md.with_preprecessor(example()); + + md.config.set("preprocess.nop-preprocessor.blow-up", true).unwrap(); + + let got = md.build(); + + assert!(got.is_err()); +} + +#[test] +fn process_the_dummy_book() { + let dummy_book = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join("dummy_book"); + let mut md = MDBook::load(&dummy_book).unwrap(); + md.with_preprecessor(example()); + + md.build().unwrap(); +} From 1d72cea972bcf813ce50d6be273f11cc3b5909ce Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 16 Sep 2018 23:28:01 +0800 Subject: [PATCH 06/10] The example preprocessor works --- examples/nop-preprocessor.rs | 7 ++++++- src/preprocess/cmd.rs | 2 +- tests/custom_preprocessors.rs | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs index df8bd7577e..7c04417a8f 100644 --- a/examples/nop-preprocessor.rs +++ b/examples/nop-preprocessor.rs @@ -1,9 +1,10 @@ extern crate mdbook; +extern crate serde_json; #[macro_use] extern crate clap; use clap::{App, Arg, ArgMatches, SubCommand}; -use mdbook::preprocess::{Preprocessor, PreprocessorContext, CmdPreprocessor}; +use mdbook::preprocess::CmdPreprocessor; use std::process; use std::io; @@ -21,6 +22,8 @@ fn handle_preprocessing(args: &ArgMatches) { let (ctx, book) = CmdPreprocessor::parse_input(io::stdin()) .expect("Couldn't parse the input"); + eprintln!("{:?}", ctx.config); + // You can tell the preprocessor to blow up by setting a particular // config value if let Some(table) = ctx.config.get_preprocessor("nop-preprocessor") { @@ -30,6 +33,8 @@ fn handle_preprocessing(args: &ArgMatches) { panic!("Boom!!!1!"); } } + + serde_json::to_writer(io::stdout(), &book).unwrap(); } fn handle_supports(sub_args: &ArgMatches) { diff --git a/src/preprocess/cmd.rs b/src/preprocess/cmd.rs index 2fc14ffca3..81f8286eec 100644 --- a/src/preprocess/cmd.rs +++ b/src/preprocess/cmd.rs @@ -105,7 +105,7 @@ impl Preprocessor for CmdPreprocessor { Ok(c) => c, Err(e) => { warn!("Unable to create the command for the \"{}\" preprocessor, {}", self.name(), e); - return true; + return false; } }; diff --git a/tests/custom_preprocessors.rs b/tests/custom_preprocessors.rs index d0182e7e38..cfc0d1b36c 100644 --- a/tests/custom_preprocessors.rs +++ b/tests/custom_preprocessors.rs @@ -32,7 +32,7 @@ fn ask_the_preprocessor_to_blow_up() { let mut md = MDBook::load(&dummy_book).unwrap(); md.with_preprecessor(example()); - md.config.set("preprocess.nop-preprocessor.blow-up", true).unwrap(); + md.config.set("preprocessor.nop-preprocessor.blow-up", true).unwrap(); let got = md.build(); From 132f4fd358504c77c9e1a9ebb67adaa255e9f9f4 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 16 Sep 2018 23:33:58 +0800 Subject: [PATCH 07/10] Fixed a bug where the tests use the wrong dummy book --- tests/custom_preprocessors.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/custom_preprocessors.rs b/tests/custom_preprocessors.rs index cfc0d1b36c..54bfd51dfc 100644 --- a/tests/custom_preprocessors.rs +++ b/tests/custom_preprocessors.rs @@ -1,8 +1,10 @@ extern crate mdbook; +mod dummy_book; + +use dummy_book::DummyBook; use mdbook::preprocess::{CmdPreprocessor, Preprocessor}; use mdbook::MDBook; -use std::path::Path; fn example() -> CmdPreprocessor { CmdPreprocessor::new("nop-preprocessor".to_string(), "cargo run --example nop-preprocessor --".to_string()) @@ -28,8 +30,9 @@ fn example_doesnt_support_not_supported() { #[test] fn ask_the_preprocessor_to_blow_up() { - let dummy_book = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join("dummy_book"); - let mut md = MDBook::load(&dummy_book).unwrap(); + let dummy_book = DummyBook::new(); + let temp = dummy_book.build().unwrap(); + let mut md = MDBook::load(temp.path()).unwrap(); md.with_preprecessor(example()); md.config.set("preprocessor.nop-preprocessor.blow-up", true).unwrap(); @@ -41,8 +44,9 @@ fn ask_the_preprocessor_to_blow_up() { #[test] fn process_the_dummy_book() { - let dummy_book = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join("dummy_book"); - let mut md = MDBook::load(&dummy_book).unwrap(); + let dummy_book = DummyBook::new(); + let temp = dummy_book.build().unwrap(); + let mut md = MDBook::load(temp.path()).unwrap(); md.with_preprecessor(example()); md.build().unwrap(); From 5cd5e4764c13dfbabd19dd5f3705705f55b23a89 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sun, 16 Sep 2018 23:44:52 +0800 Subject: [PATCH 08/10] Fleshed out the api docs --- examples/nop-preprocessor.rs | 24 ++++++++++++++++++------ src/preprocess/cmd.rs | 17 ++++++++++++++++- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs index 7c04417a8f..4cd8693d07 100644 --- a/examples/nop-preprocessor.rs +++ b/examples/nop-preprocessor.rs @@ -5,6 +5,7 @@ extern crate clap; use clap::{App, Arg, ArgMatches, SubCommand}; use mdbook::preprocess::CmdPreprocessor; +use mdbook::book::Book; use std::process; use std::io; @@ -14,18 +15,21 @@ fn main() { if let Some(sub_args) = matches.subcommand_matches("supports") { handle_supports(sub_args); } else { - handle_preprocessing(&matches); + handle_preprocessing(); } } -fn handle_preprocessing(args: &ArgMatches) { +fn handle_preprocessing() { let (ctx, book) = CmdPreprocessor::parse_input(io::stdin()) .expect("Couldn't parse the input"); - eprintln!("{:?}", ctx.config); + // You can inspect the calling mdbook's version to check for compatibility + if ctx.mdbook_version != mdbook::MDBOOK_VERSION { + panic!("The version check failed!"); + } - // You can tell the preprocessor to blow up by setting a particular - // config value + // In testing we want to tell the preprocessor to blow up by setting a + // particular config value if let Some(table) = ctx.config.get_preprocessor("nop-preprocessor") { let should_blow_up = table.get("blow-up").is_some(); @@ -34,13 +38,21 @@ fn handle_preprocessing(args: &ArgMatches) { } } - serde_json::to_writer(io::stdout(), &book).unwrap(); + let processed_book = do_processing(book); + + serde_json::to_writer(io::stdout(), &processed_book).unwrap(); +} + +fn do_processing(book: Book) -> Book { + // We *are* a nop preprocessor after all... + book } fn handle_supports(sub_args: &ArgMatches) { let renderer = sub_args.value_of("renderer").expect("Required argument"); let supported = renderer_is_supported(&renderer); + // Signal whether the renderer is supported by exiting with 1 or 0. if supported { process::exit(0); } else { diff --git a/src/preprocess/cmd.rs b/src/preprocess/cmd.rs index 81f8286eec..f16b57dacc 100644 --- a/src/preprocess/cmd.rs +++ b/src/preprocess/cmd.rs @@ -8,12 +8,27 @@ use std::process::{Child, Command, Stdio}; /// A custom preprocessor which will shell out to a 3rd-party program. /// -/// # Preprocessing +/// # Preprocessing Protocol /// /// When the `supports_renderer()` method is executed, `CmdPreprocessor` will /// execute the shell command `$cmd supports $renderer`. If the renderer is /// supported, custom preprocessors should exit with a exit code of `0`, /// any other exit code be considered as unsupported. +/// +/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)` +/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors +/// should then "return" a processed book by printing it to `stdout` as JSON. +/// For convenience, the `CmdPreprocessor::parse_input()` function can be used +/// to parse the input provided by `mdbook`. +/// +/// Exiting with a non-zero exit code while preprocessing is considered an +/// error. `stderr` is passed directly through to the user, so it can be used +/// for logging or emitting warnings if desired. +/// +/// # Examples +/// +/// An example preprocessor is available in this project's `examples/` +/// directory. #[derive(Debug, Clone, PartialEq)] pub struct CmdPreprocessor { name: String, From adec78e7f59d961767507b71649936809ce85752 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Wed, 19 Sep 2018 23:13:25 +0800 Subject: [PATCH 09/10] Forgot to implement 3rd party preprocessor discovery --- src/book/mod.rs | 91 +++++++++++++++++++++++++++++-------------- src/preprocess/cmd.rs | 5 +++ 2 files changed, 67 insertions(+), 29 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index f1e3daf473..3ffbde6f8f 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -20,7 +20,8 @@ use tempfile::Builder as TempFileBuilder; use toml::Value; use errors::*; -use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext}; +use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, + PreprocessorContext, CmdPreprocessor}; use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer}; use utils; @@ -356,36 +357,48 @@ fn is_default_preprocessor(pre: &Preprocessor) -> bool { /// Look at the `MDBook` and try to figure out what preprocessors to run. fn determine_preprocessors(config: &Config) -> Result>> { - let preprocessor_keys = config.get("preprocessor") - .and_then(|value| value.as_table()) - .map(|table| table.keys()); - - let mut preprocessors = if config.build.use_default_preprocessors { - default_preprocessors() - } else { - Vec::new() - }; - - let preprocessor_keys = match preprocessor_keys { - Some(keys) => keys, - // If no preprocessor field is set, default to the LinkPreprocessor and - // IndexPreprocessor. This allows you to disable default preprocessors - // by setting "preprocess" to an empty list. - None => return Ok(preprocessors), - }; - - for key in preprocessor_keys { - match key.as_ref() { - "links" => preprocessors.push(Box::new(LinkPreprocessor::new())), - "index" => preprocessors.push(Box::new(IndexPreprocessor::new())), - _ => bail!("{:?} is not a recognised preprocessor", key), + let mut preprocessors = Vec::new(); + + if config.build.use_default_preprocessors { + preprocessors.extend(default_preprocessors()); + } + + if let Some(preprocessor_table) = + config.get("preprocessor").and_then(|v| v.as_table()) + { + for key in preprocessor_table.keys() { + match key.as_ref() { + "links" => { + preprocessors.push(Box::new(LinkPreprocessor::new())) + } + "index" => { + preprocessors.push(Box::new(IndexPreprocessor::new())) + } + name => preprocessors.push(interpret_custom_preprocessor( + name, + &preprocessor_table[name], + )), + } } } Ok(preprocessors) } -fn interpret_custom_renderer(key: &str, table: &Value) -> Box { +fn interpret_custom_preprocessor( + key: &str, + table: &Value, +) -> Box { + let command = table + .get("command") + .and_then(|c| c.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("mdbook-{}", key)); + + Box::new(CmdPreprocessor::new(key.to_string(), command.to_string())) +} + +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 @@ -393,7 +406,8 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box { .and_then(|c| c.as_str()) .map(|s| s.to_string()); - let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key)); + let command = + table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key)); Box::new(CmdRenderer::new(key.to_string(), command.to_string())) } @@ -492,7 +506,7 @@ mod tests { } #[test] - fn config_complains_if_unimplemented_preprocessor() { + fn can_determine_third_party_preprocessors() { let cfg_str: &'static str = r#" [book] title = "Some Book" @@ -509,9 +523,28 @@ mod tests { // make sure the `preprocessor.random` table exists assert!(cfg.get_preprocessor("random").is_some()); - let got = determine_preprocessors(&cfg); + let got = determine_preprocessors(&cfg).unwrap(); + + assert!(got.into_iter().any(|p| p.name() == "random")); + } + + #[test] + fn preprocessors_can_provide_their_own_commands() { + let cfg_str = r#" + [preprocessor.random] + command = "python random.py" + "#; + + let cfg = Config::from_str(cfg_str).unwrap(); + + // make sure the `preprocessor.random` table exists + let random = cfg.get_preprocessor("random").unwrap(); + let random = interpret_custom_preprocessor( + "random", + &Value::Table(random.clone()), + ); - assert!(got.is_err()); + assert_eq!(random.cmd(), "python random.py"); } #[test] diff --git a/src/preprocess/cmd.rs b/src/preprocess/cmd.rs index f16b57dacc..870b1015c6 100644 --- a/src/preprocess/cmd.rs +++ b/src/preprocess/cmd.rs @@ -69,6 +69,11 @@ impl CmdPreprocessor { drop(stdin); } + /// The command this `Preprocessor` will invoke. + pub fn cmd(&self) -> &str { + &self.cmd + } + fn command(&self) -> Result { let mut words = Shlex::new(&self.cmd); let executable = match words.next() { From b1c7c54108e4eaeeb2a549debdef53db795f8d4e Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Tue, 25 Sep 2018 19:41:38 +0800 Subject: [PATCH 10/10] Rewrote a large proportion of the Preprocessor docs to be up-to-date --- .../src/for_developers/preprocessors.md | 106 ++++++++------- examples/de-emphasize.rs | 24 +--- examples/nop-preprocessor.rs | 124 +++++++++++------- src/preprocess/cmd.rs | 19 +-- src/preprocess/links.rs | 114 ++++++++++++---- 5 files changed, 242 insertions(+), 145 deletions(-) diff --git a/book-example/src/for_developers/preprocessors.md b/book-example/src/for_developers/preprocessors.md index 03c915bbc1..e8bcaf0a4a 100644 --- a/book-example/src/for_developers/preprocessors.md +++ b/book-example/src/for_developers/preprocessors.md @@ -11,68 +11,71 @@ the book. Possible use cases are: mathjax equivalents -## Implementing a Preprocessor +## Hooking Into MDBook + +MDBook uses a fairly simple mechanism for discovering third party plugins. +A new table is added to `book.toml` (e.g. `preprocessor.foo` for the `foo` +preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as +part of the build process. + +While preprocessors can be hard-coded to specify which backend it should be run +for (e.g. it doesn't make sense for MathJax to be used for non-HTML renderers) +with the `preprocessor.foo.renderer` key. + +```toml +[book] +title = "My Book" +authors = ["Michael-F-Bryan"] + +[preprocessor.foo] +# The command can also be specified manually +command = "python3 /path/to/foo.py" +# Only run the `foo` preprocessor for the HTML and EPUB renderer +renderer = ["html", "epub"] +``` -A preprocessor is represented by the `Preprocessor` trait. +In typical unix style, all inputs to the plugin will be written to `stdin` as +JSON and `mdbook` will read from `stdout` if it is expecting output. -```rust -pub trait Preprocessor { - fn name(&self) -> &str; - fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result; - fn supports_renderer(&self, _renderer: &str) -> bool { - true - } -} -``` +The easiest way to get started is by creating your own implementation of the +`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which +translates inputs to the correct `Preprocessor` method. For convenience, there +is [an example no-op preprocessor] in the `examples/` directory which can easily +be adapted for other preprocessors. -Where the `PreprocessorContext` is defined as +
+Example no-op preprocessor ```rust -pub struct PreprocessorContext { - pub root: PathBuf, - pub config: Config, - /// The `Renderer` this preprocessor is being used with. - pub renderer: String, -} -``` - -The `renderer` value allows you react accordingly, for example, PDF or HTML. +// nop-preprocessors.rs -## A complete Example +{{#include ../../../examples/nop-preprocessor.rs}} +``` +
-The magic happens within the `run(...)` method of the -[`Preprocessor`][preprocessor-docs] trait implementation. +## Hints For Implementing A Preprocessor -As direct access to the chapters is not possible, you will probably end up -iterating them using `for_each_mut(...)`: +By pulling in `mdbook` as a library, preprocessors can have access to the +existing infrastructure for dealing with books. -```rust -book.for_each_mut(|item: &mut BookItem| { - if let BookItem::Chapter(ref mut chapter) = *item { - eprintln!("{}: processing chapter '{}'", self.name(), chapter.name); - res = Some( - match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) { - Ok(md) => { - chapter.content = md; - Ok(()) - } - Err(err) => Err(err), - }, - ); - } -}); -``` +For example, a custom preprocessor could use the +[`CmdPreprocessor::parse_input()`] function to deserialize the JSON written to +`stdin`. Then each chapter of the `Book` can be mutated in-place via +[`Book::for_each_mut()`], and then written to `stdout` with the `serde_json` +crate. -The `chapter.content` is just a markdown formatted string, and you will have to -process it in some way. Even though it's entirely possible to implement some -sort of manual find & replace operation, if that feels too unsafe you can use -[`pulldown-cmark`][pc] to parse the string into events and work on them instead. +Chapters can be accessed either directly (by recursively iterating over +chapters) or via the `Book::for_each_mut()` convenience method. -Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events -back to a string. +The `chapter.content` is just a string which happens to be markdown. While it's +entirely possible to use regular expressions or do a manual find & replace, +you'll probably want to process the input into something more computer-friendly. +The [`pulldown-cmark`][pc] crate implements a production-quality event-based +Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to +translate events back into markdown text. -The following code block shows how to remove all emphasis from markdown, and do -so safely. +The following code block shows how to remove all emphasis from markdown, +without accidentally breaking the document. ```rust fn remove_emphasis( @@ -107,3 +110,6 @@ For everything else, have a look [at the complete example][example]. [pc]: https://crates.io/crates/pulldown-cmark [pctc]: https://crates.io/crates/pulldown-cmark-to-cmark [example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs +[an example no-op preprocessor]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/nop-preprocessor.rs +[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input +[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut diff --git a/examples/de-emphasize.rs b/examples/de-emphasize.rs index 88c1b3a413..933e5c452d 100644 --- a/examples/de-emphasize.rs +++ b/examples/de-emphasize.rs @@ -1,4 +1,6 @@ -//! This program removes all forms of emphasis from the markdown of the book. +//! An example preprocessor for removing all forms of emphasis from a markdown +//! book. + extern crate mdbook; extern crate pulldown_cmark; extern crate pulldown_cmark_to_cmark; @@ -6,31 +8,13 @@ extern crate pulldown_cmark_to_cmark; use mdbook::book::{Book, BookItem, Chapter}; use mdbook::errors::{Error, Result}; use mdbook::preprocess::{Preprocessor, PreprocessorContext}; -use mdbook::MDBook; use pulldown_cmark::{Event, Parser, Tag}; use pulldown_cmark_to_cmark::fmt::cmark; -use std::env::{args, args_os}; -use std::ffi::OsString; -use std::process; - const NAME: &str = "md-links-to-html-links"; -fn do_it(book: OsString) -> Result<()> { - let mut book = MDBook::load(book)?; - book.with_preprecessor(Deemphasize); - book.build() -} - fn main() { - if args_os().count() != 2 { - eprintln!("USAGE: {} ", args().next().expect("executable")); - return; - } - if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) { - eprintln!("{}", e); - process::exit(1); - } + panic!("This example is intended to be part of a library"); } struct Deemphasize; diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs index 4cd8693d07..d4615ef66c 100644 --- a/examples/nop-preprocessor.rs +++ b/examples/nop-preprocessor.rs @@ -1,56 +1,64 @@ +extern crate clap; extern crate mdbook; extern crate serde_json; -#[macro_use] -extern crate clap; use clap::{App, Arg, ArgMatches, SubCommand}; -use mdbook::preprocess::CmdPreprocessor; use mdbook::book::Book; -use std::process; +use mdbook::errors::Error; +use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext}; use std::io; +use std::process; +use nop_lib::Nop; + +pub fn make_app() -> App<'static, 'static> { + App::new("nop-preprocessor") + .about("A mdbook preprocessor which does precisely nothing") + .subcommand( + SubCommand::with_name("supports") + .arg(Arg::with_name("renderer").required(true)) + .about("Check whether a renderer is supported by this preprocessor")) +} fn main() { - let matches = app().get_matches(); + let matches = make_app().get_matches(); + + // Users will want to construct their own preprocessor here + let preprocessor = Nop::new(); if let Some(sub_args) = matches.subcommand_matches("supports") { - handle_supports(sub_args); + handle_supports(&preprocessor, sub_args); } else { - handle_preprocessing(); + if let Err(e) = handle_preprocessing(&preprocessor) { + eprintln!("{}", e); + process::exit(1); + } } } -fn handle_preprocessing() { - let (ctx, book) = CmdPreprocessor::parse_input(io::stdin()) - .expect("Couldn't parse the input"); +fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> { + let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?; - // You can inspect the calling mdbook's version to check for compatibility - if ctx.mdbook_version != mdbook::MDBOOK_VERSION { - panic!("The version check failed!"); + if ctx.mdbook_version != mdbook::MDBOOK_VERSION { + // We should probably use the `semver` crate to check compatibility + // here... + eprintln!( + "Warning: The {} plugin was built against version {} of mdbook, \ + but we're being called from version {}", + pre.name(), + mdbook::MDBOOK_VERSION, + ctx.mdbook_version + ); } - // In testing we want to tell the preprocessor to blow up by setting a - // particular config value - if let Some(table) = ctx.config.get_preprocessor("nop-preprocessor") { - let should_blow_up = table.get("blow-up").is_some(); - - if should_blow_up { - panic!("Boom!!!1!"); - } - } - - let processed_book = do_processing(book); - - serde_json::to_writer(io::stdout(), &processed_book).unwrap(); -} + let processed_book = pre.run(&ctx, book)?; + serde_json::to_writer(io::stdout(), &processed_book)?; -fn do_processing(book: Book) -> Book { - // We *are* a nop preprocessor after all... - book + Ok(()) } -fn handle_supports(sub_args: &ArgMatches) { +fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! { let renderer = sub_args.value_of("renderer").expect("Required argument"); - let supported = renderer_is_supported(&renderer); + let supported = pre.supports_renderer(&renderer); // Signal whether the renderer is supported by exiting with 1 or 0. if supported { @@ -60,17 +68,45 @@ fn handle_supports(sub_args: &ArgMatches) { } } -fn renderer_is_supported(renderer: &str) -> bool { - // We support everything except the `not-supported` renderer - renderer != "not-supported" -} +/// The actual implementation of the `Nop` preprocessor. This would usually go +/// in your main `lib.rs` file. +mod nop_lib { + use super::*; + + /// A no-op preprocessor. + pub struct Nop; + + impl Nop { + pub fn new() -> Nop { + Nop + } + } + + impl Preprocessor for Nop { + fn name(&self) -> &str { + "nop-preprocessor" + } -fn app() -> App<'static, 'static> { - app_from_crate!().subcommand( - SubCommand::with_name("supports") - .arg(Arg::with_name("renderer").required(true)) - .about( - "Check whether a renderer is supported by this preprocessor", - ), - ) + fn run( + &self, + ctx: &PreprocessorContext, + book: Book, + ) -> Result { + // In testing we want to tell the preprocessor to blow up by setting a + // particular config value + if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) { + if nop_cfg.contains_key("blow-up") { + return Err("Boom!!1!".into()); + } + } + + // we *are* a no-op preprocessor after all + Ok(book) + } + + fn supports_renderer(&self, renderer: &str) -> bool { + renderer != "not-supported" + } + } } + diff --git a/src/preprocess/cmd.rs b/src/preprocess/cmd.rs index 870b1015c6..4398eba603 100644 --- a/src/preprocess/cmd.rs +++ b/src/preprocess/cmd.rs @@ -3,7 +3,7 @@ use book::Book; use errors::*; use serde_json; use shlex::Shlex; -use std::io::{self, Read}; +use std::io::{self, Read, Write}; use std::process::{Child, Command, Stdio}; /// A custom preprocessor which will shell out to a 3rd-party program. @@ -50,23 +50,24 @@ impl CmdPreprocessor { .chain_err(|| "Unable to parse the input") } - fn write_input( + fn write_input_to_child( &self, child: &mut Child, - book: Book, + book: &Book, ctx: &PreprocessorContext, ) { - let mut stdin = child.stdin.take().expect("Child has stdin"); - let input = (ctx, book); + let stdin = child.stdin.take().expect("Child has stdin"); - if let Err(e) = serde_json::to_writer(&mut stdin, &input) { + if let Err(e) = self.write_input(stdin, &book, &ctx) { // Looks like the backend hung up before we could finish // sending it the render context. Log the error and keep going warn!("Error writing the RenderContext to the backend, {}", e); } + } - // explicitly close the `stdin` file handle - drop(stdin); + fn write_input(&self, writer: W, book: &Book, ctx: &PreprocessorContext) -> Result<()> { + serde_json::to_writer(writer, &(ctx, book)) + .map_err(Into::into) } /// The command this `Preprocessor` will invoke. @@ -106,7 +107,7 @@ impl Preprocessor for CmdPreprocessor { .spawn() .chain_err(|| format!("Unable to start the \"{}\" preprocessor. Is it installed?", self.name()))?; - self.write_input(&mut child, book, ctx); + self.write_input_to_child(&mut child, &book, ctx); let output = child .wait_with_output() diff --git a/src/preprocess/links.rs b/src/preprocess/links.rs index 870f96a8d9..3403891aa0 100644 --- a/src/preprocess/links.rs +++ b/src/preprocess/links.rs @@ -69,7 +69,12 @@ where Ok(new_content) => { if depth < MAX_LINK_NESTED_DEPTH { if let Some(rel_path) = playpen.link.relative_path(path) { - replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1)); + replaced.push_str(&replace_all( + &new_content, + rel_path, + source, + depth + 1, + )); } else { replaced.push_str(&new_content); } @@ -83,6 +88,10 @@ where } Err(e) => { error!("Error updating \"{}\", {}", playpen.link_text, e); + for cause in e.iter().skip(1) { + warn!("Caused By: {}", cause); + } + // This should make sure we include the raw `{{# ... }}` snippet // in the page content if there are any errors. previous_end_index = playpen.start_index; @@ -109,10 +118,18 @@ impl<'a> LinkType<'a> { let base = base.as_ref(); match self { LinkType::Escaped => None, - LinkType::IncludeRange(p, _) => Some(return_relative_path(base, &p)), - LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)), - LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)), - LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)), + LinkType::IncludeRange(p, _) => { + Some(return_relative_path(base, &p)) + } + LinkType::IncludeRangeFrom(p, _) => { + Some(return_relative_path(base, &p)) + } + LinkType::IncludeRangeTo(p, _) => { + Some(return_relative_path(base, &p)) + } + LinkType::IncludeRangeFull(p, _) => { + Some(return_relative_path(base, &p)) + } LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)), } } @@ -182,11 +199,15 @@ impl<'a> Link<'a> { match (typ.as_str(), file_arg) { ("include", Some(pth)) => Some(parse_include_path(pth)), - ("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)), + ("playpen", Some(pth)) => { + Some(LinkType::Playpen(pth.into(), props)) + } _ => None, } } - (Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => { + (Some(mat), None, None) + if mat.as_str().starts_with(ESCAPE_CHAR) => + { Some(LinkType::Escaped) } _ => None, @@ -207,20 +228,65 @@ impl<'a> Link<'a> { match self.link { // omit the escape char LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()), - LinkType::IncludeRange(ref pat, ref range) => file_to_string(base.join(pat)) - .map(|s| take_lines(&s, range.clone())) - .chain_err(|| format!("Could not read file for link {}", self.link_text)), - LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(base.join(pat)) - .map(|s| take_lines(&s, range.clone())) - .chain_err(|| format!("Could not read file for link {}", self.link_text)), - LinkType::IncludeRangeTo(ref pat, ref range) => file_to_string(base.join(pat)) - .map(|s| take_lines(&s, range.clone())) - .chain_err(|| format!("Could not read file for link {}", self.link_text)), - LinkType::IncludeRangeFull(ref pat, _) => file_to_string(base.join(pat)) - .chain_err(|| format!("Could not read file for link {}", self.link_text)), + LinkType::IncludeRange(ref pat, ref range) => { + let target = base.join(pat); + + file_to_string(&target) + .map(|s| take_lines(&s, range.clone())) + .chain_err(|| { + format!( + "Could not read file for link {} ({})", + self.link_text, + target.display(), + ) + }) + } + LinkType::IncludeRangeFrom(ref pat, ref range) => { + let target = base.join(pat); + + file_to_string(&target) + .map(|s| take_lines(&s, range.clone())) + .chain_err(|| { + format!( + "Could not read file for link {} ({})", + self.link_text, + target.display(), + ) + }) + } + LinkType::IncludeRangeTo(ref pat, ref range) => { + let target = base.join(pat); + + file_to_string(&target) + .map(|s| take_lines(&s, range.clone())) + .chain_err(|| { + format!( + "Could not read file for link {} ({})", + self.link_text, + target.display(), + ) + }) + } + LinkType::IncludeRangeFull(ref pat, _) => { + let target = base.join(pat); + + file_to_string(&target).chain_err(|| { + format!("Could not read file for link {} ({})", + self.link_text, + target.display()) + }) + } LinkType::Playpen(ref pat, ref attrs) => { - let contents = file_to_string(base.join(pat)) - .chain_err(|| format!("Could not read file for link {}", self.link_text))?; + let target = base.join(pat); + + let contents = + file_to_string(&target).chain_err(|| { + format!( + "Could not read file for link {} ({})", + self.link_text, + target.display() + ) + })?; let ftype = if !attrs.is_empty() { "rust," } else { "rust" }; Ok(format!( "```{}{}\n{}\n```\n", @@ -465,7 +531,10 @@ mod tests { Link { start_index: 38, end_index: 68, - link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]), + link: LinkType::Playpen( + PathBuf::from("file.rs"), + vec!["editable"] + ), link_text: "{{#playpen file.rs editable }}", }, Link { @@ -475,7 +544,8 @@ mod tests { PathBuf::from("my.rs"), vec!["editable", "no_run", "should_panic"], ), - link_text: "{{#playpen my.rs editable no_run should_panic}}", + link_text: + "{{#playpen my.rs editable no_run should_panic}}", }, ] );