summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Bryan <michaelfbryan@gmail.com>2018-10-16 00:02:12 +0800
committerGitHub <noreply@github.com>2018-10-16 00:02:12 +0800
commit29f8b791f1be4eaabc3e5e49791a6551fc2443e4 (patch)
tree4e88d6fe579b3306cb19f7ad393aba353d5c77ac
parent877bf37d18fb4ab533927b7980ef3794cfc9313b (diff)
parentb1c7c54108e4eaeeb2a549debdef53db795f8d4e (diff)
Merge pull request #792 from rust-lang-nursery/custom-preprocessor
WIP: Custom Preprocessors
-rw-r--r--book-example/src/for_developers/preprocessors.md106
-rw-r--r--examples/de-emphasize.rs24
-rw-r--r--examples/nop-preprocessor.rs112
-rw-r--r--src/book/mod.rs91
-rw-r--r--src/lib.rs6
-rw-r--r--src/preprocess/cmd.rs154
-rw-r--r--src/preprocess/links.rs114
-rw-r--r--src/preprocess/mod.rs14
-rw-r--r--src/renderer/mod.rs6
-rw-r--r--tests/custom_preprocessors.rs53
10 files changed, 555 insertions, 125 deletions
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
+
+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<Book>;
- 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
+<details>
+<summary>Example no-op preprocessor</summary>
```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}}
+```
+</details>
-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 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: {} <book>", 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<Book, Error> {
+ // 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<Vec<Box<Preprocessor>>> {
- 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<Renderer> {
+fn interpret_custom_preprocessor(
+ key: &str,
+ table: &Value,
+) -> Box<CmdPreprocessor> {
+ 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<CmdRenderer> {
// 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<Renderer> {
.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/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<R: Read>(
+ 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<W: Write>(&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<Command> {
+ 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<Book> {
+ 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());