diff --git a/book-example/src/for_developers/backends.md b/book-example/src/for_developers/backends.md index d58583a9..605c2191 100644 --- a/book-example/src/for_developers/backends.md +++ b/book-example/src/for_developers/backends.md @@ -329,6 +329,36 @@ generation or a warning). All environment variables are passed through to the backend, allowing you to use the usual `RUST_LOG` to control logging verbosity. +## Handling missing backends + +If you enable a backend that isn't installed, the default behavior is to throw an error: + +```text +The command wasn't found, is the "wordcount" backend installed? +``` + +This behavior can be changed by marking the backend as optional. + +```diff + [book] + title = "mdBook Documentation" + description = "Create book from markdown files. Like Gitbook but implemented in Rust" + authors = ["Mathieu David", "Michael-F-Bryan"] + + [output.html] + + [output.wordcount] + command = "python /path/to/wordcount.py" ++ optional = true +``` + +This demotes the error to a warning, and it will instead look like this: + +```text +The command was not found, but was marked as optional. + Command: wordcount +``` + ## Wrapping Up diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md index 82c896c3..d8be13e8 100644 --- a/book-example/src/format/config.md +++ b/book-example/src/format/config.md @@ -306,11 +306,17 @@ specify which preprocessors should run before the Markdown renderer. A custom renderer can be enabled by adding a `[output.foo]` table to your `book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will instruct `mdbook` to pass a representation of the book to `mdbook-foo` for -rendering. +rendering. See the [alternative backends] chapter for more detail. -Custom renderers will have access to all configuration within their table -(i.e. anything under `[output.foo]`), and the command to be invoked can be -manually specified with the `command` field. +The custom renderer has access to all the fields within its table (i.e. +anything under `[output.foo]`). mdBook checks for two common fields: + +- **command:** The command to execute for this custom renderer. Defaults to + the name of the renderer with the `mdbook-` prefix (such as `mdbook-foo`). +- **optional:** If `true`, then the command will be ignored if it is not + installed, otherwise mdBook will fail with an error. Defaults to `false`. + +[alternative backends]: ../for_developers/backends.md ## Environment Variables diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 410dd3af..318e7da3 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -19,13 +19,14 @@ mod markdown_renderer; use shlex::Shlex; use std::fs; -use std::io::{self, Read}; +use std::io::{self, ErrorKind, Read}; use std::path::PathBuf; use std::process::{Command, Stdio}; use crate::book::Book; use crate::config::Config; use crate::errors::*; +use toml::Value; /// An arbitrary `mdbook` backend. /// @@ -149,6 +150,41 @@ impl CmdRenderer { } } +impl CmdRenderer { + fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> { + match error.kind() { + ErrorKind::NotFound => { + // Look for "output.{self.name}.optional". + // If it exists and is true, treat this as a warning. + // Otherwise, fail the build. + + let optional_key = format!("output.{}.optional", self.name); + + let is_optional = match ctx.config.get(&optional_key) { + Some(Value::Boolean(value)) => *value, + _ => false, + }; + + if is_optional { + warn!( + "The command `{}` for backend `{}` was not found, \ + but was marked as optional.", + self.cmd, self.name + ); + return Ok(()); + } else { + error!( + "The command `{}` wasn't found, is the `{}` backend installed?", + self.cmd, self.name + ); + } + } + _ => {} + } + Err(error).chain_err(|| "Unable to start the backend")? + } +} + impl Renderer for CmdRenderer { fn name(&self) -> &str { &self.name @@ -168,17 +204,7 @@ impl Renderer for CmdRenderer { .spawn() { Ok(c) => c, - Err(ref e) if e.kind() == io::ErrorKind::NotFound => { - warn!( - "The command wasn't found, is the \"{}\" backend installed?", - self.name - ); - warn!("\tCommand: {}", self.cmd); - return Ok(()); - } - Err(e) => { - return Err(e).chain_err(|| "Unable to start the backend")?; - } + Err(e) => return self.handle_render_command_error(ctx, e), }; { diff --git a/tests/alternative_backends.rs b/tests/alternative_backends.rs index ff2ab687..875b2cfc 100644 --- a/tests/alternative_backends.rs +++ b/tests/alternative_backends.rs @@ -8,28 +8,33 @@ use tempfile::{Builder as TempFileBuilder, TempDir}; #[test] fn passing_alternate_backend() { - let (md, _temp) = dummy_book_with_backend("passing", success_cmd()); + let (md, _temp) = dummy_book_with_backend("passing", success_cmd(), false); md.build().unwrap(); } #[test] fn failing_alternate_backend() { - let (md, _temp) = dummy_book_with_backend("failing", fail_cmd()); + let (md, _temp) = dummy_book_with_backend("failing", fail_cmd(), false); md.build().unwrap_err(); } #[test] -fn missing_backends_arent_fatal() { - let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn"); +fn missing_backends_are_fatal() { + let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", false); + assert!(md.build().is_err()); +} +#[test] +fn missing_optional_backends_are_not_fatal() { + let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", true); assert!(md.build().is_ok()); } #[test] fn alternate_backend_with_arguments() { - let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!"); + let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!", false); md.build().unwrap(); } @@ -56,7 +61,7 @@ fn backends_receive_render_context_via_stdin() { let out_file = temp.path().join("out.txt"); let cmd = tee_command(&out_file); - let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd); + let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd, false); assert!(!out_file.exists()); md.build().unwrap(); @@ -66,7 +71,11 @@ fn backends_receive_render_context_via_stdin() { assert!(got.is_ok()); } -fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) { +fn dummy_book_with_backend( + name: &str, + command: &str, + backend_is_optional: bool, +) -> (MDBook, TempDir) { let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap(); let mut config = Config::default(); @@ -74,6 +83,12 @@ fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) { .set(format!("output.{}.command", name), command) .unwrap(); + if backend_is_optional { + config + .set(format!("output.{}.optional", name), true) + .unwrap(); + } + let md = MDBook::init(temp.path()) .with_config(config) .build()