Skip to content

Commit

Permalink
Merge pull request #792 from rust-lang-nursery/custom-preprocessor
Browse files Browse the repository at this point in the history
WIP: Custom Preprocessors
  • Loading branch information
Michael-F-Bryan authored Oct 15, 2018
2 parents 877bf37 + b1c7c54 commit 29f8b79
Show file tree
Hide file tree
Showing 10 changed files with 555 additions and 125 deletions.
106 changes: 56 additions & 50 deletions book-example/src/for_developers/preprocessors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
24 changes: 4 additions & 20 deletions examples/de-emphasize.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,20 @@
//! 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;

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;
Expand Down
112 changes: 112 additions & 0 deletions examples/nop-preprocessor.rs
Original file line number Diff line number Diff line change
@@ -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"
}
}
}

Loading

0 comments on commit 29f8b79

Please sign in to comment.