Skip to content

Commit 29f8b79

Browse files
Merge pull request #792 from rust-lang-nursery/custom-preprocessor
WIP: Custom Preprocessors
2 parents 877bf37 + b1c7c54 commit 29f8b79

File tree

10 files changed

+555
-125
lines changed

10 files changed

+555
-125
lines changed

book-example/src/for_developers/preprocessors.md

+56-50
Original file line numberDiff line numberDiff line change
@@ -11,68 +11,71 @@ the book. Possible use cases are:
1111
mathjax equivalents
1212

1313

14-
## Implementing a Preprocessor
14+
## Hooking Into MDBook
15+
16+
MDBook uses a fairly simple mechanism for discovering third party plugins.
17+
A new table is added to `book.toml` (e.g. `preprocessor.foo` for the `foo`
18+
preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
19+
part of the build process.
20+
21+
While preprocessors can be hard-coded to specify which backend it should be run
22+
for (e.g. it doesn't make sense for MathJax to be used for non-HTML renderers)
23+
with the `preprocessor.foo.renderer` key.
24+
25+
```toml
26+
[book]
27+
title = "My Book"
28+
authors = ["Michael-F-Bryan"]
29+
30+
[preprocessor.foo]
31+
# The command can also be specified manually
32+
command = "python3 /path/to/foo.py"
33+
# Only run the `foo` preprocessor for the HTML and EPUB renderer
34+
renderer = ["html", "epub"]
35+
```
1536

16-
A preprocessor is represented by the `Preprocessor` trait.
37+
In typical unix style, all inputs to the plugin will be written to `stdin` as
38+
JSON and `mdbook` will read from `stdout` if it is expecting output.
1739

18-
```rust
19-
pub trait Preprocessor {
20-
fn name(&self) -> &str;
21-
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
22-
fn supports_renderer(&self, _renderer: &str) -> bool {
23-
true
24-
}
25-
}
26-
```
40+
The easiest way to get started is by creating your own implementation of the
41+
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
42+
translates inputs to the correct `Preprocessor` method. For convenience, there
43+
is [an example no-op preprocessor] in the `examples/` directory which can easily
44+
be adapted for other preprocessors.
2745

28-
Where the `PreprocessorContext` is defined as
46+
<details>
47+
<summary>Example no-op preprocessor</summary>
2948

3049
```rust
31-
pub struct PreprocessorContext {
32-
pub root: PathBuf,
33-
pub config: Config,
34-
/// The `Renderer` this preprocessor is being used with.
35-
pub renderer: String,
36-
}
37-
```
38-
39-
The `renderer` value allows you react accordingly, for example, PDF or HTML.
50+
// nop-preprocessors.rs
4051

41-
## A complete Example
52+
{{#include ../../../examples/nop-preprocessor.rs}}
53+
```
54+
</details>
4255

43-
The magic happens within the `run(...)` method of the
44-
[`Preprocessor`][preprocessor-docs] trait implementation.
56+
## Hints For Implementing A Preprocessor
4557

46-
As direct access to the chapters is not possible, you will probably end up
47-
iterating them using `for_each_mut(...)`:
58+
By pulling in `mdbook` as a library, preprocessors can have access to the
59+
existing infrastructure for dealing with books.
4860

49-
```rust
50-
book.for_each_mut(|item: &mut BookItem| {
51-
if let BookItem::Chapter(ref mut chapter) = *item {
52-
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
53-
res = Some(
54-
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
55-
Ok(md) => {
56-
chapter.content = md;
57-
Ok(())
58-
}
59-
Err(err) => Err(err),
60-
},
61-
);
62-
}
63-
});
64-
```
61+
For example, a custom preprocessor could use the
62+
[`CmdPreprocessor::parse_input()`] function to deserialize the JSON written to
63+
`stdin`. Then each chapter of the `Book` can be mutated in-place via
64+
[`Book::for_each_mut()`], and then written to `stdout` with the `serde_json`
65+
crate.
6566

66-
The `chapter.content` is just a markdown formatted string, and you will have to
67-
process it in some way. Even though it's entirely possible to implement some
68-
sort of manual find & replace operation, if that feels too unsafe you can use
69-
[`pulldown-cmark`][pc] to parse the string into events and work on them instead.
67+
Chapters can be accessed either directly (by recursively iterating over
68+
chapters) or via the `Book::for_each_mut()` convenience method.
7069

71-
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events
72-
back to a string.
70+
The `chapter.content` is just a string which happens to be markdown. While it's
71+
entirely possible to use regular expressions or do a manual find & replace,
72+
you'll probably want to process the input into something more computer-friendly.
73+
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
74+
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to
75+
translate events back into markdown text.
7376

74-
The following code block shows how to remove all emphasis from markdown, and do
75-
so safely.
77+
The following code block shows how to remove all emphasis from markdown,
78+
without accidentally breaking the document.
7679

7780
```rust
7881
fn remove_emphasis(
@@ -107,3 +110,6 @@ For everything else, have a look [at the complete example][example].
107110
[pc]: https://crates.io/crates/pulldown-cmark
108111
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
109112
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs
113+
[an example no-op preprocessor]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/nop-preprocessor.rs
114+
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
115+
[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut

examples/de-emphasize.rs

+4-20
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,20 @@
1-
//! This program removes all forms of emphasis from the markdown of the book.
1+
//! An example preprocessor for removing all forms of emphasis from a markdown
2+
//! book.
3+
24
extern crate mdbook;
35
extern crate pulldown_cmark;
46
extern crate pulldown_cmark_to_cmark;
57

68
use mdbook::book::{Book, BookItem, Chapter};
79
use mdbook::errors::{Error, Result};
810
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
9-
use mdbook::MDBook;
1011
use pulldown_cmark::{Event, Parser, Tag};
1112
use pulldown_cmark_to_cmark::fmt::cmark;
1213

13-
use std::env::{args, args_os};
14-
use std::ffi::OsString;
15-
use std::process;
16-
1714
const NAME: &str = "md-links-to-html-links";
1815

19-
fn do_it(book: OsString) -> Result<()> {
20-
let mut book = MDBook::load(book)?;
21-
book.with_preprecessor(Deemphasize);
22-
book.build()
23-
}
24-
2516
fn main() {
26-
if args_os().count() != 2 {
27-
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
28-
return;
29-
}
30-
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
31-
eprintln!("{}", e);
32-
process::exit(1);
33-
}
17+
panic!("This example is intended to be part of a library");
3418
}
3519

3620
struct Deemphasize;

examples/nop-preprocessor.rs

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
extern crate clap;
2+
extern crate mdbook;
3+
extern crate serde_json;
4+
5+
use clap::{App, Arg, ArgMatches, SubCommand};
6+
use mdbook::book::Book;
7+
use mdbook::errors::Error;
8+
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
9+
use std::io;
10+
use std::process;
11+
use nop_lib::Nop;
12+
13+
pub fn make_app() -> App<'static, 'static> {
14+
App::new("nop-preprocessor")
15+
.about("A mdbook preprocessor which does precisely nothing")
16+
.subcommand(
17+
SubCommand::with_name("supports")
18+
.arg(Arg::with_name("renderer").required(true))
19+
.about("Check whether a renderer is supported by this preprocessor"))
20+
}
21+
22+
fn main() {
23+
let matches = make_app().get_matches();
24+
25+
// Users will want to construct their own preprocessor here
26+
let preprocessor = Nop::new();
27+
28+
if let Some(sub_args) = matches.subcommand_matches("supports") {
29+
handle_supports(&preprocessor, sub_args);
30+
} else {
31+
if let Err(e) = handle_preprocessing(&preprocessor) {
32+
eprintln!("{}", e);
33+
process::exit(1);
34+
}
35+
}
36+
}
37+
38+
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
39+
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
40+
41+
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
42+
// We should probably use the `semver` crate to check compatibility
43+
// here...
44+
eprintln!(
45+
"Warning: The {} plugin was built against version {} of mdbook, \
46+
but we're being called from version {}",
47+
pre.name(),
48+
mdbook::MDBOOK_VERSION,
49+
ctx.mdbook_version
50+
);
51+
}
52+
53+
let processed_book = pre.run(&ctx, book)?;
54+
serde_json::to_writer(io::stdout(), &processed_book)?;
55+
56+
Ok(())
57+
}
58+
59+
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
60+
let renderer = sub_args.value_of("renderer").expect("Required argument");
61+
let supported = pre.supports_renderer(&renderer);
62+
63+
// Signal whether the renderer is supported by exiting with 1 or 0.
64+
if supported {
65+
process::exit(0);
66+
} else {
67+
process::exit(1);
68+
}
69+
}
70+
71+
/// The actual implementation of the `Nop` preprocessor. This would usually go
72+
/// in your main `lib.rs` file.
73+
mod nop_lib {
74+
use super::*;
75+
76+
/// A no-op preprocessor.
77+
pub struct Nop;
78+
79+
impl Nop {
80+
pub fn new() -> Nop {
81+
Nop
82+
}
83+
}
84+
85+
impl Preprocessor for Nop {
86+
fn name(&self) -> &str {
87+
"nop-preprocessor"
88+
}
89+
90+
fn run(
91+
&self,
92+
ctx: &PreprocessorContext,
93+
book: Book,
94+
) -> Result<Book, Error> {
95+
// In testing we want to tell the preprocessor to blow up by setting a
96+
// particular config value
97+
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
98+
if nop_cfg.contains_key("blow-up") {
99+
return Err("Boom!!1!".into());
100+
}
101+
}
102+
103+
// we *are* a no-op preprocessor after all
104+
Ok(book)
105+
}
106+
107+
fn supports_renderer(&self, renderer: &str) -> bool {
108+
renderer != "not-supported"
109+
}
110+
}
111+
}
112+

0 commit comments

Comments
 (0)