mdBook/for_developers/backends.html

515 lines
29 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE HTML>
<html lang="en" class="light" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Alternative Backends - 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/backends.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="alternative-backends"><a class="header" href="#alternative-backends">Alternative Backends</a></h1>
<p>A "backend" is simply a program which <code>mdbook</code> will invoke during the book
rendering process. This program is passed a JSON representation of the book and
configuration information via <code>stdin</code>. Once the backend receives this
information it is free to do whatever it wants.</p>
<p>See <a href="../format/configuration/renderers.html">Configuring Renderers</a> for more information about using backends.</p>
<p>The community has developed several backends.
See the <a href="https://github.com/rust-lang/mdBook/wiki/Third-party-plugins">Third Party Plugins</a> wiki page for a list of available backends.</p>
<h2 id="setting-up"><a class="header" href="#setting-up">Setting Up</a></h2>
<p>This page will step you through creating your own alternative backend in the form
of a simple word counting program. Although it will be written in Rust, there's
no reason why it couldn't be accomplished using something like Python or Ruby.</p>
<p>First you'll want to create a new binary program and add <code>mdbook</code> as a
dependency.</p>
<pre><code class="language-shell">$ cargo new --bin mdbook-wordcount
$ cd mdbook-wordcount
$ cargo add mdbook
</code></pre>
<p>When our <code>mdbook-wordcount</code> plugin is invoked, <code>mdbook</code> will send it a JSON
version of <a href="https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html"><code>RenderContext</code></a> via our plugin's <code>stdin</code>. For convenience, there's
a <a href="https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json"><code>RenderContext::from_json()</code></a> constructor which will load a <code>RenderContext</code>.</p>
<p>This is all the boilerplate necessary for our backend to load the book.</p>
<pre><pre class="playground"><code class="language-rust edition2018">// src/main.rs
extern crate mdbook;
use std::io;
use mdbook::renderer::RenderContext;
fn main() {
let mut stdin = io::stdin();
let ctx = RenderContext::from_json(&amp;mut stdin).unwrap();
}</code></pre></pre>
<blockquote>
<p><strong>Note:</strong> The <code>RenderContext</code> contains a <code>version</code> field. This lets backends
figure out whether they are compatible with the version of <code>mdbook</code> it's being
called by. This <code>version</code> comes directly from the corresponding field in
<code>mdbook</code>'s <code>Cargo.toml</code>.</p>
</blockquote>
<p>It is recommended that backends use the <a href="https://crates.io/crates/semver"><code>semver</code></a> crate to inspect this field
and emit a warning if there may be a compatibility issue.</p>
<h2 id="inspecting-the-book"><a class="header" href="#inspecting-the-book">Inspecting the Book</a></h2>
<p>Now our backend has a copy of the book, lets count how many words are in each
chapter!</p>
<p>Because the <code>RenderContext</code> contains a <a href="https://docs.rs/mdbook/*/mdbook/book/struct.Book.html"><code>Book</code></a> field (<code>book</code>), and a <code>Book</code> has
the <a href="https://docs.rs/mdbook/*/mdbook/book/struct.Book.html#method.iter"><code>Book::iter()</code></a> method for iterating over all items in a <code>Book</code>, this step
turns out to be just as easy as the first.</p>
<pre><pre class="playground"><code class="language-rust edition2018">
fn main() {
let mut stdin = io::stdin();
let ctx = RenderContext::from_json(&amp;mut stdin).unwrap();
for item in ctx.book.iter() {
if let BookItem::Chapter(ref ch) = *item {
let num_words = count_words(ch);
println!("{}: {}", ch.name, num_words);
}
}
}
fn count_words(ch: &amp;Chapter) -&gt; usize {
ch.content.split_whitespace().count()
}</code></pre></pre>
<h2 id="enabling-the-backend"><a class="header" href="#enabling-the-backend">Enabling the Backend</a></h2>
<p>Now we've got the basics running, we want to actually use it. First, install the
program.</p>
<pre><code class="language-shell">$ cargo install --path .
</code></pre>
<p>Then <code>cd</code> to the particular book you'd like to count the words of and update its
<code>book.toml</code> file.</p>
<pre><code class="language-diff"> [book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David", "Michael-F-Bryan"]
+ [output.html]
+ [output.wordcount]
</code></pre>
<p>When it loads a book into memory, <code>mdbook</code> will inspect your <code>book.toml</code> file to
try and figure out which backends to use by looking for all <code>output.*</code> tables.
If none are provided it'll fall back to using the default HTML renderer.</p>
<p>Notably, this means if you want to add your own custom backend you'll also need
to make sure to add the HTML backend, even if its table just stays empty.</p>
<p>Now you just need to build your book like normal, and everything should <em>Just
Work</em>.</p>
<pre><code class="language-shell">$ mdbook build
...
2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
mdBook: 126
Command Line Tool: 224
init: 283
build: 145
watch: 146
serve: 292
test: 139
Format: 30
SUMMARY.md: 259
Configuration: 784
Theme: 304
index.hbs: 447
Syntax highlighting: 314
MathJax Support: 153
Rust code specific features: 148
For Developers: 788
Alternative Backends: 710
Contributors: 85
</code></pre>
<p>The reason we didn't need to specify the full name/path of our <code>wordcount</code>
backend is because <code>mdbook</code> will try to <em>infer</em> the program's name via
convention. The executable for the <code>foo</code> backend is typically called
<code>mdbook-foo</code>, with an associated <code>[output.foo]</code> entry in the <code>book.toml</code>. To
explicitly tell <code>mdbook</code> what command to invoke (it may require command-line
arguments or be an interpreted script), you can use the <code>command</code> field.</p>
<pre><code class="language-diff"> [book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David", "Michael-F-Bryan"]
[output.html]
[output.wordcount]
+ command = "python /path/to/wordcount.py"
</code></pre>
<h2 id="configuration"><a class="header" href="#configuration">Configuration</a></h2>
<p>Now imagine you don't want to count the number of words on a particular chapter
(it might be generated text/code, etc). The canonical way to do this is via the
usual <code>book.toml</code> configuration file by adding items to your <code>[output.foo]</code>
table.</p>
<p>The <code>Config</code> can be treated roughly as a nested hashmap which lets you call
methods like <code>get()</code> to access the config's contents, with a
<code>get_deserialized()</code> convenience method for retrieving a value and automatically
deserializing to some arbitrary type <code>T</code>.</p>
<p>To implement this, we'll create our own serializable <code>WordcountConfig</code> struct
which will encapsulate all configuration for this backend.</p>
<p>First add <code>serde</code> and <code>serde_derive</code> to your <code>Cargo.toml</code>,</p>
<pre><code>$ cargo add serde serde_derive
</code></pre>
<p>And then you can create the config struct,</p>
<pre><pre class="playground"><code class="language-rust edition2018"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>extern crate serde;
#[macro_use]
extern crate serde_derive;
...
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct WordcountConfig {
pub ignores: Vec&lt;String&gt;,
}
<span class="boring">}</span></code></pre></pre>
<p>Now we just need to deserialize the <code>WordcountConfig</code> from our <code>RenderContext</code>
and then add a check to make sure we skip ignored chapters.</p>
<pre><code class="language-diff"> fn main() {
let mut stdin = io::stdin();
let ctx = RenderContext::from_json(&amp;mut stdin).unwrap();
+ let cfg: WordcountConfig = ctx.config
+ .get_deserialized("output.wordcount")
+ .unwrap_or_default();
for item in ctx.book.iter() {
if let BookItem::Chapter(ref ch) = *item {
+ if cfg.ignores.contains(&amp;ch.name) {
+ continue;
+ }
+
let num_words = count_words(ch);
println!("{}: {}", ch.name, num_words);
}
}
}
</code></pre>
<h2 id="output-and-signalling-failure"><a class="header" href="#output-and-signalling-failure">Output and Signalling Failure</a></h2>
<p>While it's nice to print word counts to the terminal when a book is built, it
might also be a good idea to output them to a file somewhere. <code>mdbook</code> tells a
backend where it should place any generated output via the <code>destination</code> field
in <a href="https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html"><code>RenderContext</code></a>.</p>
<pre><code class="language-diff">+ use std::fs::{self, File};
+ use std::io::{self, Write};
- use std::io;
use mdbook::renderer::RenderContext;
use mdbook::book::{BookItem, Chapter};
fn main() {
...
+ let _ = fs::create_dir_all(&amp;ctx.destination);
+ let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
+
for item in ctx.book.iter() {
if let BookItem::Chapter(ref ch) = *item {
...
let num_words = count_words(ch);
println!("{}: {}", ch.name, num_words);
+ writeln!(f, "{}: {}", ch.name, num_words).unwrap();
}
}
}
</code></pre>
<blockquote>
<p><strong>Note:</strong> There is no guarantee that the destination directory exists or is
empty (<code>mdbook</code> may leave the previous contents to let backends do caching),
so it's always a good idea to create it with <code>fs::create_dir_all()</code>.</p>
<p>If the destination directory already exists, don't assume it will be empty.
To allow backends to cache the results from previous runs, <code>mdbook</code> may leave
old content in the directory.</p>
</blockquote>
<p>There's always the possibility that an error will occur while processing a book
(just look at all the <code>unwrap()</code>'s we've written already), so <code>mdbook</code> will
interpret a non-zero exit code as a rendering failure.</p>
<p>For example, if we wanted to make sure all chapters have an <em>even</em> number of
words, erroring out if an odd number is encountered, then you may do something
like this:</p>
<pre><code class="language-diff">+ use std::process;
...
fn main() {
...
for item in ctx.book.iter() {
if let BookItem::Chapter(ref ch) = *item {
...
let num_words = count_words(ch);
println!("{}: {}", ch.name, num_words);
writeln!(f, "{}: {}", ch.name, num_words).unwrap();
+ if cfg.deny_odds &amp;&amp; num_words % 2 == 1 {
+ eprintln!("{} has an odd number of words!", ch.name);
+ process::exit(1);
+ }
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct WordcountConfig {
pub ignores: Vec&lt;String&gt;,
+ pub deny_odds: bool,
}
</code></pre>
<p>Now, if we reinstall the backend and build a book,</p>
<pre><code class="language-shell">$ cargo install --path . --force
$ mdbook build /path/to/book
...
2018-01-16 21:21:39 [INFO] (mdbook::renderer): Invoking the "wordcount" renderer
mdBook: 126
Command Line Tool: 224
init: 283
init has an odd number of words!
2018-01-16 21:21:39 [ERROR] (mdbook::renderer): Renderer exited with non-zero return code.
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Error: Rendering failed
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Caused By: The "mdbook-wordcount" renderer failed
</code></pre>
<p>As you've probably already noticed, output from the plugin's subprocess is
immediately passed through to the user. It is encouraged for plugins to follow
the "rule of silence" and only generate output when necessary (e.g. an error in
generation or a warning).</p>
<p>All environment variables are passed through to the backend, allowing you to use
the usual <code>RUST_LOG</code> to control logging verbosity.</p>
<h2 id="wrapping-up"><a class="header" href="#wrapping-up">Wrapping Up</a></h2>
<p>Although contrived, hopefully this example was enough to show how you'd create
an alternative backend for <code>mdbook</code>. If you feel it's missing something, don't
hesitate to create an issue in the <a href="https://github.com/rust-lang/mdBook/issues">issue tracker</a> so we can improve the user
guide.</p>
<p>The existing backends mentioned towards the start of this chapter should serve
as a good example of how it's done in real life, so feel free to skim through
the source code or ask questions.</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../for_developers/preprocessors.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="../misc/contributors.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/preprocessors.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="../misc/contributors.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>