Preprocessors
A preprocessor is simply a bit of code which gets run immediately after the book is loaded and before it gets rendered, allowing you to update and mutate the book. Possible use cases are:
- Creating custom helpers like
{{#include /path/to/file.md}}
- Updating links so
[some chapter](some_chapter.md)
is automatically changed to[some chapter](some_chapter.html)
for the HTML renderer - Substituting in latex-style expressions (
$$ \frac{1}{3} $$
) with their mathjax equivalents
Implementing a Preprocessor
A preprocessor is represented by the Preprocessor
trait.
# #![allow(unused_variables)] #fn main() { pub trait Preprocessor { fn name(&self) -> &str; fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>; } #}
Where the PreprocessorContext
is defined as
# #![allow(unused_variables)] #fn main() { pub struct PreprocessorContext { pub root: PathBuf, pub config: Config, } #}
A complete Example
The magic happens within the run(...)
method of the
Preprocessor
trait implementation.
As direct access to the chapters is not possible, you will probably end up
iterating them using for_each_mut(...)
:
# #![allow(unused_variables)] #fn main() { 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), }, ); } }); #}
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
to parse the string into events and work on them instead.
Finally you can use pulldown-cmark-to-cmark
to transform these events
back to a string.
The following code block shows how to remove all emphasis from markdown, and do so safely.
# #![allow(unused_variables)] #fn main() { fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result<String> { let mut buf = String::with_capacity(chapter.content.len()); let events = Parser::new(&chapter.content).filter(|e| { let should_keep = match *e { Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong) | Event::End(Tag::Emphasis) | Event::End(Tag::Strong) => false, _ => true, }; if !should_keep { *num_removed_items += 1; } should_keep }); cmark(events, &mut buf, None) .map(|_| buf) .map_err(|err| Error::from(format!("Markdown serialization failed: {}", err))) } #}
For everything else, have a look at the complete example.