mdBook/for_developers/preprocessors.html

503 lines
28 KiB
HTML
Raw Normal View History

<!DOCTYPE HTML>
<html lang="en" class="light" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Preprocessors - mdBook Documentation</title>
<!-- Custom HTML head -->
<meta name="description" content="Create book from markdown files. Like Gitbook but implemented in Rust">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<!-- MathJax -->
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
</head>
<body class="sidebar-visible no-js">
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('light')
html.classList.add(theme);
var body = document.querySelector('body');
body.classList.remove('no-js')
body.classList.add('js');
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var body = document.querySelector('body');
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
body.classList.remove('sidebar-visible');
body.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="../index.html">Introduction</a></li><li class="chapter-item expanded affix "><li class="part-title">User Guide</li><li class="chapter-item expanded "><a href="../guide/installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li class="chapter-item expanded "><a href="../guide/reading.html"><strong aria-hidden="true">2.</strong> Reading Books</a></li><li class="chapter-item expanded "><a href="../guide/creating.html"><strong aria-hidden="true">3.</strong> Creating a Book</a></li><li class="chapter-item expanded affix "><li class="part-title">Reference Guide</li><li class="chapter-item expanded "><a href="../cli/index.html"><strong aria-hidden="true">4.</strong> Command Line Tool</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../cli/init.html"><strong aria-hidden="true">4.1.</strong> init</a></li><li class="chapter-item expanded "><a href="../cli/build.html"><strong aria-hidden="true">4.2.</strong> build</a></li><li class="chapter-item expanded "><a href="../cli/watch.html"><strong aria-hidden="true">4.3.</strong> watch</a></li><li class="chapter-item expanded "><a href="../cli/serve.html"><strong aria-hidden="true">4.4.</strong> serve</a></li><li class="chapter-item expanded "><a href="../cli/test.html"><strong aria-hidden="true">4.5.</strong> test</a></li><li class="chapter-item expanded "><a href="../cli/clean.html"><strong aria-hidden="true">4.6.</strong> clean</a></li><li class="chapter-item expanded "><a href="../cli/completions.html"><strong aria-hidden="true">4.7.</strong> completions</a></li></ol></li><li class="chapter-item expanded "><a href="../format/index.html"><strong aria-hidden="true">5.</strong> Format</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../format/summary.html"><strong aria-hidden="true">5.1.</strong> SUMMARY.md</a></li><li><ol class="section"><li class="chapter-item expanded "><div><strong aria-hidden="true">5.1.1.</strong> Draft chapter</div></li></ol></li><li class="chapter-item expanded "><a href="../format/configuration/index.html"><strong aria-hidden="true">5.2.</strong> Configuration</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../format/configuration/general.html"><strong aria-hidden="true">5.2.1.</strong> General</a></li><li class="chapter-item expanded "><a href="../format/configuration/preprocessors.html"><strong aria-hidden="true">5.2.2.</strong> Preprocessors</a></li><li class="chapter-item expanded "><a href="../format/configuration/renderers.html"><strong aria-hidden="true">5.2.3.</strong> Renderers</a></li><li class="chapter-item expanded "><a href="../format/configuration/environment-variables.html"><strong aria-hidden="true">5.2.4.</strong> Environment Variables</a></li></ol></li><li class="chapter-item expanded "><a href="../format/theme/index.html"><strong aria-hidden="true">5.3.</strong> Theme</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../format/theme/index-hbs.html"><strong aria-hidden="true">5.3.1.</strong> index.hbs</a></li><li class="chapter-item expanded "><a href="../format/theme/syntax-highlighting.html"><strong aria-hidden="true">5.3.2.</strong> Syntax highlighting</a></li><li class="chapter-item expanded "><a href="../format/theme/editor.html"><strong aria-hidden="true">5.3.3.</strong> Editor</a></li></ol></li><li class="chapter-item expanded "><a href="../format/mathjax.html"><strong aria-hidden="true">5.4.</strong> MathJax Support</a></li><li class="chapter-item expanded "><a href="../format/mdbook.html"><strong aria-hidden="true">5.5.</strong> mdBook-specific features</a></li><li class="chapter-item expanded "><a href="../format/markdown.html"><strong aria-hidden="true">5.6.</strong> Markdown</a></li></ol></li><li class="chapter-item expanded "><a href="../continuous-integration.html"><strong aria-hidden="true">6.</strong> Continuous Integration</a></li><li class="chapter-item expanded "><a href="../for_developers/index.html"><strong
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">mdBook Documentation</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/rust-lang/mdBook/tree/master/guide" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
<a href="https://github.com/rust-lang/mdBook/edit/master/guide/src/for_developers/preprocessors.md" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="preprocessors"><a class="header" href="#preprocessors">Preprocessors</a></h1>
<p>A <em>preprocessor</em> 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:</p>
<ul>
<li>Creating custom helpers like <code>{{#include /path/to/file.md}}</code></li>
<li>Substituting in latex-style expressions (<code>$$ \frac{1}{3} $$</code>) with their
mathjax equivalents</li>
</ul>
<p>See <a href="../format/configuration/preprocessors.html">Configuring Preprocessors</a> for more information about using preprocessors.</p>
<h2 id="hooking-into-mdbook"><a class="header" href="#hooking-into-mdbook">Hooking Into MDBook</a></h2>
<p>MDBook uses a fairly simple mechanism for discovering third party plugins.
A new table is added to <code>book.toml</code> (e.g. <code>[preprocessor.foo]</code> for the <code>foo</code>
preprocessor) and then <code>mdbook</code> will try to invoke the <code>mdbook-foo</code> program as
part of the build process.</p>
<p>Once the preprocessor has been defined and the build process starts, mdBook executes the command defined in the <code>preprocessor.foo.command</code> key twice.
The first time it runs the preprocessor to determine if it supports the given renderer.
mdBook passes two arguments to the process: the first argument is the string <code>supports</code> and the second argument is the renderer name.
The preprocessor should exit with a status code 0 if it supports the given renderer, or return a non-zero exit code if it does not.</p>
<p>If the preprocessor supports the renderer, then mdbook runs it a second time, passing JSON data into stdin.
The JSON consists of an array of <code>[context, book]</code> where <code>context</code> is the serialized object <a href="https://docs.rs/mdbook/latest/mdbook/preprocess/struct.PreprocessorContext.html"><code>PreprocessorContext</code></a> and <code>book</code> is a <a href="https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html"><code>Book</code></a> object containing the content of the book.</p>
<p>The preprocessor should return the JSON format of the <a href="https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html"><code>Book</code></a> object to stdout, with any modifications it wishes to perform.</p>
<p>The easiest way to get started is by creating your own implementation of the
<code>Preprocessor</code> trait (e.g. in <code>lib.rs</code>) and then creating a shell binary which
translates inputs to the correct <code>Preprocessor</code> method. For convenience, there
is <a href="https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs">an example no-op preprocessor</a> in the <code>examples/</code> directory which can easily
be adapted for other preprocessors.</p>
<details>
<summary>Example no-op preprocessor</summary>
<pre><pre class="playground"><code class="language-rust edition2018">// nop-preprocessors.rs
use crate::nop_lib::Nop;
use clap::{Arg, ArgMatches, Command};
use mdbook::book::Book;
use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
use semver::{Version, VersionReq};
use std::io;
use std::process;
pub fn make_app() -&gt; Command {
Command::new("nop-preprocessor")
.about("A mdbook preprocessor which does precisely nothing")
.subcommand(
Command::new("supports")
.arg(Arg::new("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(&amp;preprocessor, sub_args);
} else if let Err(e) = handle_preprocessing(&amp;preprocessor) {
eprintln!("{}", e);
process::exit(1);
}
}
fn handle_preprocessing(pre: &amp;dyn Preprocessor) -&gt; Result&lt;(), Error&gt; {
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
let book_version = Version::parse(&amp;ctx.mdbook_version)?;
let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
if !version_req.matches(&amp;book_version) {
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(&amp;ctx, book)?;
serde_json::to_writer(io::stdout(), &amp;processed_book)?;
Ok(())
}
fn handle_supports(pre: &amp;dyn Preprocessor, sub_args: &amp;ArgMatches) -&gt; ! {
let renderer = sub_args
.get_one::&lt;String&gt;("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() -&gt; Nop {
Nop
}
}
impl Preprocessor for Nop {
fn name(&amp;self) -&gt; &amp;str {
"nop-preprocessor"
}
fn run(&amp;self, ctx: &amp;PreprocessorContext, book: Book) -&gt; Result&lt;Book, Error&gt; {
// 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") {
anyhow::bail!("Boom!!1!");
}
}
// we *are* a no-op preprocessor after all
Ok(book)
}
fn supports_renderer(&amp;self, renderer: &amp;str) -&gt; bool {
renderer != "not-supported"
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn nop_preprocessor_run() {
let input_json = r##"[
{
"root": "/path/to/book",
"config": {
"book": {
"authors": ["AUTHOR"],
"language": "en",
"multilingual": false,
"src": "src",
"title": "TITLE"
},
"preprocessor": {
"nop": {}
}
},
"renderer": "html",
"mdbook_version": "0.4.21"
},
{
"sections": [
{
"Chapter": {
"name": "Chapter 1",
"content": "# Chapter 1\n",
"number": [1],
"sub_items": [],
"path": "chapter_1.md",
"source_path": "chapter_1.md",
"parent_names": []
}
}
],
"__non_exhaustive": null
}
]"##;
let input_json = input_json.as_bytes();
let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
let expected_book = book.clone();
let result = Nop::new().run(&amp;ctx, book);
assert!(result.is_ok());
// The nop-preprocessor should not have made any changes to the book content.
let actual_book = result.unwrap();
assert_eq!(actual_book, expected_book);
}
}
}</code></pre></pre>
</details>
<h2 id="hints-for-implementing-a-preprocessor"><a class="header" href="#hints-for-implementing-a-preprocessor">Hints For Implementing A Preprocessor</a></h2>
<p>By pulling in <code>mdbook</code> as a library, preprocessors can have access to the
existing infrastructure for dealing with books.</p>
<p>For example, a custom preprocessor could use the
<a href="https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input"><code>CmdPreprocessor::parse_input()</code></a> function to deserialize the JSON written to
<code>stdin</code>. Then each chapter of the <code>Book</code> can be mutated in-place via
<a href="https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut"><code>Book::for_each_mut()</code></a>, and then written to <code>stdout</code> with the <code>serde_json</code>
crate.</p>
<p>Chapters can be accessed either directly (by recursively iterating over
chapters) or via the <code>Book::for_each_mut()</code> convenience method.</p>
<p>The <code>chapter.content</code> is just a string which happens to be markdown. While it's
entirely possible to use regular expressions or do a manual find &amp; replace,
you'll probably want to process the input into something more computer-friendly.
The <a href="https://crates.io/crates/pulldown-cmark"><code>pulldown-cmark</code></a> crate implements a production-quality event-based
Markdown parser, with the <a href="https://crates.io/crates/pulldown-cmark-to-cmark"><code>pulldown-cmark-to-cmark</code></a> crate allowing you to
translate events back into markdown text.</p>
<p>The following code block shows how to remove all emphasis from markdown,
without accidentally breaking the document.</p>
<pre><pre class="playground"><code class="language-rust edition2018"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>fn remove_emphasis(
num_removed_items: &amp;mut usize,
chapter: &amp;mut Chapter,
) -&gt; Result&lt;String&gt; {
let mut buf = String::with_capacity(chapter.content.len());
let events = Parser::new(&amp;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) =&gt; false,
_ =&gt; true,
};
if !should_keep {
*num_removed_items += 1;
}
should_keep
});
cmark(events, &amp;mut buf, None).map(|_| buf).map_err(|err| {
Error::from(format!("Markdown serialization failed: {}", err))
})
}
<span class="boring">}</span></code></pre></pre>
<p>For everything else, have a look <a href="https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs">at the complete example</a>.</p>
<h2 id="implementing-a-preprocessor-with-a-different-language"><a class="header" href="#implementing-a-preprocessor-with-a-different-language">Implementing a preprocessor with a different language</a></h2>
<p>The fact that mdBook utilizes stdin and stdout to communicate with the preprocessors makes it easy to implement them in a language other than Rust.
The following code shows how to implement a simple preprocessor in Python, which will modify the content of the first chapter.
The example below follows the configuration shown above with <code>preprocessor.foo.command</code> actually pointing to a Python script.</p>
<pre><code class="language-python">import json
import sys
if __name__ == '__main__':
if len(sys.argv) &gt; 1: # we check if we received any argument
if sys.argv[1] == "supports":
# then we are good to return an exit status code of 0, since the other argument will just be the renderer's name
sys.exit(0)
# load both the context and the book representations from stdin
context, book = json.load(sys.stdin)
# and now, we can just modify the content of the first chapter
book['sections'][0]['Chapter']['content'] = '# Hello'
# we are done with the book's modification, we can just print it to stdout,
print(json.dumps(book))
</code></pre>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../for_developers/index.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../for_developers/backends.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../for_developers/index.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../for_developers/backends.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_line_numbers = true;
</script>
<script>
window.playground_copyable = true;
</script>
<script src="../ace.js"></script>
<script src="../editor.js"></script>
<script src="../mode-rust.js"></script>
<script src="../theme-dawn.js"></script>
<script src="../theme-tomorrow_night.js"></script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>