Merge pull request #792 from rust-lang-nursery/custom-preprocessor
WIP: Custom Preprocessors
This commit is contained in:
commit
29f8b791f1
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
match key.as_ref() {
|
||||||
None => return Ok(preprocessors),
|
"links" => {
|
||||||
};
|
preprocessors.push(Box::new(LinkPreprocessor::new()))
|
||||||
|
}
|
||||||
for key in preprocessor_keys {
|
"index" => {
|
||||||
match key.as_ref() {
|
preprocessors.push(Box::new(IndexPreprocessor::new()))
|
||||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
}
|
||||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
name => preprocessors.push(interpret_custom_preprocessor(
|
||||||
_ => bail!("{:?} is not a recognised preprocessor", key),
|
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]
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) => {
|
||||||
.map(|s| take_lines(&s, range.clone()))
|
let target = base.join(pat);
|
||||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
|
||||||
LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(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!(
|
||||||
.map(|s| take_lines(&s, range.clone()))
|
"Could not read file for link {} ({})",
|
||||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
self.link_text,
|
||||||
LinkType::IncludeRangeFull(ref pat, _) => file_to_string(base.join(pat))
|
target.display(),
|
||||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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) => {
|
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}}",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -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: (),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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: (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
Loading…
Reference in New Issue