diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs new file mode 100644 index 00000000..1349a591 --- /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 00000000..2047cc43 --- /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 87a135db..b7ab1986 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 00000000..bad64f98 --- /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); +}