Merge pull request #792 from rust-lang-nursery/custom-preprocessor

WIP: Custom Preprocessors
This commit is contained in:
Michael Bryan 2018-10-16 00:02:12 +08:00 committed by GitHub
commit 29f8b791f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 552 additions and 122 deletions

View File

@ -11,68 +11,71 @@ the book. Possible use cases are:
mathjax equivalents 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 While preprocessors can be hard-coded to specify which backend it should be run
pub trait Preprocessor { for (e.g. it doesn't make sense for MathJax to be used for non-HTML renderers)
fn name(&self) -> &str; with the `preprocessor.foo.renderer` key.
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
fn supports_renderer(&self, _renderer: &str) -> bool { ```toml
true [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.
<details>
<summary>Example no-op preprocessor</summary>
```rust ```rust
pub struct PreprocessorContext { // nop-preprocessors.rs
pub root: PathBuf,
pub config: Config, {{#include ../../../examples/nop-preprocessor.rs}}
/// The `Renderer` this preprocessor is being used with.
pub renderer: String,
}
``` ```
</details>
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 For example, a custom preprocessor could use the
[`Preprocessor`][preprocessor-docs] trait implementation. [`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 Chapters can be accessed either directly (by recursively iterating over
iterating them using `for_each_mut(...)`: chapters) or via the `Book::for_each_mut()` convenience method.
```rust The `chapter.content` is just a string which happens to be markdown. While it's
book.for_each_mut(|item: &mut BookItem| { entirely possible to use regular expressions or do a manual find & replace,
if let BookItem::Chapter(ref mut chapter) = *item { you'll probably want to process the input into something more computer-friendly.
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name); The [`pulldown-cmark`][pc] crate implements a production-quality event-based
res = Some( Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) { translate events back into markdown text.
Ok(md) => {
chapter.content = md;
Ok(())
}
Err(err) => Err(err),
},
);
}
});
```
The `chapter.content` is just a markdown formatted string, and you will have to The following code block shows how to remove all emphasis from markdown,
process it in some way. Even though it's entirely possible to implement some without accidentally breaking the document.
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.
```rust ```rust
fn remove_emphasis( 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 [pc]: https://crates.io/crates/pulldown-cmark
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark [pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs [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

View File

@ -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 mdbook;
extern crate pulldown_cmark; extern crate pulldown_cmark;
extern crate pulldown_cmark_to_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::book::{Book, BookItem, Chapter};
use mdbook::errors::{Error, Result}; use mdbook::errors::{Error, Result};
use mdbook::preprocess::{Preprocessor, PreprocessorContext}; use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::MDBook;
use pulldown_cmark::{Event, Parser, Tag}; use pulldown_cmark::{Event, Parser, Tag};
use pulldown_cmark_to_cmark::fmt::cmark; 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"; 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() { fn main() {
if args_os().count() != 2 { panic!("This example is intended to be part of a library");
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);
}
} }
struct Deemphasize; struct Deemphasize;

View File

@ -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"
}
}
}

View File

@ -20,7 +20,8 @@ use tempfile::Builder as TempFileBuilder;
use toml::Value; use toml::Value;
use errors::*; use errors::*;
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext}; use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor,
PreprocessorContext, CmdPreprocessor};
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer}; use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
use utils; 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. /// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> { fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
let preprocessor_keys = config.get("preprocessor") let mut preprocessors = Vec::new();
.and_then(|value| value.as_table())
.map(|table| table.keys());
let mut preprocessors = if config.build.use_default_preprocessors { if config.build.use_default_preprocessors {
default_preprocessors() preprocessors.extend(default_preprocessors());
} else { }
Vec::new()
};
let preprocessor_keys = match preprocessor_keys { if let Some(preprocessor_table) =
Some(keys) => keys, config.get("preprocessor").and_then(|v| v.as_table())
// If no preprocessor field is set, default to the LinkPreprocessor and {
// IndexPreprocessor. This allows you to disable default preprocessors for key in preprocessor_table.keys() {
// by setting "preprocess" to an empty list.
None => return Ok(preprocessors),
};
for key in preprocessor_keys {
match key.as_ref() { match key.as_ref() {
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())), "links" => {
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())), preprocessors.push(Box::new(LinkPreprocessor::new()))
_ => bail!("{:?} is not a recognised preprocessor", key), }
"index" => {
preprocessors.push(Box::new(IndexPreprocessor::new()))
}
name => preprocessors.push(interpret_custom_preprocessor(
name,
&preprocessor_table[name],
)),
}
} }
} }
Ok(preprocessors) 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 // look for the `command` field, falling back to using the key
// prepended by "mdbook-" // prepended by "mdbook-"
let table_dot_command = table 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()) .and_then(|c| c.as_str())
.map(|s| s.to_string()); .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())) Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
} }
@ -492,7 +506,7 @@ mod tests {
} }
#[test] #[test]
fn config_complains_if_unimplemented_preprocessor() { fn can_determine_third_party_preprocessors() {
let cfg_str: &'static str = r#" let cfg_str: &'static str = r#"
[book] [book]
title = "Some Book" title = "Some Book"
@ -509,9 +523,28 @@ mod tests {
// make sure the `preprocessor.random` table exists // make sure the `preprocessor.random` table exists
assert!(cfg.get_preprocessor("random").is_some()); 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] #[test]

View File

@ -114,6 +114,12 @@ pub mod renderer;
pub mod theme; pub mod theme;
pub mod utils; 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::BookItem;
pub use book::MDBook; pub use book::MDBook;
pub use config::Config; pub use config::Config;

154
src/preprocess/cmd.rs Normal file
View File

@ -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)
}
}

View File

@ -69,7 +69,12 @@ where
Ok(new_content) => { Ok(new_content) => {
if depth < MAX_LINK_NESTED_DEPTH { if depth < MAX_LINK_NESTED_DEPTH {
if let Some(rel_path) = playpen.link.relative_path(path) { 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 { } else {
replaced.push_str(&new_content); replaced.push_str(&new_content);
} }
@ -83,6 +88,10 @@ where
} }
Err(e) => { Err(e) => {
error!("Error updating \"{}\", {}", playpen.link_text, 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 // This should make sure we include the raw `{{# ... }}` snippet
// in the page content if there are any errors. // in the page content if there are any errors.
previous_end_index = playpen.start_index; previous_end_index = playpen.start_index;
@ -109,10 +118,18 @@ impl<'a> LinkType<'a> {
let base = base.as_ref(); let base = base.as_ref();
match self { match self {
LinkType::Escaped => None, LinkType::Escaped => None,
LinkType::IncludeRange(p, _) => Some(return_relative_path(base, &p)), LinkType::IncludeRange(p, _) => {
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &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::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)), LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
} }
} }
@ -182,11 +199,15 @@ impl<'a> Link<'a> {
match (typ.as_str(), file_arg) { match (typ.as_str(), file_arg) {
("include", Some(pth)) => Some(parse_include_path(pth)), ("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, _ => 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) Some(LinkType::Escaped)
} }
_ => None, _ => None,
@ -207,20 +228,65 @@ impl<'a> Link<'a> {
match self.link { match self.link {
// omit the escape char // omit the escape char
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()), LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
LinkType::IncludeRange(ref pat, ref range) => file_to_string(base.join(pat)) LinkType::IncludeRange(ref pat, ref range) => {
let target = base.join(pat);
file_to_string(&target)
.map(|s| take_lines(&s, range.clone())) .map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)), .chain_err(|| {
LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(base.join(pat)) 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())) .map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)), .chain_err(|| {
LinkType::IncludeRangeTo(ref pat, ref range) => file_to_string(base.join(pat)) 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())) .map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)), .chain_err(|| {
LinkType::IncludeRangeFull(ref pat, _) => file_to_string(base.join(pat)) format!(
.chain_err(|| format!("Could not read file for link {}", self.link_text)), "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) => { LinkType::Playpen(ref pat, ref attrs) => {
let contents = file_to_string(base.join(pat)) let target = base.join(pat);
.chain_err(|| format!("Could not read file for link {}", self.link_text))?;
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" }; let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
Ok(format!( Ok(format!(
"```{}{}\n{}\n```\n", "```{}{}\n{}\n```\n",
@ -465,7 +531,10 @@ mod tests {
Link { Link {
start_index: 38, start_index: 38,
end_index: 68, 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_text: "{{#playpen file.rs editable }}",
}, },
Link { Link {
@ -475,7 +544,8 @@ mod tests {
PathBuf::from("my.rs"), PathBuf::from("my.rs"),
vec!["editable", "no_run", "should_panic"], 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}}",
}, },
] ]
); );

View File

@ -2,9 +2,11 @@
pub use self::index::IndexPreprocessor; pub use self::index::IndexPreprocessor;
pub use self::links::LinkPreprocessor; pub use self::links::LinkPreprocessor;
pub use self::cmd::CmdPreprocessor;
mod index; mod index;
mod links; mod links;
mod cmd;
use book::Book; use book::Book;
use config::Config; use config::Config;
@ -14,6 +16,7 @@ use std::path::PathBuf;
/// Extra information for a `Preprocessor` to give them more context when /// Extra information for a `Preprocessor` to give them more context when
/// processing a book. /// processing a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PreprocessorContext { pub struct PreprocessorContext {
/// The location of the book directory on disk. /// The location of the book directory on disk.
pub root: PathBuf, pub root: PathBuf,
@ -21,12 +24,21 @@ pub struct PreprocessorContext {
pub config: Config, pub config: Config,
/// The `Renderer` this preprocessor is being used with. /// The `Renderer` this preprocessor is being used with.
pub renderer: String, pub renderer: String,
/// The calling `mdbook` version.
pub mdbook_version: String,
__non_exhaustive: (),
} }
impl PreprocessorContext { impl PreprocessorContext {
/// Create a new `PreprocessorContext`. /// Create a new `PreprocessorContext`.
pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self { 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: (),
}
} }
} }

View File

@ -26,8 +26,6 @@ use book::Book;
use config::Config; use config::Config;
use errors::*; use errors::*;
const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
/// An arbitrary `mdbook` backend. /// An arbitrary `mdbook` backend.
/// ///
/// Although it's quite possible for you to import `mdbook` as a library and /// 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 /// renderers to cache intermediate results, this directory is not
/// guaranteed to be empty or even exist. /// guaranteed to be empty or even exist.
pub destination: PathBuf, pub destination: PathBuf,
__non_exhaustive: (),
} }
impl RenderContext { impl RenderContext {
@ -78,9 +77,10 @@ impl RenderContext {
RenderContext { RenderContext {
book: book, book: book,
config: config, config: config,
version: MDBOOK_VERSION.to_string(), version: ::MDBOOK_VERSION.to_string(),
root: root.into(), root: root.into(),
destination: destination.into(), destination: destination.into(),
__non_exhaustive: (),
} }
} }

View File

@ -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();
}