diff --git a/book-example/src/for_developers/preprocessors.md b/book-example/src/for_developers/preprocessors.md index 03c915bb..e8bcaf0a 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 -A preprocessor is represented by the `Preprocessor` trait. +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. -```rust -pub trait Preprocessor { - fn name(&self) -> &str; - fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result; - fn supports_renderer(&self, _renderer: &str) -> bool { - true - } -} +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"] ``` -Where the `PreprocessorContext` is defined as +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. + +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. + +
+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, -} +// nop-preprocessors.rs + +{{#include ../../../examples/nop-preprocessor.rs}} ``` +
-The `renderer` value allows you react accordingly, for example, PDF or HTML. +## Hints For Implementing A Preprocessor -## A complete Example +By pulling in `mdbook` as a library, preprocessors can have access to the +existing infrastructure for dealing with books. -The magic happens within the `run(...)` method of the -[`Preprocessor`][preprocessor-docs] trait implementation. +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. -As direct access to the chapters is not possible, you will probably end up -iterating them using `for_each_mut(...)`: +Chapters can be accessed either directly (by recursively iterating over +chapters) or via the `Book::for_each_mut()` convenience method. -```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), - }, - ); - } -}); -``` +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 `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. - -Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events -back to a string. - -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 88c1b3a4..933e5c45 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 new file mode 100644 index 00000000..d4615ef6 --- /dev/null +++ b/examples/nop-preprocessor.rs @@ -0,0 +1,112 @@ +extern crate clap; +extern crate mdbook; +extern crate serde_json; + +use clap::{App, Arg, ArgMatches, SubCommand}; +use mdbook::book::Book; +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 = 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(&preprocessor, sub_args); + } else { + if let Err(e) = handle_preprocessing(&preprocessor) { + eprintln!("{}", e); + process::exit(1); + } + } +} + +fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> { + let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?; + + 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 + ); + } + + let processed_book = pre.run(&ctx, book)?; + serde_json::to_writer(io::stdout(), &processed_book)?; + + Ok(()) +} + +fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! { + let renderer = sub_args.value_of("renderer").expect("Required argument"); + let supported = pre.supports_renderer(&renderer); + + // Signal whether the renderer is supported by exiting with 1 or 0. + if supported { + process::exit(0); + } else { + process::exit(1); + } +} + +/// 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 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/book/mod.rs b/src/book/mod.rs index f1e3daf4..3ffbde6f 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 = Vec::new(); - let mut preprocessors = if config.build.use_default_preprocessors { - default_preprocessors() - } else { - Vec::new() - }; + if config.build.use_default_preprocessors { + preprocessors.extend(default_preprocessors()); + } - 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), + 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.is_err()); + 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_eq!(random.cmd(), "python random.py"); } #[test] diff --git a/src/lib.rs b/src/lib.rs index 2c91e283..028a2ba8 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/preprocess/cmd.rs b/src/preprocess/cmd.rs new file mode 100644 index 00000000..4398eba6 --- /dev/null +++ b/src/preprocess/cmd.rs @@ -0,0 +1,154 @@ +use super::{Preprocessor, PreprocessorContext}; +use book::Book; +use errors::*; +use serde_json; +use shlex::Shlex; +use std::io::{self, Read, Write}; +use std::process::{Child, Command, Stdio}; + +/// A custom preprocessor which will shell out to a 3rd-party program. +/// +/// # 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, + 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_to_child( + &self, + child: &mut Child, + book: &Book, + ctx: &PreprocessorContext, + ) { + let stdin = child.stdin.take().expect("Child has stdin"); + + 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); + } + } + + 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. + pub fn cmd(&self) -> &str { + &self.cmd + } + + 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 { + 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_to_child(&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 { + debug!("Checking if the \"{}\" preprocessor supports \"{}\"", self.name(), renderer); + + let mut cmd = match self.command() { + Ok(c) => c, + Err(e) => { + warn!("Unable to create the command for the \"{}\" preprocessor, {}", self.name(), e); + return false; + } + }; + + let outcome = cmd + .arg("supports") + .arg(renderer) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .map(|status| status.code() == Some(0)); + + 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); + } + } + + outcome.unwrap_or(false) + } +} diff --git a/src/preprocess/links.rs b/src/preprocess/links.rs index 870f96a8..3403891a 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}}", }, ] ); diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs index 5f59c5bf..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, @@ -21,12 +24,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 906f7e27..41176ae0 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 @@ -66,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 { @@ -78,9 +77,10 @@ impl RenderContext { RenderContext { book: book, config: config, - version: MDBOOK_VERSION.to_string(), + version: ::MDBOOK_VERSION.to_string(), root: root.into(), destination: destination.into(), + __non_exhaustive: (), } } diff --git a/tests/custom_preprocessors.rs b/tests/custom_preprocessors.rs new file mode 100644 index 00000000..54bfd51d --- /dev/null +++ b/tests/custom_preprocessors.rs @@ -0,0 +1,53 @@ +extern crate mdbook; + +mod dummy_book; + +use dummy_book::DummyBook; +use mdbook::preprocess::{CmdPreprocessor, Preprocessor}; +use mdbook::MDBook; + +fn example() -> CmdPreprocessor { + CmdPreprocessor::new("nop-preprocessor".to_string(), "cargo run --example nop-preprocessor --".to_string()) +} + +#[test] +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); +} + +#[test] +fn ask_the_preprocessor_to_blow_up() { + 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(); + + let got = md.build(); + + assert!(got.is_err()); +} + +#[test] +fn process_the_dummy_book() { + 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(); +}