summaryrefslogtreecommitdiffstats
path: root/vendor/mdbook/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
commit698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch)
tree173a775858bd501c378080a10dca74132f05bc50 /vendor/mdbook/src
parentInitial commit. (diff)
downloadrustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz
rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/mdbook/src')
-rw-r--r--vendor/mdbook/src/book/book.rs640
-rw-r--r--vendor/mdbook/src/book/init.rs207
-rw-r--r--vendor/mdbook/src/book/mod.rs836
-rw-r--r--vendor/mdbook/src/book/summary.rs1097
-rw-r--r--vendor/mdbook/src/cmd/build.rs50
-rw-r--r--vendor/mdbook/src/cmd/clean.rs44
-rw-r--r--vendor/mdbook/src/cmd/init.rs126
-rw-r--r--vendor/mdbook/src/cmd/mod.rs10
-rw-r--r--vendor/mdbook/src/cmd/serve.rs177
-rw-r--r--vendor/mdbook/src/cmd/test.rs54
-rw-r--r--vendor/mdbook/src/cmd/watch.rs175
-rw-r--r--vendor/mdbook/src/config.rs1190
-rw-r--r--vendor/mdbook/src/lib.rs119
-rw-r--r--vendor/mdbook/src/main.rs150
-rw-r--r--vendor/mdbook/src/preprocess/cmd.rs207
-rw-r--r--vendor/mdbook/src/preprocess/index.rs105
-rw-r--r--vendor/mdbook/src/preprocess/links.rs937
-rw-r--r--vendor/mdbook/src/preprocess/mod.rs70
-rw-r--r--vendor/mdbook/src/renderer/html_handlebars/hbs_renderer.rs1106
-rw-r--r--vendor/mdbook/src/renderer/html_handlebars/helpers/mod.rs3
-rw-r--r--vendor/mdbook/src/renderer/html_handlebars/helpers/navigation.rs290
-rw-r--r--vendor/mdbook/src/renderer/html_handlebars/helpers/theme.rs28
-rw-r--r--vendor/mdbook/src/renderer/html_handlebars/helpers/toc.rs203
-rw-r--r--vendor/mdbook/src/renderer/html_handlebars/mod.rs9
-rw-r--r--vendor/mdbook/src/renderer/html_handlebars/search.rs286
-rw-r--r--vendor/mdbook/src/renderer/markdown_renderer.rs52
-rw-r--r--vendor/mdbook/src/renderer/mod.rs265
-rw-r--r--vendor/mdbook/src/theme/ayu-highlight.css78
-rw-r--r--vendor/mdbook/src/theme/book.js679
-rw-r--r--vendor/mdbook/src/theme/css/chrome.css534
-rw-r--r--vendor/mdbook/src/theme/css/general.css191
-rw-r--r--vendor/mdbook/src/theme/css/print.css54
-rw-r--r--vendor/mdbook/src/theme/css/variables.css253
-rw-r--r--vendor/mdbook/src/theme/favicon.pngbin0 -> 5679 bytes
-rwxr-xr-xvendor/mdbook/src/theme/favicon.svg22
-rw-r--r--vendor/mdbook/src/theme/head.hbs1
-rw-r--r--vendor/mdbook/src/theme/header.hbs1
-rw-r--r--vendor/mdbook/src/theme/index.hbs314
-rw-r--r--vendor/mdbook/src/theme/mod.rs270
-rw-r--r--vendor/mdbook/src/theme/redirect.hbs12
-rw-r--r--vendor/mdbook/src/theme/searcher/mod.rs6
-rw-r--r--vendor/mdbook/src/theme/searcher/searcher.js483
-rw-r--r--vendor/mdbook/src/theme/tomorrow-night.css102
-rw-r--r--vendor/mdbook/src/utils/fs.rs275
-rw-r--r--vendor/mdbook/src/utils/mod.rs494
-rw-r--r--vendor/mdbook/src/utils/string.rs255
-rw-r--r--vendor/mdbook/src/utils/toml_ext.rs130
47 files changed, 12590 insertions, 0 deletions
diff --git a/vendor/mdbook/src/book/book.rs b/vendor/mdbook/src/book/book.rs
new file mode 100644
index 000000000..d28c22dad
--- /dev/null
+++ b/vendor/mdbook/src/book/book.rs
@@ -0,0 +1,640 @@
+use std::collections::VecDeque;
+use std::fmt::{self, Display, Formatter};
+use std::fs::{self, File};
+use std::io::{Read, Write};
+use std::path::{Path, PathBuf};
+
+use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
+use crate::config::BuildConfig;
+use crate::errors::*;
+use crate::utils::bracket_escape;
+
+use serde::{Deserialize, Serialize};
+
+/// Load a book into memory from its `src/` directory.
+pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
+ let src_dir = src_dir.as_ref();
+ let summary_md = src_dir.join("SUMMARY.md");
+
+ let mut summary_content = String::new();
+ File::open(&summary_md)
+ .with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))?
+ .read_to_string(&mut summary_content)?;
+
+ let summary = parse_summary(&summary_content)
+ .with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
+
+ if cfg.create_missing {
+ create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
+ }
+
+ load_book_from_disk(&summary, src_dir)
+}
+
+fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
+ let mut items: Vec<_> = summary
+ .prefix_chapters
+ .iter()
+ .chain(summary.numbered_chapters.iter())
+ .chain(summary.suffix_chapters.iter())
+ .collect();
+
+ while !items.is_empty() {
+ let next = items.pop().expect("already checked");
+
+ if let SummaryItem::Link(ref link) = *next {
+ if let Some(ref location) = link.location {
+ let filename = src_dir.join(location);
+ if !filename.exists() {
+ if let Some(parent) = filename.parent() {
+ if !parent.exists() {
+ fs::create_dir_all(parent)?;
+ }
+ }
+ debug!("Creating missing file {}", filename.display());
+
+ let mut f = File::create(&filename).with_context(|| {
+ format!("Unable to create missing file: {}", filename.display())
+ })?;
+ writeln!(f, "# {}", bracket_escape(&link.name))?;
+ }
+ }
+
+ items.extend(&link.nested_items);
+ }
+ }
+
+ Ok(())
+}
+
+/// A dumb tree structure representing a book.
+///
+/// For the moment a book is just a collection of [`BookItems`] which are
+/// accessible by either iterating (immutably) over the book with [`iter()`], or
+/// recursively applying a closure to each section to mutate the chapters, using
+/// [`for_each_mut()`].
+///
+/// [`iter()`]: #method.iter
+/// [`for_each_mut()`]: #method.for_each_mut
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
+pub struct Book {
+ /// The sections in this book.
+ pub sections: Vec<BookItem>,
+ __non_exhaustive: (),
+}
+
+impl Book {
+ /// Create an empty book.
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Get a depth-first iterator over the items in the book.
+ pub fn iter(&self) -> BookItems<'_> {
+ BookItems {
+ items: self.sections.iter().collect(),
+ }
+ }
+
+ /// Recursively apply a closure to each item in the book, allowing you to
+ /// mutate them.
+ ///
+ /// # Note
+ ///
+ /// Unlike the `iter()` method, this requires a closure instead of returning
+ /// an iterator. This is because using iterators can possibly allow you
+ /// to have iterator invalidation errors.
+ pub fn for_each_mut<F>(&mut self, mut func: F)
+ where
+ F: FnMut(&mut BookItem),
+ {
+ for_each_mut(&mut func, &mut self.sections);
+ }
+
+ /// Append a `BookItem` to the `Book`.
+ pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
+ self.sections.push(item.into());
+ self
+ }
+}
+
+pub fn for_each_mut<'a, F, I>(func: &mut F, items: I)
+where
+ F: FnMut(&mut BookItem),
+ I: IntoIterator<Item = &'a mut BookItem>,
+{
+ for item in items {
+ if let BookItem::Chapter(ch) = item {
+ for_each_mut(func, &mut ch.sub_items);
+ }
+
+ func(item);
+ }
+}
+
+/// Enum representing any type of item which can be added to a book.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub enum BookItem {
+ /// A nested chapter.
+ Chapter(Chapter),
+ /// A section separator.
+ Separator,
+ /// A part title.
+ PartTitle(String),
+}
+
+impl From<Chapter> for BookItem {
+ fn from(other: Chapter) -> BookItem {
+ BookItem::Chapter(other)
+ }
+}
+
+/// The representation of a "chapter", usually mapping to a single file on
+/// disk however it may contain multiple sub-chapters.
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
+pub struct Chapter {
+ /// The chapter's name.
+ pub name: String,
+ /// The chapter's contents.
+ pub content: String,
+ /// The chapter's section number, if it has one.
+ pub number: Option<SectionNumber>,
+ /// Nested items.
+ pub sub_items: Vec<BookItem>,
+ /// The chapter's location, relative to the `SUMMARY.md` file.
+ pub path: Option<PathBuf>,
+ /// The chapter's source file, relative to the `SUMMARY.md` file.
+ pub source_path: Option<PathBuf>,
+ /// An ordered list of the names of each chapter above this one in the hierarchy.
+ pub parent_names: Vec<String>,
+}
+
+impl Chapter {
+ /// Create a new chapter with the provided content.
+ pub fn new<P: Into<PathBuf>>(
+ name: &str,
+ content: String,
+ p: P,
+ parent_names: Vec<String>,
+ ) -> Chapter {
+ let path: PathBuf = p.into();
+ Chapter {
+ name: name.to_string(),
+ content,
+ path: Some(path.clone()),
+ source_path: Some(path),
+ parent_names,
+ ..Default::default()
+ }
+ }
+
+ /// Create a new draft chapter that is not attached to a source markdown file (and thus
+ /// has no content).
+ pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
+ Chapter {
+ name: name.to_string(),
+ content: String::new(),
+ path: None,
+ source_path: None,
+ parent_names,
+ ..Default::default()
+ }
+ }
+
+ /// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
+ pub fn is_draft_chapter(&self) -> bool {
+ self.path.is_none()
+ }
+}
+
+/// Use the provided `Summary` to load a `Book` from disk.
+///
+/// You need to pass in the book's source directory because all the links in
+/// `SUMMARY.md` give the chapter locations relative to it.
+pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
+ debug!("Loading the book from disk");
+ let src_dir = src_dir.as_ref();
+
+ let prefix = summary.prefix_chapters.iter();
+ let numbered = summary.numbered_chapters.iter();
+ let suffix = summary.suffix_chapters.iter();
+
+ let summary_items = prefix.chain(numbered).chain(suffix);
+
+ let mut chapters = Vec::new();
+
+ for summary_item in summary_items {
+ let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
+ chapters.push(chapter);
+ }
+
+ Ok(Book {
+ sections: chapters,
+ __non_exhaustive: (),
+ })
+}
+
+fn load_summary_item<P: AsRef<Path> + Clone>(
+ item: &SummaryItem,
+ src_dir: P,
+ parent_names: Vec<String>,
+) -> Result<BookItem> {
+ match item {
+ SummaryItem::Separator => Ok(BookItem::Separator),
+ SummaryItem::Link(ref link) => {
+ load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
+ }
+ SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
+ }
+}
+
+fn load_chapter<P: AsRef<Path>>(
+ link: &Link,
+ src_dir: P,
+ parent_names: Vec<String>,
+) -> Result<Chapter> {
+ let src_dir = src_dir.as_ref();
+
+ let mut ch = if let Some(ref link_location) = link.location {
+ debug!("Loading {} ({})", link.name, link_location.display());
+
+ let location = if link_location.is_absolute() {
+ link_location.clone()
+ } else {
+ src_dir.join(link_location)
+ };
+
+ let mut f = File::open(&location)
+ .with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
+
+ let mut content = String::new();
+ f.read_to_string(&mut content).with_context(|| {
+ format!("Unable to read \"{}\" ({})", link.name, location.display())
+ })?;
+
+ if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
+ content.replace_range(..3, "");
+ }
+
+ let stripped = location
+ .strip_prefix(&src_dir)
+ .expect("Chapters are always inside a book");
+
+ Chapter::new(&link.name, content, stripped, parent_names.clone())
+ } else {
+ Chapter::new_draft(&link.name, parent_names.clone())
+ };
+
+ let mut sub_item_parents = parent_names;
+
+ ch.number = link.number.clone();
+
+ sub_item_parents.push(link.name.clone());
+ let sub_items = link
+ .nested_items
+ .iter()
+ .map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
+ .collect::<Result<Vec<_>>>()?;
+
+ ch.sub_items = sub_items;
+
+ Ok(ch)
+}
+
+/// A depth-first iterator over the items in a book.
+///
+/// # Note
+///
+/// This struct shouldn't be created directly, instead prefer the
+/// [`Book::iter()`] method.
+pub struct BookItems<'a> {
+ items: VecDeque<&'a BookItem>,
+}
+
+impl<'a> Iterator for BookItems<'a> {
+ type Item = &'a BookItem;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let item = self.items.pop_front();
+
+ if let Some(&BookItem::Chapter(ref ch)) = item {
+ // if we wanted a breadth-first iterator we'd `extend()` here
+ for sub_item in ch.sub_items.iter().rev() {
+ self.items.push_front(sub_item);
+ }
+ }
+
+ item
+ }
+}
+
+impl Display for Chapter {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ if let Some(ref section_number) = self.number {
+ write!(f, "{} ", section_number)?;
+ }
+
+ write!(f, "{}", self.name)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::io::Write;
+ use tempfile::{Builder as TempFileBuilder, TempDir};
+
+ const DUMMY_SRC: &str = "
+# Dummy Chapter
+
+this is some dummy text.
+
+And here is some \
+ more text.
+";
+
+ /// Create a dummy `Link` in a temporary directory.
+ fn dummy_link() -> (Link, TempDir) {
+ let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
+
+ let chapter_path = temp.path().join("chapter_1.md");
+ File::create(&chapter_path)
+ .unwrap()
+ .write_all(DUMMY_SRC.as_bytes())
+ .unwrap();
+
+ let link = Link::new("Chapter 1", chapter_path);
+
+ (link, temp)
+ }
+
+ /// Create a nested `Link` written to a temporary directory.
+ fn nested_links() -> (Link, TempDir) {
+ let (mut root, temp_dir) = dummy_link();
+
+ let second_path = temp_dir.path().join("second.md");
+
+ File::create(&second_path)
+ .unwrap()
+ .write_all(b"Hello World!")
+ .unwrap();
+
+ let mut second = Link::new("Nested Chapter 1", &second_path);
+ second.number = Some(SectionNumber(vec![1, 2]));
+
+ root.nested_items.push(second.clone().into());
+ root.nested_items.push(SummaryItem::Separator);
+ root.nested_items.push(second.into());
+
+ (root, temp_dir)
+ }
+
+ #[test]
+ fn load_a_single_chapter_from_disk() {
+ let (link, temp_dir) = dummy_link();
+ let should_be = Chapter::new(
+ "Chapter 1",
+ DUMMY_SRC.to_string(),
+ "chapter_1.md",
+ Vec::new(),
+ );
+
+ let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn load_a_single_chapter_with_utf8_bom_from_disk() {
+ let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
+
+ let chapter_path = temp_dir.path().join("chapter_1.md");
+ File::create(&chapter_path)
+ .unwrap()
+ .write_all(("\u{feff}".to_owned() + DUMMY_SRC).as_bytes())
+ .unwrap();
+
+ let link = Link::new("Chapter 1", chapter_path);
+
+ let should_be = Chapter::new(
+ "Chapter 1",
+ DUMMY_SRC.to_string(),
+ "chapter_1.md",
+ Vec::new(),
+ );
+
+ let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn cant_load_a_nonexistent_chapter() {
+ let link = Link::new("Chapter 1", "/foo/bar/baz.md");
+
+ let got = load_chapter(&link, "", Vec::new());
+ assert!(got.is_err());
+ }
+
+ #[test]
+ fn load_recursive_link_with_separators() {
+ let (root, temp) = nested_links();
+
+ let nested = Chapter {
+ name: String::from("Nested Chapter 1"),
+ content: String::from("Hello World!"),
+ number: Some(SectionNumber(vec![1, 2])),
+ path: Some(PathBuf::from("second.md")),
+ source_path: Some(PathBuf::from("second.md")),
+ parent_names: vec![String::from("Chapter 1")],
+ sub_items: Vec::new(),
+ };
+ let should_be = BookItem::Chapter(Chapter {
+ name: String::from("Chapter 1"),
+ content: String::from(DUMMY_SRC),
+ number: None,
+ path: Some(PathBuf::from("chapter_1.md")),
+ source_path: Some(PathBuf::from("chapter_1.md")),
+ parent_names: Vec::new(),
+ sub_items: vec![
+ BookItem::Chapter(nested.clone()),
+ BookItem::Separator,
+ BookItem::Chapter(nested),
+ ],
+ });
+
+ let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn load_a_book_with_a_single_chapter() {
+ let (link, temp) = dummy_link();
+ let summary = Summary {
+ numbered_chapters: vec![SummaryItem::Link(link)],
+ ..Default::default()
+ };
+ let should_be = Book {
+ sections: vec![BookItem::Chapter(Chapter {
+ name: String::from("Chapter 1"),
+ content: String::from(DUMMY_SRC),
+ path: Some(PathBuf::from("chapter_1.md")),
+ source_path: Some(PathBuf::from("chapter_1.md")),
+ ..Default::default()
+ })],
+ ..Default::default()
+ };
+
+ let got = load_book_from_disk(&summary, temp.path()).unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn book_iter_iterates_over_sequential_items() {
+ let book = Book {
+ sections: vec![
+ BookItem::Chapter(Chapter {
+ name: String::from("Chapter 1"),
+ content: String::from(DUMMY_SRC),
+ ..Default::default()
+ }),
+ BookItem::Separator,
+ ],
+ ..Default::default()
+ };
+
+ let should_be: Vec<_> = book.sections.iter().collect();
+
+ let got: Vec<_> = book.iter().collect();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn iterate_over_nested_book_items() {
+ let book = Book {
+ sections: vec![
+ BookItem::Chapter(Chapter {
+ name: String::from("Chapter 1"),
+ content: String::from(DUMMY_SRC),
+ number: None,
+ path: Some(PathBuf::from("Chapter_1/index.md")),
+ source_path: Some(PathBuf::from("Chapter_1/index.md")),
+ parent_names: Vec::new(),
+ sub_items: vec![
+ BookItem::Chapter(Chapter::new(
+ "Hello World",
+ String::new(),
+ "Chapter_1/hello.md",
+ Vec::new(),
+ )),
+ BookItem::Separator,
+ BookItem::Chapter(Chapter::new(
+ "Goodbye World",
+ String::new(),
+ "Chapter_1/goodbye.md",
+ Vec::new(),
+ )),
+ ],
+ }),
+ BookItem::Separator,
+ ],
+ ..Default::default()
+ };
+
+ let got: Vec<_> = book.iter().collect();
+
+ assert_eq!(got.len(), 5);
+
+ // checking the chapter names are in the order should be sufficient here...
+ let chapter_names: Vec<String> = got
+ .into_iter()
+ .filter_map(|i| match *i {
+ BookItem::Chapter(ref ch) => Some(ch.name.clone()),
+ _ => None,
+ })
+ .collect();
+ let should_be: Vec<_> = vec![
+ String::from("Chapter 1"),
+ String::from("Hello World"),
+ String::from("Goodbye World"),
+ ];
+
+ assert_eq!(chapter_names, should_be);
+ }
+
+ #[test]
+ fn for_each_mut_visits_all_items() {
+ let mut book = Book {
+ sections: vec![
+ BookItem::Chapter(Chapter {
+ name: String::from("Chapter 1"),
+ content: String::from(DUMMY_SRC),
+ number: None,
+ path: Some(PathBuf::from("Chapter_1/index.md")),
+ source_path: Some(PathBuf::from("Chapter_1/index.md")),
+ parent_names: Vec::new(),
+ sub_items: vec![
+ BookItem::Chapter(Chapter::new(
+ "Hello World",
+ String::new(),
+ "Chapter_1/hello.md",
+ Vec::new(),
+ )),
+ BookItem::Separator,
+ BookItem::Chapter(Chapter::new(
+ "Goodbye World",
+ String::new(),
+ "Chapter_1/goodbye.md",
+ Vec::new(),
+ )),
+ ],
+ }),
+ BookItem::Separator,
+ ],
+ ..Default::default()
+ };
+
+ let num_items = book.iter().count();
+ let mut visited = 0;
+
+ book.for_each_mut(|_| visited += 1);
+
+ assert_eq!(visited, num_items);
+ }
+
+ #[test]
+ fn cant_load_chapters_with_an_empty_path() {
+ let (_, temp) = dummy_link();
+ let summary = Summary {
+ numbered_chapters: vec![SummaryItem::Link(Link {
+ name: String::from("Empty"),
+ location: Some(PathBuf::from("")),
+ ..Default::default()
+ })],
+
+ ..Default::default()
+ };
+
+ let got = load_book_from_disk(&summary, temp.path());
+ assert!(got.is_err());
+ }
+
+ #[test]
+ fn cant_load_chapters_when_the_link_is_a_directory() {
+ let (_, temp) = dummy_link();
+ let dir = temp.path().join("nested");
+ fs::create_dir(&dir).unwrap();
+
+ let summary = Summary {
+ numbered_chapters: vec![SummaryItem::Link(Link {
+ name: String::from("nested"),
+ location: Some(dir),
+ ..Default::default()
+ })],
+ ..Default::default()
+ };
+
+ let got = load_book_from_disk(&summary, temp.path());
+ assert!(got.is_err());
+ }
+}
diff --git a/vendor/mdbook/src/book/init.rs b/vendor/mdbook/src/book/init.rs
new file mode 100644
index 000000000..264c113d3
--- /dev/null
+++ b/vendor/mdbook/src/book/init.rs
@@ -0,0 +1,207 @@
+use std::fs::{self, File};
+use std::io::Write;
+use std::path::PathBuf;
+
+use super::MDBook;
+use crate::config::Config;
+use crate::errors::*;
+use crate::theme;
+
+/// A helper for setting up a new book and its directory structure.
+#[derive(Debug, Clone, PartialEq)]
+pub struct BookBuilder {
+ root: PathBuf,
+ create_gitignore: bool,
+ config: Config,
+ copy_theme: bool,
+}
+
+impl BookBuilder {
+ /// Create a new `BookBuilder` which will generate a book in the provided
+ /// root directory.
+ pub fn new<P: Into<PathBuf>>(root: P) -> BookBuilder {
+ BookBuilder {
+ root: root.into(),
+ create_gitignore: false,
+ config: Config::default(),
+ copy_theme: false,
+ }
+ }
+
+ /// Set the [`Config`] to be used.
+ pub fn with_config(&mut self, cfg: Config) -> &mut BookBuilder {
+ self.config = cfg;
+ self
+ }
+
+ /// Get the config used by the `BookBuilder`.
+ pub fn config(&self) -> &Config {
+ &self.config
+ }
+
+ /// Should the theme be copied into the generated book (so users can tweak
+ /// it)?
+ pub fn copy_theme(&mut self, copy: bool) -> &mut BookBuilder {
+ self.copy_theme = copy;
+ self
+ }
+
+ /// Should we create a `.gitignore` file?
+ pub fn create_gitignore(&mut self, create: bool) -> &mut BookBuilder {
+ self.create_gitignore = create;
+ self
+ }
+
+ /// Generate the actual book. This will:
+ ///
+ /// - Create the directory structure.
+ /// - Stub out some dummy chapters and the `SUMMARY.md`.
+ /// - Create a `.gitignore` (if applicable)
+ /// - Create a themes directory and populate it (if applicable)
+ /// - Generate a `book.toml` file,
+ /// - Then load the book so we can build it or run tests.
+ pub fn build(&self) -> Result<MDBook> {
+ info!("Creating a new book with stub content");
+
+ self.create_directory_structure()
+ .with_context(|| "Unable to create directory structure")?;
+
+ self.create_stub_files()
+ .with_context(|| "Unable to create stub files")?;
+
+ if self.create_gitignore {
+ self.build_gitignore()
+ .with_context(|| "Unable to create .gitignore")?;
+ }
+
+ if self.copy_theme {
+ self.copy_across_theme()
+ .with_context(|| "Unable to copy across the theme")?;
+ }
+
+ self.write_book_toml()?;
+
+ match MDBook::load(&self.root) {
+ Ok(book) => Ok(book),
+ Err(e) => {
+ error!("{}", e);
+
+ panic!(
+ "The BookBuilder should always create a valid book. If you are seeing this it \
+ is a bug and should be reported."
+ );
+ }
+ }
+ }
+
+ fn write_book_toml(&self) -> Result<()> {
+ debug!("Writing book.toml");
+ let book_toml = self.root.join("book.toml");
+ let cfg = toml::to_vec(&self.config).with_context(|| "Unable to serialize the config")?;
+
+ File::create(book_toml)
+ .with_context(|| "Couldn't create book.toml")?
+ .write_all(&cfg)
+ .with_context(|| "Unable to write config to book.toml")?;
+ Ok(())
+ }
+
+ fn copy_across_theme(&self) -> Result<()> {
+ debug!("Copying theme");
+
+ let html_config = self.config.html_config().unwrap_or_default();
+ let themedir = html_config.theme_dir(&self.root);
+
+ if !themedir.exists() {
+ debug!(
+ "{} does not exist, creating the directory",
+ themedir.display()
+ );
+ fs::create_dir(&themedir)?;
+ }
+
+ let mut index = File::create(themedir.join("index.hbs"))?;
+ index.write_all(theme::INDEX)?;
+
+ let cssdir = themedir.join("css");
+ if !cssdir.exists() {
+ fs::create_dir(&cssdir)?;
+ }
+
+ let mut general_css = File::create(cssdir.join("general.css"))?;
+ general_css.write_all(theme::GENERAL_CSS)?;
+
+ let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
+ chrome_css.write_all(theme::CHROME_CSS)?;
+
+ if html_config.print.enable {
+ let mut print_css = File::create(cssdir.join("print.css"))?;
+ print_css.write_all(theme::PRINT_CSS)?;
+ }
+
+ let mut variables_css = File::create(cssdir.join("variables.css"))?;
+ variables_css.write_all(theme::VARIABLES_CSS)?;
+
+ let mut favicon = File::create(themedir.join("favicon.png"))?;
+ favicon.write_all(theme::FAVICON_PNG)?;
+
+ let mut favicon = File::create(themedir.join("favicon.svg"))?;
+ favicon.write_all(theme::FAVICON_SVG)?;
+
+ let mut js = File::create(themedir.join("book.js"))?;
+ js.write_all(theme::JS)?;
+
+ let mut highlight_css = File::create(themedir.join("highlight.css"))?;
+ highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
+
+ let mut highlight_js = File::create(themedir.join("highlight.js"))?;
+ highlight_js.write_all(theme::HIGHLIGHT_JS)?;
+
+ Ok(())
+ }
+
+ fn build_gitignore(&self) -> Result<()> {
+ debug!("Creating .gitignore");
+
+ let mut f = File::create(self.root.join(".gitignore"))?;
+
+ writeln!(f, "{}", self.config.build.build_dir.display())?;
+
+ Ok(())
+ }
+
+ fn create_stub_files(&self) -> Result<()> {
+ debug!("Creating example book contents");
+ let src_dir = self.root.join(&self.config.book.src);
+
+ let summary = src_dir.join("SUMMARY.md");
+ if !summary.exists() {
+ trace!("No summary found creating stub summary and chapter_1.md.");
+ let mut f = File::create(&summary).with_context(|| "Unable to create SUMMARY.md")?;
+ writeln!(f, "# Summary")?;
+ writeln!(f)?;
+ writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
+
+ let chapter_1 = src_dir.join("chapter_1.md");
+ let mut f =
+ File::create(&chapter_1).with_context(|| "Unable to create chapter_1.md")?;
+ writeln!(f, "# Chapter 1")?;
+ } else {
+ trace!("Existing summary found, no need to create stub files.");
+ }
+ Ok(())
+ }
+
+ fn create_directory_structure(&self) -> Result<()> {
+ debug!("Creating directory tree");
+ fs::create_dir_all(&self.root)?;
+
+ let src = self.root.join(&self.config.book.src);
+ fs::create_dir_all(&src)?;
+
+ let build = self.root.join(&self.config.build.build_dir);
+ fs::create_dir_all(&build)?;
+
+ Ok(())
+ }
+}
diff --git a/vendor/mdbook/src/book/mod.rs b/vendor/mdbook/src/book/mod.rs
new file mode 100644
index 000000000..9745d2b7e
--- /dev/null
+++ b/vendor/mdbook/src/book/mod.rs
@@ -0,0 +1,836 @@
+//! The internal representation of a book and infrastructure for loading it from
+//! disk and building it.
+//!
+//! For examples on using `MDBook`, consult the [top-level documentation][1].
+//!
+//! [1]: ../index.html
+
+#[allow(clippy::module_inception)]
+mod book;
+mod init;
+mod summary;
+
+pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
+pub use self::init::BookBuilder;
+pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
+
+use std::io::Write;
+use std::path::PathBuf;
+use std::process::Command;
+use std::string::ToString;
+use tempfile::Builder as TempFileBuilder;
+use toml::Value;
+use topological_sort::TopologicalSort;
+
+use crate::errors::*;
+use crate::preprocess::{
+ CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
+};
+use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
+use crate::utils;
+
+use crate::config::{Config, RustEdition};
+
+/// The object used to manage and build a book.
+pub struct MDBook {
+ /// The book's root directory.
+ pub root: PathBuf,
+ /// The configuration used to tweak now a book is built.
+ pub config: Config,
+ /// A representation of the book's contents in memory.
+ pub book: Book,
+ renderers: Vec<Box<dyn Renderer>>,
+
+ /// List of pre-processors to be run on the book.
+ preprocessors: Vec<Box<dyn Preprocessor>>,
+}
+
+impl MDBook {
+ /// Load a book from its root directory on disk.
+ pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
+ let book_root = book_root.into();
+ let config_location = book_root.join("book.toml");
+
+ // the book.json file is no longer used, so we should emit a warning to
+ // let people know to migrate to book.toml
+ if book_root.join("book.json").exists() {
+ warn!("It appears you are still using book.json for configuration.");
+ warn!("This format is no longer used, so you should migrate to the");
+ warn!("book.toml format.");
+ warn!("Check the user guide for migration information:");
+ warn!("\thttps://rust-lang.github.io/mdBook/format/config.html");
+ }
+
+ let mut config = if config_location.exists() {
+ debug!("Loading config from {}", config_location.display());
+ Config::from_disk(&config_location)?
+ } else {
+ Config::default()
+ };
+
+ config.update_from_env();
+
+ if config
+ .html_config()
+ .map_or(false, |html| html.google_analytics.is_some())
+ {
+ warn!(
+ "The output.html.google-analytics field has been deprecated; \
+ it will be removed in a future release.\n\
+ Consider placing the appropriate site tag code into the \
+ theme/head.hbs file instead.\n\
+ The tracking code may be found in the Google Analytics Admin page.\n\
+ "
+ );
+ }
+
+ if log_enabled!(log::Level::Trace) {
+ for line in format!("Config: {:#?}", config).lines() {
+ trace!("{}", line);
+ }
+ }
+
+ MDBook::load_with_config(book_root, config)
+ }
+
+ /// Load a book from its root directory using a custom `Config`.
+ pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
+ let root = book_root.into();
+
+ let src_dir = root.join(&config.book.src);
+ let book = book::load_book(&src_dir, &config.build)?;
+
+ let renderers = determine_renderers(&config);
+ let preprocessors = determine_preprocessors(&config)?;
+
+ Ok(MDBook {
+ root,
+ config,
+ book,
+ renderers,
+ preprocessors,
+ })
+ }
+
+ /// Load a book from its root directory using a custom `Config` and a custom summary.
+ pub fn load_with_config_and_summary<P: Into<PathBuf>>(
+ book_root: P,
+ config: Config,
+ summary: Summary,
+ ) -> Result<MDBook> {
+ let root = book_root.into();
+
+ let src_dir = root.join(&config.book.src);
+ let book = book::load_book_from_disk(&summary, &src_dir)?;
+
+ let renderers = determine_renderers(&config);
+ let preprocessors = determine_preprocessors(&config)?;
+
+ Ok(MDBook {
+ root,
+ config,
+ book,
+ renderers,
+ preprocessors,
+ })
+ }
+
+ /// Returns a flat depth-first iterator over the elements of the book,
+ /// it returns a [`BookItem`] enum:
+ /// `(section: String, bookitem: &BookItem)`
+ ///
+ /// ```no_run
+ /// # use mdbook::MDBook;
+ /// # use mdbook::book::BookItem;
+ /// # let book = MDBook::load("mybook").unwrap();
+ /// for item in book.iter() {
+ /// match *item {
+ /// BookItem::Chapter(ref chapter) => {},
+ /// BookItem::Separator => {},
+ /// BookItem::PartTitle(ref title) => {}
+ /// }
+ /// }
+ ///
+ /// // would print something like this:
+ /// // 1. Chapter 1
+ /// // 1.1 Sub Chapter
+ /// // 1.2 Sub Chapter
+ /// // 2. Chapter 2
+ /// //
+ /// // etc.
+ /// ```
+ pub fn iter(&self) -> BookItems<'_> {
+ self.book.iter()
+ }
+
+ /// `init()` gives you a `BookBuilder` which you can use to setup a new book
+ /// and its accompanying directory structure.
+ ///
+ /// The `BookBuilder` creates some boilerplate files and directories to get
+ /// you started with your book.
+ ///
+ /// ```text
+ /// book-test/
+ /// ├── book
+ /// └── src
+ /// ├── chapter_1.md
+ /// └── SUMMARY.md
+ /// ```
+ ///
+ /// It uses the path provided as the root directory for your book, then adds
+ /// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
+ /// to get you started.
+ pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
+ BookBuilder::new(book_root)
+ }
+
+ /// Tells the renderer to build our book and put it in the build directory.
+ pub fn build(&self) -> Result<()> {
+ info!("Book building has started");
+
+ for renderer in &self.renderers {
+ self.execute_build_process(&**renderer)?;
+ }
+
+ Ok(())
+ }
+
+ /// Run the entire build process for a particular [`Renderer`].
+ pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
+ let mut preprocessed_book = self.book.clone();
+ let preprocess_ctx = PreprocessorContext::new(
+ self.root.clone(),
+ self.config.clone(),
+ renderer.name().to_string(),
+ );
+
+ for preprocessor in &self.preprocessors {
+ if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
+ debug!("Running the {} preprocessor.", preprocessor.name());
+ preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
+ }
+ }
+
+ let name = renderer.name();
+ let build_dir = self.build_dir_for(name);
+
+ let mut render_context = RenderContext::new(
+ self.root.clone(),
+ preprocessed_book,
+ self.config.clone(),
+ build_dir,
+ );
+ render_context
+ .chapter_titles
+ .extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
+
+ info!("Running the {} backend", renderer.name());
+ renderer
+ .render(&render_context)
+ .with_context(|| "Rendering failed")
+ }
+
+ /// You can change the default renderer to another one by using this method.
+ /// The only requirement is that your renderer implement the [`Renderer`]
+ /// trait.
+ pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
+ self.renderers.push(Box::new(renderer));
+ self
+ }
+
+ /// Register a [`Preprocessor`] to be used when rendering the book.
+ pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
+ self.preprocessors.push(Box::new(preprocessor));
+ self
+ }
+
+ /// Run `rustdoc` tests on the book, linking against the provided libraries.
+ pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
+ let library_args: Vec<&str> = (0..library_paths.len())
+ .map(|_| "-L")
+ .zip(library_paths.into_iter())
+ .flat_map(|x| vec![x.0, x.1])
+ .collect();
+
+ let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
+
+ // FIXME: Is "test" the proper renderer name to use here?
+ let preprocess_context =
+ PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
+
+ let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
+ // Index Preprocessor is disabled so that chapter paths continue to point to the
+ // actual markdown files.
+
+ let mut failed = false;
+ for item in book.iter() {
+ if let BookItem::Chapter(ref ch) = *item {
+ let chapter_path = match ch.path {
+ Some(ref path) if !path.as_os_str().is_empty() => path,
+ _ => continue,
+ };
+
+ let path = self.source_dir().join(&chapter_path);
+ info!("Testing file: {:?}", path);
+
+ // write preprocessed file to tempdir
+ let path = temp_dir.path().join(&chapter_path);
+ let mut tmpf = utils::fs::create_file(&path)?;
+ tmpf.write_all(ch.content.as_bytes())?;
+
+ let mut cmd = Command::new("rustdoc");
+ cmd.arg(&path).arg("--test").args(&library_args);
+
+ if let Some(edition) = self.config.rust.edition {
+ match edition {
+ RustEdition::E2015 => {
+ cmd.args(&["--edition", "2015"]);
+ }
+ RustEdition::E2018 => {
+ cmd.args(&["--edition", "2018"]);
+ }
+ RustEdition::E2021 => {
+ cmd.args(&["--edition", "2021"]);
+ }
+ }
+ }
+
+ let output = cmd.output()?;
+
+ if !output.status.success() {
+ failed = true;
+ error!(
+ "rustdoc returned an error:\n\
+ \n--- stdout\n{}\n--- stderr\n{}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+ }
+ }
+ if failed {
+ bail!("One or more tests failed");
+ }
+ Ok(())
+ }
+
+ /// The logic for determining where a backend should put its build
+ /// artefacts.
+ ///
+ /// If there is only 1 renderer, put it in the directory pointed to by the
+ /// `build.build_dir` key in [`Config`]. If there is more than one then the
+ /// renderer gets its own directory within the main build dir.
+ ///
+ /// i.e. If there were only one renderer (in this case, the HTML renderer):
+ ///
+ /// - build/
+ /// - index.html
+ /// - ...
+ ///
+ /// Otherwise if there are multiple:
+ ///
+ /// - build/
+ /// - epub/
+ /// - my_awesome_book.epub
+ /// - html/
+ /// - index.html
+ /// - ...
+ /// - latex/
+ /// - my_awesome_book.tex
+ ///
+ pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
+ let build_dir = self.root.join(&self.config.build.build_dir);
+
+ if self.renderers.len() <= 1 {
+ build_dir
+ } else {
+ build_dir.join(backend_name)
+ }
+ }
+
+ /// Get the directory containing this book's source files.
+ pub fn source_dir(&self) -> PathBuf {
+ self.root.join(&self.config.book.src)
+ }
+
+ /// Get the directory containing the theme resources for the book.
+ pub fn theme_dir(&self) -> PathBuf {
+ self.config
+ .html_config()
+ .unwrap_or_default()
+ .theme_dir(&self.root)
+ }
+}
+
+/// Look at the `Config` and try to figure out what renderers to use.
+fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
+ let mut renderers = Vec::new();
+
+ if let Some(output_table) = config.get("output").and_then(Value::as_table) {
+ renderers.extend(output_table.iter().map(|(key, table)| {
+ if key == "html" {
+ Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
+ } else if key == "markdown" {
+ Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
+ } else {
+ interpret_custom_renderer(key, table)
+ }
+ }));
+ }
+
+ // if we couldn't find anything, add the HTML renderer as a default
+ if renderers.is_empty() {
+ renderers.push(Box::new(HtmlHandlebars::new()));
+ }
+
+ renderers
+}
+
+const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
+
+fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
+ let name = pre.name();
+ name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
+}
+
+/// Look at the `MDBook` and try to figure out what preprocessors to run.
+fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
+ // Collect the names of all preprocessors intended to be run, and the order
+ // in which they should be run.
+ let mut preprocessor_names = TopologicalSort::<String>::new();
+
+ if config.build.use_default_preprocessors {
+ for name in DEFAULT_PREPROCESSORS {
+ preprocessor_names.insert(name.to_string());
+ }
+ }
+
+ if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
+ for (name, table) in preprocessor_table.iter() {
+ preprocessor_names.insert(name.to_string());
+
+ let exists = |name| {
+ (config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
+ || preprocessor_table.contains_key(name)
+ };
+
+ if let Some(before) = table.get("before") {
+ let before = before.as_array().ok_or_else(|| {
+ Error::msg(format!(
+ "Expected preprocessor.{}.before to be an array",
+ name
+ ))
+ })?;
+ for after in before {
+ let after = after.as_str().ok_or_else(|| {
+ Error::msg(format!(
+ "Expected preprocessor.{}.before to contain strings",
+ name
+ ))
+ })?;
+
+ if !exists(after) {
+ // Only warn so that preprocessors can be toggled on and off (e.g. for
+ // troubleshooting) without having to worry about order too much.
+ warn!(
+ "preprocessor.{}.after contains \"{}\", which was not found",
+ name, after
+ );
+ } else {
+ preprocessor_names.add_dependency(name, after);
+ }
+ }
+ }
+
+ if let Some(after) = table.get("after") {
+ let after = after.as_array().ok_or_else(|| {
+ Error::msg(format!(
+ "Expected preprocessor.{}.after to be an array",
+ name
+ ))
+ })?;
+ for before in after {
+ let before = before.as_str().ok_or_else(|| {
+ Error::msg(format!(
+ "Expected preprocessor.{}.after to contain strings",
+ name
+ ))
+ })?;
+
+ if !exists(before) {
+ // See equivalent warning above for rationale
+ warn!(
+ "preprocessor.{}.before contains \"{}\", which was not found",
+ name, before
+ );
+ } else {
+ preprocessor_names.add_dependency(before, name);
+ }
+ }
+ }
+ }
+ }
+
+ // Now that all links have been established, queue preprocessors in a suitable order
+ let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
+ // `pop_all()` returns an empty vector when no more items are not being depended upon
+ for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
+ .take_while(|names| !names.is_empty())
+ {
+ // The `topological_sort` crate does not guarantee a stable order for ties, even across
+ // runs of the same program. Thus, we break ties manually by sorting.
+ // Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
+ // values ([1]), which may not be an alphabetical sort.
+ // As mentioned in [1], doing so depends on locale, which is not desirable for deciding
+ // preprocessor execution order.
+ // [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
+ names.sort();
+ for name in names {
+ let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
+ "links" => Box::new(LinkPreprocessor::new()),
+ "index" => Box::new(IndexPreprocessor::new()),
+ _ => {
+ // The only way to request a custom preprocessor is through the `preprocessor`
+ // table, so it must exist, be a table, and contain the key.
+ let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
+ let command = get_custom_preprocessor_cmd(&name, table);
+ Box::new(CmdPreprocessor::new(name, command))
+ }
+ };
+ preprocessors.push(preprocessor);
+ }
+ }
+
+ // "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
+ // Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
+ if preprocessor_names.is_empty() {
+ Ok(preprocessors)
+ } else {
+ Err(Error::msg("Cyclic dependency detected in preprocessors"))
+ }
+}
+
+fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
+ table
+ .get("command")
+ .and_then(Value::as_str)
+ .map(ToString::to_string)
+ .unwrap_or_else(|| format!("mdbook-{}", key))
+}
+
+fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
+ // look for the `command` field, falling back to using the key
+ // prepended by "mdbook-"
+ let table_dot_command = table
+ .get("command")
+ .and_then(Value::as_str)
+ .map(ToString::to_string);
+
+ let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
+
+ Box::new(CmdRenderer::new(key.to_string(), command))
+}
+
+/// Check whether we should run a particular `Preprocessor` in combination
+/// with the renderer, falling back to `Preprocessor::supports_renderer()`
+/// method if the user doesn't say anything.
+///
+/// The `build.use-default-preprocessors` config option can be used to ensure
+/// default preprocessors always run if they support the renderer.
+fn preprocessor_should_run(
+ preprocessor: &dyn Preprocessor,
+ renderer: &dyn Renderer,
+ cfg: &Config,
+) -> bool {
+ // default preprocessors should be run by default (if supported)
+ if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
+ return preprocessor.supports_renderer(renderer.name());
+ }
+
+ let key = format!("preprocessor.{}.renderers", preprocessor.name());
+ let renderer_name = renderer.name();
+
+ if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
+ return explicit_renderers
+ .iter()
+ .filter_map(Value::as_str)
+ .any(|name| name == renderer_name);
+ }
+
+ preprocessor.supports_renderer(renderer_name)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::str::FromStr;
+ use toml::value::{Table, Value};
+
+ #[test]
+ fn config_defaults_to_html_renderer_if_empty() {
+ let cfg = Config::default();
+
+ // make sure we haven't got anything in the `output` table
+ assert!(cfg.get("output").is_none());
+
+ let got = determine_renderers(&cfg);
+
+ assert_eq!(got.len(), 1);
+ assert_eq!(got[0].name(), "html");
+ }
+
+ #[test]
+ fn add_a_random_renderer_to_the_config() {
+ let mut cfg = Config::default();
+ cfg.set("output.random", Table::new()).unwrap();
+
+ let got = determine_renderers(&cfg);
+
+ assert_eq!(got.len(), 1);
+ assert_eq!(got[0].name(), "random");
+ }
+
+ #[test]
+ fn add_a_random_renderer_with_custom_command_to_the_config() {
+ let mut cfg = Config::default();
+
+ let mut table = Table::new();
+ table.insert("command".to_string(), Value::String("false".to_string()));
+ cfg.set("output.random", table).unwrap();
+
+ let got = determine_renderers(&cfg);
+
+ assert_eq!(got.len(), 1);
+ assert_eq!(got[0].name(), "random");
+ }
+
+ #[test]
+ fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
+ let cfg = Config::default();
+
+ // make sure we haven't got anything in the `preprocessor` table
+ assert!(cfg.get("preprocessor").is_none());
+
+ let got = determine_preprocessors(&cfg);
+
+ assert!(got.is_ok());
+ assert_eq!(got.as_ref().unwrap().len(), 2);
+ assert_eq!(got.as_ref().unwrap()[0].name(), "index");
+ assert_eq!(got.as_ref().unwrap()[1].name(), "links");
+ }
+
+ #[test]
+ fn use_default_preprocessors_works() {
+ let mut cfg = Config::default();
+ cfg.build.use_default_preprocessors = false;
+
+ let got = determine_preprocessors(&cfg).unwrap();
+
+ assert_eq!(got.len(), 0);
+ }
+
+ #[test]
+ fn can_determine_third_party_preprocessors() {
+ let cfg_str = r#"
+ [book]
+ title = "Some Book"
+
+ [preprocessor.random]
+
+ [build]
+ build-dir = "outputs"
+ create-missing = false
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ // make sure the `preprocessor.random` table exists
+ assert!(cfg.get_preprocessor("random").is_some());
+
+ let got = determine_preprocessors(&cfg).unwrap();
+
+ assert!(got.into_iter().any(|p| p.name() == "random"));
+ }
+
+ #[test]
+ fn preprocessors_can_provide_their_own_commands() {
+ let cfg_str = r#"
+ [preprocessor.random]
+ command = "python random.py"
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ // make sure the `preprocessor.random` table exists
+ let random = cfg.get_preprocessor("random").unwrap();
+ let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
+
+ assert_eq!(random, "python random.py");
+ }
+
+ #[test]
+ fn preprocessor_before_must_be_array() {
+ let cfg_str = r#"
+ [preprocessor.random]
+ before = 0
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ assert!(determine_preprocessors(&cfg).is_err());
+ }
+
+ #[test]
+ fn preprocessor_after_must_be_array() {
+ let cfg_str = r#"
+ [preprocessor.random]
+ after = 0
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ assert!(determine_preprocessors(&cfg).is_err());
+ }
+
+ #[test]
+ fn preprocessor_order_is_honored() {
+ let cfg_str = r#"
+ [preprocessor.random]
+ before = [ "last" ]
+ after = [ "index" ]
+
+ [preprocessor.last]
+ after = [ "links", "index" ]
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ let preprocessors = determine_preprocessors(&cfg).unwrap();
+ let index = |name| {
+ preprocessors
+ .iter()
+ .enumerate()
+ .find(|(_, preprocessor)| preprocessor.name() == name)
+ .unwrap()
+ .0
+ };
+ let assert_before = |before, after| {
+ if index(before) >= index(after) {
+ eprintln!("Preprocessor order:");
+ for preprocessor in &preprocessors {
+ eprintln!(" {}", preprocessor.name());
+ }
+ panic!("{} should come before {}", before, after);
+ }
+ };
+
+ assert_before("index", "random");
+ assert_before("index", "last");
+ assert_before("random", "last");
+ assert_before("links", "last");
+ }
+
+ #[test]
+ fn cyclic_dependencies_are_detected() {
+ let cfg_str = r#"
+ [preprocessor.links]
+ before = [ "index" ]
+
+ [preprocessor.index]
+ before = [ "links" ]
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ assert!(determine_preprocessors(&cfg).is_err());
+ }
+
+ #[test]
+ fn dependencies_dont_register_undefined_preprocessors() {
+ let cfg_str = r#"
+ [preprocessor.links]
+ before = [ "random" ]
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ let preprocessors = determine_preprocessors(&cfg).unwrap();
+
+ assert!(!preprocessors
+ .iter()
+ .any(|preprocessor| preprocessor.name() == "random"));
+ }
+
+ #[test]
+ fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
+ let cfg_str = r#"
+ [preprocessor.random]
+ before = [ "links" ]
+
+ [build]
+ use-default-preprocessors = false
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ let preprocessors = determine_preprocessors(&cfg).unwrap();
+
+ assert!(!preprocessors
+ .iter()
+ .any(|preprocessor| preprocessor.name() == "links"));
+ }
+
+ #[test]
+ fn config_respects_preprocessor_selection() {
+ let cfg_str = r#"
+ [preprocessor.links]
+ renderers = ["html"]
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ // double-check that we can access preprocessor.links.renderers[0]
+ let html = cfg
+ .get_preprocessor("links")
+ .and_then(|links| links.get("renderers"))
+ .and_then(Value::as_array)
+ .and_then(|renderers| renderers.get(0))
+ .and_then(Value::as_str)
+ .unwrap();
+ assert_eq!(html, "html");
+ let html_renderer = HtmlHandlebars::default();
+ let pre = LinkPreprocessor::new();
+
+ let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
+ assert!(should_run);
+ }
+
+ struct BoolPreprocessor(bool);
+ impl Preprocessor for BoolPreprocessor {
+ fn name(&self) -> &str {
+ "bool-preprocessor"
+ }
+
+ fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
+ unimplemented!()
+ }
+
+ fn supports_renderer(&self, _renderer: &str) -> bool {
+ self.0
+ }
+ }
+
+ #[test]
+ fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
+ let cfg = Config::default();
+ let html = HtmlHandlebars::new();
+
+ let should_be = true;
+ let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
+ assert_eq!(got, should_be);
+
+ let should_be = false;
+ let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
+ assert_eq!(got, should_be);
+ }
+}
diff --git a/vendor/mdbook/src/book/summary.rs b/vendor/mdbook/src/book/summary.rs
new file mode 100644
index 000000000..2bd81580f
--- /dev/null
+++ b/vendor/mdbook/src/book/summary.rs
@@ -0,0 +1,1097 @@
+use crate::errors::*;
+use memchr::{self, Memchr};
+use pulldown_cmark::{self, Event, HeadingLevel, Tag};
+use serde::{Deserialize, Serialize};
+use std::fmt::{self, Display, Formatter};
+use std::iter::FromIterator;
+use std::ops::{Deref, DerefMut};
+use std::path::{Path, PathBuf};
+
+/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
+/// used when loading a book from disk.
+///
+/// # Summary Format
+///
+/// **Title:** It's common practice to begin with a title, generally
+/// "# Summary". It's not mandatory and the parser (currently) ignores it, so
+/// you can too if you feel like it.
+///
+/// **Prefix Chapter:** Before the main numbered chapters you can add a couple
+/// of elements that will not be numbered. This is useful for forewords,
+/// introductions, etc. There are however some constraints. You can not nest
+/// prefix chapters, they should all be on the root level. And you can not add
+/// prefix chapters once you have added numbered chapters.
+///
+/// ```markdown
+/// [Title of prefix element](relative/path/to/markdown.md)
+/// ```
+///
+/// **Part Title:** An optional title for the next collect of numbered chapters. The numbered
+/// chapters can be broken into as many parts as desired.
+///
+/// **Numbered Chapter:** Numbered chapters are the main content of the book,
+/// they
+/// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
+/// sub-chapters, etc.)
+///
+/// ```markdown
+/// # Title of Part
+///
+/// - [Title of the Chapter](relative/path/to/markdown.md)
+/// ```
+///
+/// You can either use - or * to indicate a numbered chapter, the parser doesn't
+/// care but you'll probably want to stay consistent.
+///
+/// **Suffix Chapter:** After the numbered chapters you can add a couple of
+/// non-numbered chapters. They are the same as prefix chapters but come after
+/// the numbered chapters instead of before.
+///
+/// All other elements are unsupported and will be ignored at best or result in
+/// an error.
+pub fn parse_summary(summary: &str) -> Result<Summary> {
+ let parser = SummaryParser::new(summary);
+ parser.parse()
+}
+
+/// The parsed `SUMMARY.md`, specifying how the book should be laid out.
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
+pub struct Summary {
+ /// An optional title for the `SUMMARY.md`, currently just ignored.
+ pub title: Option<String>,
+ /// Chapters before the main text (e.g. an introduction).
+ pub prefix_chapters: Vec<SummaryItem>,
+ /// The main numbered chapters of the book, broken into one or more possibly named parts.
+ pub numbered_chapters: Vec<SummaryItem>,
+ /// Items which come after the main document (e.g. a conclusion).
+ pub suffix_chapters: Vec<SummaryItem>,
+}
+
+/// A struct representing an entry in the `SUMMARY.md`, possibly with nested
+/// entries.
+///
+/// This is roughly the equivalent of `[Some section](./path/to/file.md)`.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct Link {
+ /// The name of the chapter.
+ pub name: String,
+ /// The location of the chapter's source file, taking the book's `src`
+ /// directory as the root.
+ pub location: Option<PathBuf>,
+ /// The section number, if this chapter is in the numbered section.
+ pub number: Option<SectionNumber>,
+ /// Any nested items this chapter may contain.
+ pub nested_items: Vec<SummaryItem>,
+}
+
+impl Link {
+ /// Create a new link with no nested items.
+ pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
+ Link {
+ name: name.into(),
+ location: Some(location.as_ref().to_path_buf()),
+ number: None,
+ nested_items: Vec::new(),
+ }
+ }
+}
+
+impl Default for Link {
+ fn default() -> Self {
+ Link {
+ name: String::new(),
+ location: Some(PathBuf::new()),
+ number: None,
+ nested_items: Vec::new(),
+ }
+ }
+}
+
+/// An item in `SUMMARY.md` which could be either a separator or a `Link`.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub enum SummaryItem {
+ /// A link to a chapter.
+ Link(Link),
+ /// A separator (`---`).
+ Separator,
+ /// A part title.
+ PartTitle(String),
+}
+
+impl SummaryItem {
+ fn maybe_link_mut(&mut self) -> Option<&mut Link> {
+ match *self {
+ SummaryItem::Link(ref mut l) => Some(l),
+ _ => None,
+ }
+ }
+}
+
+impl From<Link> for SummaryItem {
+ fn from(other: Link) -> SummaryItem {
+ SummaryItem::Link(other)
+ }
+}
+
+/// A recursive descent (-ish) parser for a `SUMMARY.md`.
+///
+///
+/// # Grammar
+///
+/// The `SUMMARY.md` file has a grammar which looks something like this:
+///
+/// ```text
+/// summary ::= title prefix_chapters numbered_chapters
+/// suffix_chapters
+/// title ::= "# " TEXT
+/// | EPSILON
+/// prefix_chapters ::= item*
+/// suffix_chapters ::= item*
+/// numbered_chapters ::= part+
+/// part ::= title dotted_item+
+/// dotted_item ::= INDENT* DOT_POINT item
+/// item ::= link
+/// | separator
+/// separator ::= "---"
+/// link ::= "[" TEXT "]" "(" TEXT ")"
+/// DOT_POINT ::= "-"
+/// | "*"
+/// ```
+///
+/// > **Note:** the `TEXT` terminal is "normal" text, and should (roughly)
+/// > match the following regex: "[^<>\n[]]+".
+struct SummaryParser<'a> {
+ src: &'a str,
+ stream: pulldown_cmark::OffsetIter<'a, 'a>,
+ offset: usize,
+
+ /// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
+ /// here until somebody calls `next_event` again.
+ back: Option<Event<'a>>,
+}
+
+/// Reads `Events` from the provided stream until the corresponding
+/// `Event::End` is encountered which matches the `$delimiter` pattern.
+///
+/// This is the equivalent of doing
+/// `$stream.take_while(|e| e != $delimiter).collect()` but it allows you to
+/// use pattern matching and you won't get errors because `take_while()`
+/// moves `$stream` out of self.
+macro_rules! collect_events {
+ ($stream:expr,start $delimiter:pat) => {
+ collect_events!($stream, Event::Start($delimiter))
+ };
+ ($stream:expr,end $delimiter:pat) => {
+ collect_events!($stream, Event::End($delimiter))
+ };
+ ($stream:expr, $delimiter:pat) => {{
+ let mut events = Vec::new();
+
+ loop {
+ let event = $stream.next().map(|(ev, _range)| ev);
+ trace!("Next event: {:?}", event);
+
+ match event {
+ Some($delimiter) => break,
+ Some(other) => events.push(other),
+ None => {
+ debug!(
+ "Reached end of stream without finding the closing pattern, {}",
+ stringify!($delimiter)
+ );
+ break;
+ }
+ }
+ }
+
+ events
+ }};
+}
+
+impl<'a> SummaryParser<'a> {
+ fn new(text: &str) -> SummaryParser<'_> {
+ let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
+
+ SummaryParser {
+ src: text,
+ stream: pulldown_parser,
+ offset: 0,
+ back: None,
+ }
+ }
+
+ /// Get the current line and column to give the user more useful error
+ /// messages.
+ fn current_location(&self) -> (usize, usize) {
+ let previous_text = self.src[..self.offset].as_bytes();
+ let line = Memchr::new(b'\n', previous_text).count() + 1;
+ let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
+ let col = self.src[start_of_line..self.offset].chars().count();
+
+ (line, col)
+ }
+
+ /// Parse the text the `SummaryParser` was created with.
+ fn parse(mut self) -> Result<Summary> {
+ let title = self.parse_title();
+
+ let prefix_chapters = self
+ .parse_affix(true)
+ .with_context(|| "There was an error parsing the prefix chapters")?;
+ let numbered_chapters = self
+ .parse_parts()
+ .with_context(|| "There was an error parsing the numbered chapters")?;
+ let suffix_chapters = self
+ .parse_affix(false)
+ .with_context(|| "There was an error parsing the suffix chapters")?;
+
+ Ok(Summary {
+ title,
+ prefix_chapters,
+ numbered_chapters,
+ suffix_chapters,
+ })
+ }
+
+ /// Parse the affix chapters.
+ fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
+ let mut items = Vec::new();
+ debug!(
+ "Parsing {} items",
+ if is_prefix { "prefix" } else { "suffix" }
+ );
+
+ loop {
+ match self.next_event() {
+ Some(ev @ Event::Start(Tag::List(..)))
+ | Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
+ if is_prefix {
+ // we've finished prefix chapters and are at the start
+ // of the numbered section.
+ self.back(ev);
+ break;
+ } else {
+ bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
+ }
+ }
+ Some(Event::Start(Tag::Link(_type, href, _title))) => {
+ let link = self.parse_link(href.to_string());
+ items.push(SummaryItem::Link(link));
+ }
+ Some(Event::Rule) => items.push(SummaryItem::Separator),
+ Some(_) => {}
+ None => break,
+ }
+ }
+
+ Ok(items)
+ }
+
+ fn parse_parts(&mut self) -> Result<Vec<SummaryItem>> {
+ let mut parts = vec![];
+
+ // We want the section numbers to be continues through all parts.
+ let mut root_number = SectionNumber::default();
+ let mut root_items = 0;
+
+ loop {
+ // Possibly match a title or the end of the "numbered chapters part".
+ let title = match self.next_event() {
+ Some(ev @ Event::Start(Tag::Paragraph)) => {
+ // we're starting the suffix chapters
+ self.back(ev);
+ break;
+ }
+
+ Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
+ debug!("Found a h1 in the SUMMARY");
+
+ let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
+ Some(stringify_events(tags))
+ }
+
+ Some(ev) => {
+ self.back(ev);
+ None
+ }
+
+ None => break, // EOF, bail...
+ };
+
+ // Parse the rest of the part.
+ let numbered_chapters = self
+ .parse_numbered(&mut root_items, &mut root_number)
+ .with_context(|| "There was an error parsing the numbered chapters")?;
+
+ if let Some(title) = title {
+ parts.push(SummaryItem::PartTitle(title));
+ }
+ parts.extend(numbered_chapters);
+ }
+
+ Ok(parts)
+ }
+
+ /// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened.
+ fn parse_link(&mut self, href: String) -> Link {
+ let href = href.replace("%20", " ");
+ let link_content = collect_events!(self.stream, end Tag::Link(..));
+ let name = stringify_events(link_content);
+
+ let path = if href.is_empty() {
+ None
+ } else {
+ Some(PathBuf::from(href))
+ };
+
+ Link {
+ name,
+ location: path,
+ number: None,
+ nested_items: Vec::new(),
+ }
+ }
+
+ /// Parse the numbered chapters.
+ fn parse_numbered(
+ &mut self,
+ root_items: &mut u32,
+ root_number: &mut SectionNumber,
+ ) -> Result<Vec<SummaryItem>> {
+ let mut items = Vec::new();
+
+ // For the first iteration, we want to just skip any opening paragraph tags, as that just
+ // marks the start of the list. But after that, another opening paragraph indicates that we
+ // have started a new part or the suffix chapters.
+ let mut first = true;
+
+ loop {
+ match self.next_event() {
+ Some(ev @ Event::Start(Tag::Paragraph)) => {
+ if !first {
+ // we're starting the suffix chapters
+ self.back(ev);
+ break;
+ }
+ }
+ // The expectation is that pulldown cmark will terminate a paragraph before a new
+ // heading, so we can always count on this to return without skipping headings.
+ Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
+ // we're starting a new part
+ self.back(ev);
+ break;
+ }
+ Some(ev @ Event::Start(Tag::List(..))) => {
+ self.back(ev);
+ let mut bunch_of_items = self.parse_nested_numbered(root_number)?;
+
+ // if we've resumed after something like a rule the root sections
+ // will be numbered from 1. We need to manually go back and update
+ // them
+ update_section_numbers(&mut bunch_of_items, 0, *root_items);
+ *root_items += bunch_of_items.len() as u32;
+ items.extend(bunch_of_items);
+ }
+ Some(Event::Start(other_tag)) => {
+ trace!("Skipping contents of {:?}", other_tag);
+
+ // Skip over the contents of this tag
+ while let Some(event) = self.next_event() {
+ if event == Event::End(other_tag.clone()) {
+ break;
+ }
+ }
+ }
+ Some(Event::Rule) => {
+ items.push(SummaryItem::Separator);
+ }
+
+ // something else... ignore
+ Some(_) => {}
+
+ // EOF, bail...
+ None => {
+ break;
+ }
+ }
+
+ // From now on, we cannot accept any new paragraph opening tags.
+ first = false;
+ }
+
+ Ok(items)
+ }
+
+ /// Push an event back to the tail of the stream.
+ fn back(&mut self, ev: Event<'a>) {
+ assert!(self.back.is_none());
+ trace!("Back: {:?}", ev);
+ self.back = Some(ev);
+ }
+
+ fn next_event(&mut self) -> Option<Event<'a>> {
+ let next = self.back.take().or_else(|| {
+ self.stream.next().map(|(ev, range)| {
+ self.offset = range.start;
+ ev
+ })
+ });
+
+ trace!("Next event: {:?}", next);
+
+ next
+ }
+
+ fn parse_nested_numbered(&mut self, parent: &SectionNumber) -> Result<Vec<SummaryItem>> {
+ debug!("Parsing numbered chapters at level {}", parent);
+ let mut items = Vec::new();
+
+ loop {
+ match self.next_event() {
+ Some(Event::Start(Tag::Item)) => {
+ let item = self.parse_nested_item(parent, items.len())?;
+ items.push(item);
+ }
+ Some(Event::Start(Tag::List(..))) => {
+ // Skip this tag after comment bacause it is not nested.
+ if items.is_empty() {
+ continue;
+ }
+ // recurse to parse the nested list
+ let (_, last_item) = get_last_link(&mut items)?;
+ let last_item_number = last_item
+ .number
+ .as_ref()
+ .expect("All numbered chapters have numbers");
+
+ let sub_items = self.parse_nested_numbered(last_item_number)?;
+
+ last_item.nested_items = sub_items;
+ }
+ Some(Event::End(Tag::List(..))) => break,
+ Some(_) => {}
+ None => break,
+ }
+ }
+
+ Ok(items)
+ }
+
+ fn parse_nested_item(
+ &mut self,
+ parent: &SectionNumber,
+ num_existing_items: usize,
+ ) -> Result<SummaryItem> {
+ loop {
+ match self.next_event() {
+ Some(Event::Start(Tag::Paragraph)) => continue,
+ Some(Event::Start(Tag::Link(_type, href, _title))) => {
+ let mut link = self.parse_link(href.to_string());
+
+ let mut number = parent.clone();
+ number.0.push(num_existing_items as u32 + 1);
+ trace!(
+ "Found chapter: {} {} ({})",
+ number,
+ link.name,
+ link.location
+ .as_ref()
+ .map(|p| p.to_str().unwrap_or(""))
+ .unwrap_or("[draft]")
+ );
+
+ link.number = Some(number);
+
+ return Ok(SummaryItem::Link(link));
+ }
+ other => {
+ warn!("Expected a start of a link, actually got {:?}", other);
+ bail!(self.parse_error(
+ "The link items for nested chapters must only contain a hyperlink"
+ ));
+ }
+ }
+ }
+ }
+
+ fn parse_error<D: Display>(&self, msg: D) -> Error {
+ let (line, col) = self.current_location();
+ anyhow::anyhow!(
+ "failed to parse SUMMARY.md line {}, column {}: {}",
+ line,
+ col,
+ msg
+ )
+ }
+
+ /// Try to parse the title line.
+ fn parse_title(&mut self) -> Option<String> {
+ loop {
+ match self.next_event() {
+ Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
+ debug!("Found a h1 in the SUMMARY");
+
+ let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
+ return Some(stringify_events(tags));
+ }
+ // Skip a HTML element such as a comment line.
+ Some(Event::Html(_)) => {}
+ // Otherwise, no title.
+ Some(ev) => {
+ self.back(ev);
+ return None;
+ }
+ _ => return None,
+ }
+ }
+ }
+}
+
+fn update_section_numbers(sections: &mut [SummaryItem], level: usize, by: u32) {
+ for section in sections {
+ if let SummaryItem::Link(ref mut link) = *section {
+ if let Some(ref mut number) = link.number {
+ number.0[level] += by;
+ }
+
+ update_section_numbers(&mut link.nested_items, level, by);
+ }
+ }
+}
+
+/// Gets a pointer to the last `Link` in a list of `SummaryItem`s, and its
+/// index.
+fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
+ links
+ .iter_mut()
+ .enumerate()
+ .filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
+ .rev()
+ .next()
+ .ok_or_else(||
+ anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links")
+ )
+}
+
+/// Removes the styling from a list of Markdown events and returns just the
+/// plain text.
+fn stringify_events(events: Vec<Event<'_>>) -> String {
+ events
+ .into_iter()
+ .filter_map(|t| match t {
+ Event::Text(text) | Event::Code(text) => Some(text.into_string()),
+ Event::SoftBreak => Some(String::from(" ")),
+ _ => None,
+ })
+ .collect()
+}
+
+/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
+/// a pretty `Display` impl.
+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
+pub struct SectionNumber(pub Vec<u32>);
+
+impl Display for SectionNumber {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ if self.0.is_empty() {
+ write!(f, "0")
+ } else {
+ for item in &self.0 {
+ write!(f, "{}.", item)?;
+ }
+ Ok(())
+ }
+ }
+}
+
+impl Deref for SectionNumber {
+ type Target = Vec<u32>;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for SectionNumber {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl FromIterator<u32> for SectionNumber {
+ fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
+ SectionNumber(it.into_iter().collect())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn section_number_has_correct_dotted_representation() {
+ let inputs = vec![
+ (vec![0], "0."),
+ (vec![1, 3], "1.3."),
+ (vec![1, 2, 3], "1.2.3."),
+ ];
+
+ for (input, should_be) in inputs {
+ let section_number = SectionNumber(input).to_string();
+ assert_eq!(section_number, should_be);
+ }
+ }
+
+ #[test]
+ fn parse_initial_title() {
+ let src = "# Summary";
+ let should_be = String::from("Summary");
+
+ let mut parser = SummaryParser::new(src);
+ let got = parser.parse_title().unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn no_initial_title() {
+ let src = "[Link]()";
+ let mut parser = SummaryParser::new(src);
+
+ assert!(parser.parse_title().is_none());
+ assert!(matches!(
+ parser.next_event(),
+ Some(Event::Start(Tag::Paragraph))
+ ));
+ }
+
+ #[test]
+ fn parse_title_with_styling() {
+ let src = "# My **Awesome** Summary";
+ let should_be = String::from("My Awesome Summary");
+
+ let mut parser = SummaryParser::new(src);
+ let got = parser.parse_title().unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn convert_markdown_events_to_a_string() {
+ let src = "Hello *World*, `this` is some text [and a link](./path/to/link)";
+ let should_be = "Hello World, this is some text and a link";
+
+ let events = pulldown_cmark::Parser::new(src).collect();
+ let got = stringify_events(events);
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn parse_some_prefix_items() {
+ let src = "[First](./first.md)\n[Second](./second.md)\n";
+ let mut parser = SummaryParser::new(src);
+
+ let should_be = vec![
+ SummaryItem::Link(Link {
+ name: String::from("First"),
+ location: Some(PathBuf::from("./first.md")),
+ ..Default::default()
+ }),
+ SummaryItem::Link(Link {
+ name: String::from("Second"),
+ location: Some(PathBuf::from("./second.md")),
+ ..Default::default()
+ }),
+ ];
+
+ let got = parser.parse_affix(true).unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn parse_prefix_items_with_a_separator() {
+ let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
+ let mut parser = SummaryParser::new(src);
+
+ let got = parser.parse_affix(true).unwrap();
+
+ assert_eq!(got.len(), 3);
+ assert_eq!(got[1], SummaryItem::Separator);
+ }
+
+ #[test]
+ fn suffix_items_cannot_be_followed_by_a_list() {
+ let src = "[First](./first.md)\n- [Second](./second.md)\n";
+ let mut parser = SummaryParser::new(src);
+
+ let got = parser.parse_affix(false);
+
+ assert!(got.is_err());
+ }
+
+ #[test]
+ fn parse_a_link() {
+ let src = "[First](./first.md)";
+ let should_be = Link {
+ name: String::from("First"),
+ location: Some(PathBuf::from("./first.md")),
+ ..Default::default()
+ };
+
+ let mut parser = SummaryParser::new(src);
+ let _ = parser.stream.next(); // Discard opening paragraph
+
+ let href = match parser.stream.next() {
+ Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(),
+ other => panic!("Unreachable, {:?}", other),
+ };
+
+ let got = parser.parse_link(href);
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn parse_a_numbered_chapter() {
+ let src = "- [First](./first.md)\n";
+ let link = Link {
+ name: String::from("First"),
+ location: Some(PathBuf::from("./first.md")),
+ number: Some(SectionNumber(vec![1])),
+ ..Default::default()
+ };
+ let should_be = vec![SummaryItem::Link(link)];
+
+ let mut parser = SummaryParser::new(src);
+ let got = parser
+ .parse_numbered(&mut 0, &mut SectionNumber::default())
+ .unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn parse_nested_numbered_chapters() {
+ let src = "- [First](./first.md)\n - [Nested](./nested.md)\n- [Second](./second.md)";
+
+ let should_be = vec![
+ SummaryItem::Link(Link {
+ name: String::from("First"),
+ location: Some(PathBuf::from("./first.md")),
+ number: Some(SectionNumber(vec![1])),
+ nested_items: vec![SummaryItem::Link(Link {
+ name: String::from("Nested"),
+ location: Some(PathBuf::from("./nested.md")),
+ number: Some(SectionNumber(vec![1, 1])),
+ nested_items: Vec::new(),
+ })],
+ }),
+ SummaryItem::Link(Link {
+ name: String::from("Second"),
+ location: Some(PathBuf::from("./second.md")),
+ number: Some(SectionNumber(vec![2])),
+ nested_items: Vec::new(),
+ }),
+ ];
+
+ let mut parser = SummaryParser::new(src);
+ let got = parser
+ .parse_numbered(&mut 0, &mut SectionNumber::default())
+ .unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn parse_numbered_chapters_separated_by_comment() {
+ let src = "- [First](./first.md)\n<!-- this is a comment -->\n- [Second](./second.md)";
+
+ let should_be = vec![
+ SummaryItem::Link(Link {
+ name: String::from("First"),
+ location: Some(PathBuf::from("./first.md")),
+ number: Some(SectionNumber(vec![1])),
+ nested_items: Vec::new(),
+ }),
+ SummaryItem::Link(Link {
+ name: String::from("Second"),
+ location: Some(PathBuf::from("./second.md")),
+ number: Some(SectionNumber(vec![2])),
+ nested_items: Vec::new(),
+ }),
+ ];
+
+ let mut parser = SummaryParser::new(src);
+ let got = parser
+ .parse_numbered(&mut 0, &mut SectionNumber::default())
+ .unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn parse_titled_parts() {
+ let src = "- [First](./first.md)\n- [Second](./second.md)\n\
+ # Title 2\n- [Third](./third.md)\n\t- [Fourth](./fourth.md)";
+
+ let should_be = vec![
+ SummaryItem::Link(Link {
+ name: String::from("First"),
+ location: Some(PathBuf::from("./first.md")),
+ number: Some(SectionNumber(vec![1])),
+ nested_items: Vec::new(),
+ }),
+ SummaryItem::Link(Link {
+ name: String::from("Second"),
+ location: Some(PathBuf::from("./second.md")),
+ number: Some(SectionNumber(vec![2])),
+ nested_items: Vec::new(),
+ }),
+ SummaryItem::PartTitle(String::from("Title 2")),
+ SummaryItem::Link(Link {
+ name: String::from("Third"),
+ location: Some(PathBuf::from("./third.md")),
+ number: Some(SectionNumber(vec![3])),
+ nested_items: vec![SummaryItem::Link(Link {
+ name: String::from("Fourth"),
+ location: Some(PathBuf::from("./fourth.md")),
+ number: Some(SectionNumber(vec![3, 1])),
+ nested_items: Vec::new(),
+ })],
+ }),
+ ];
+
+ let mut parser = SummaryParser::new(src);
+ let got = parser.parse_parts().unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ /// This test ensures the book will continue to pass because it breaks the
+ /// `SUMMARY.md` up using level 2 headers ([example]).
+ ///
+ /// [example]: https://github.com/rust-lang/book/blob/2c942dc094f4ddcdc7aba7564f80782801197c99/second-edition/src/SUMMARY.md#basic-rust-literacy
+ #[test]
+ fn can_have_a_subheader_between_nested_items() {
+ let src = "- [First](./first.md)\n\n## Subheading\n\n- [Second](./second.md)\n";
+ let should_be = vec![
+ SummaryItem::Link(Link {
+ name: String::from("First"),
+ location: Some(PathBuf::from("./first.md")),
+ number: Some(SectionNumber(vec![1])),
+ nested_items: Vec::new(),
+ }),
+ SummaryItem::Link(Link {
+ name: String::from("Second"),
+ location: Some(PathBuf::from("./second.md")),
+ number: Some(SectionNumber(vec![2])),
+ nested_items: Vec::new(),
+ }),
+ ];
+
+ let mut parser = SummaryParser::new(src);
+ let got = parser
+ .parse_numbered(&mut 0, &mut SectionNumber::default())
+ .unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn an_empty_link_location_is_a_draft_chapter() {
+ let src = "- [Empty]()\n";
+ let mut parser = SummaryParser::new(src);
+
+ let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default());
+ let should_be = vec![SummaryItem::Link(Link {
+ name: String::from("Empty"),
+ location: None,
+ number: Some(SectionNumber(vec![1])),
+ nested_items: Vec::new(),
+ })];
+
+ assert!(got.is_ok());
+ assert_eq!(got.unwrap(), should_be);
+ }
+
+ /// Regression test for https://github.com/rust-lang/mdBook/issues/779
+ /// Ensure section numbers are correctly incremented after a horizontal separator.
+ #[test]
+ fn keep_numbering_after_separator() {
+ let src =
+ "- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
+ let should_be = vec![
+ SummaryItem::Link(Link {
+ name: String::from("First"),
+ location: Some(PathBuf::from("./first.md")),
+ number: Some(SectionNumber(vec![1])),
+ nested_items: Vec::new(),
+ }),
+ SummaryItem::Separator,
+ SummaryItem::Link(Link {
+ name: String::from("Second"),
+ location: Some(PathBuf::from("./second.md")),
+ number: Some(SectionNumber(vec![2])),
+ nested_items: Vec::new(),
+ }),
+ SummaryItem::Separator,
+ SummaryItem::Link(Link {
+ name: String::from("Third"),
+ location: Some(PathBuf::from("./third.md")),
+ number: Some(SectionNumber(vec![3])),
+ nested_items: Vec::new(),
+ }),
+ ];
+
+ let mut parser = SummaryParser::new(src);
+ let got = parser
+ .parse_numbered(&mut 0, &mut SectionNumber::default())
+ .unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ /// Regression test for https://github.com/rust-lang/mdBook/issues/1218
+ /// Ensure chapter names spread across multiple lines have spaces between all the words.
+ #[test]
+ fn add_space_for_multi_line_chapter_names() {
+ let src = "- [Chapter\ntitle](./chapter.md)";
+ let should_be = vec![SummaryItem::Link(Link {
+ name: String::from("Chapter title"),
+ location: Some(PathBuf::from("./chapter.md")),
+ number: Some(SectionNumber(vec![1])),
+ nested_items: Vec::new(),
+ })];
+
+ let mut parser = SummaryParser::new(src);
+ let got = parser
+ .parse_numbered(&mut 0, &mut SectionNumber::default())
+ .unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn allow_space_in_link_destination() {
+ let src = "- [test1](./test%20link1.md)\n- [test2](<./test link2.md>)";
+ let should_be = vec![
+ SummaryItem::Link(Link {
+ name: String::from("test1"),
+ location: Some(PathBuf::from("./test link1.md")),
+ number: Some(SectionNumber(vec![1])),
+ nested_items: Vec::new(),
+ }),
+ SummaryItem::Link(Link {
+ name: String::from("test2"),
+ location: Some(PathBuf::from("./test link2.md")),
+ number: Some(SectionNumber(vec![2])),
+ nested_items: Vec::new(),
+ }),
+ ];
+ let mut parser = SummaryParser::new(src);
+ let got = parser
+ .parse_numbered(&mut 0, &mut SectionNumber::default())
+ .unwrap();
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn skip_html_comments() {
+ let src = r#"<!--
+# Title - En
+-->
+# Title - Local
+
+<!--
+[Prefix 00-01 - En](ch00-01.md)
+[Prefix 00-02 - En](ch00-02.md)
+-->
+[Prefix 00-01 - Local](ch00-01.md)
+[Prefix 00-02 - Local](ch00-02.md)
+
+<!--
+## Section Title - En
+-->
+## Section Title - Localized
+
+<!--
+- [Ch 01-00 - En](ch01-00.md)
+ - [Ch 01-01 - En](ch01-01.md)
+ - [Ch 01-02 - En](ch01-02.md)
+-->
+- [Ch 01-00 - Local](ch01-00.md)
+ - [Ch 01-01 - Local](ch01-01.md)
+ - [Ch 01-02 - Local](ch01-02.md)
+
+<!--
+- [Ch 02-00 - En](ch02-00.md)
+-->
+- [Ch 02-00 - Local](ch02-00.md)
+
+<!--
+[Appendix A - En](appendix-01.md)
+[Appendix B - En](appendix-02.md)
+-->`
+[Appendix A - Local](appendix-01.md)
+[Appendix B - Local](appendix-02.md)
+"#;
+
+ let mut parser = SummaryParser::new(src);
+
+ // ---- Title ----
+ let title = parser.parse_title();
+ assert_eq!(title, Some(String::from("Title - Local")));
+
+ // ---- Prefix Chapters ----
+
+ let new_affix_item = |name, location| {
+ SummaryItem::Link(Link {
+ name: String::from(name),
+ location: Some(PathBuf::from(location)),
+ ..Default::default()
+ })
+ };
+
+ let should_be = vec![
+ new_affix_item("Prefix 00-01 - Local", "ch00-01.md"),
+ new_affix_item("Prefix 00-02 - Local", "ch00-02.md"),
+ ];
+
+ let got = parser.parse_affix(true).unwrap();
+ assert_eq!(got, should_be);
+
+ // ---- Numbered Chapters ----
+
+ let new_numbered_item = |name, location, numbers: &[u32], nested_items| {
+ SummaryItem::Link(Link {
+ name: String::from(name),
+ location: Some(PathBuf::from(location)),
+ number: Some(SectionNumber(numbers.to_vec())),
+ nested_items,
+ })
+ };
+
+ let ch01_nested = vec![
+ new_numbered_item("Ch 01-01 - Local", "ch01-01.md", &[1, 1], vec![]),
+ new_numbered_item("Ch 01-02 - Local", "ch01-02.md", &[1, 2], vec![]),
+ ];
+
+ let should_be = vec![
+ new_numbered_item("Ch 01-00 - Local", "ch01-00.md", &[1], ch01_nested),
+ new_numbered_item("Ch 02-00 - Local", "ch02-00.md", &[2], vec![]),
+ ];
+ let got = parser.parse_parts().unwrap();
+ assert_eq!(got, should_be);
+
+ // ---- Suffix Chapters ----
+
+ let should_be = vec![
+ new_affix_item("Appendix A - Local", "appendix-01.md"),
+ new_affix_item("Appendix B - Local", "appendix-02.md"),
+ ];
+
+ let got = parser.parse_affix(false).unwrap();
+ assert_eq!(got, should_be);
+ }
+}
diff --git a/vendor/mdbook/src/cmd/build.rs b/vendor/mdbook/src/cmd/build.rs
new file mode 100644
index 000000000..5fe73236c
--- /dev/null
+++ b/vendor/mdbook/src/cmd/build.rs
@@ -0,0 +1,50 @@
+use crate::{get_book_dir, open};
+use clap::{arg, App, Arg, ArgMatches};
+use mdbook::errors::Result;
+use mdbook::MDBook;
+
+// Create clap subcommand arguments
+pub fn make_subcommand<'help>() -> App<'help> {
+ App::new("build")
+ .about("Builds a book from its markdown files")
+ .arg(
+ Arg::new("dest-dir")
+ .short('d')
+ .long("dest-dir")
+ .value_name("dest-dir")
+ .help(
+ "Output directory for the book{n}\
+ Relative paths are interpreted relative to the book's root directory.{n}\
+ If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
+ ),
+ )
+ .arg(arg!([dir]
+ "Root directory for the book{n}\
+ (Defaults to the Current Directory when omitted)"
+ ))
+ .arg(arg!(-o --open "Opens the compiled book in a web browser"))
+}
+
+// Build command implementation
+pub fn execute(args: &ArgMatches) -> Result<()> {
+ let book_dir = get_book_dir(args);
+ let mut book = MDBook::load(&book_dir)?;
+
+ if let Some(dest_dir) = args.value_of("dest-dir") {
+ book.config.build.build_dir = dest_dir.into();
+ }
+
+ book.build()?;
+
+ if args.is_present("open") {
+ // FIXME: What's the right behaviour if we don't use the HTML renderer?
+ let path = book.build_dir_for("html").join("index.html");
+ if !path.exists() {
+ error!("No chapter available to open");
+ std::process::exit(1)
+ }
+ open(path);
+ }
+
+ Ok(())
+}
diff --git a/vendor/mdbook/src/cmd/clean.rs b/vendor/mdbook/src/cmd/clean.rs
new file mode 100644
index 000000000..0569726e1
--- /dev/null
+++ b/vendor/mdbook/src/cmd/clean.rs
@@ -0,0 +1,44 @@
+use crate::get_book_dir;
+use anyhow::Context;
+use clap::{arg, App, Arg, ArgMatches};
+use mdbook::MDBook;
+use std::fs;
+
+// Create clap subcommand arguments
+pub fn make_subcommand<'help>() -> App<'help> {
+ App::new("clean")
+ .about("Deletes a built book")
+ .arg(
+ Arg::new("dest-dir")
+ .short('d')
+ .long("dest-dir")
+ .value_name("dest-dir")
+ .help(
+ "Output directory for the book{n}\
+ Relative paths are interpreted relative to the book's root directory.{n}\
+ If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
+ ),
+ )
+ .arg(arg!([dir]
+ "Root directory for the book{n}\
+ (Defaults to the Current Directory when omitted)"
+ ))
+}
+
+// Clean command implementation
+pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
+ let book_dir = get_book_dir(args);
+ let book = MDBook::load(&book_dir)?;
+
+ let dir_to_remove = match args.value_of("dest-dir") {
+ Some(dest_dir) => dest_dir.into(),
+ None => book.root.join(&book.config.build.build_dir),
+ };
+
+ if dir_to_remove.exists() {
+ fs::remove_dir_all(&dir_to_remove)
+ .with_context(|| "Unable to remove the build directory")?;
+ }
+
+ Ok(())
+}
diff --git a/vendor/mdbook/src/cmd/init.rs b/vendor/mdbook/src/cmd/init.rs
new file mode 100644
index 000000000..c964dcc13
--- /dev/null
+++ b/vendor/mdbook/src/cmd/init.rs
@@ -0,0 +1,126 @@
+use crate::get_book_dir;
+use clap::{arg, App, Arg, ArgMatches};
+use mdbook::config;
+use mdbook::errors::Result;
+use mdbook::MDBook;
+use std::io;
+use std::io::Write;
+use std::process::Command;
+
+// Create clap subcommand arguments
+pub fn make_subcommand<'help>() -> App<'help> {
+ App::new("init")
+ .about("Creates the boilerplate structure and files for a new book")
+ // the {n} denotes a newline which will properly aligned in all help messages
+ .arg(arg!([dir]
+ "Directory to create the book in{n}\
+ (Defaults to the Current Directory when omitted)"
+ ))
+ .arg(arg!(--theme "Copies the default theme into your source folder"))
+ .arg(arg!(--force "Skips confirmation prompts"))
+ .arg(
+ Arg::new("title")
+ .long("title")
+ .takes_value(true)
+ .help("Sets the book title")
+ .required(false),
+ )
+ .arg(
+ Arg::new("ignore")
+ .long("ignore")
+ .takes_value(true)
+ .possible_values(&["none", "git"])
+ .help("Creates a VCS ignore file (i.e. .gitignore)")
+ .required(false),
+ )
+}
+
+// Init command implementation
+pub fn execute(args: &ArgMatches) -> Result<()> {
+ let book_dir = get_book_dir(args);
+ let mut builder = MDBook::init(&book_dir);
+ let mut config = config::Config::default();
+ // If flag `--theme` is present, copy theme to src
+ if args.is_present("theme") {
+ let theme_dir = book_dir.join("theme");
+ println!();
+ println!("Copying the default theme to {}", theme_dir.display());
+ // Skip this if `--force` is present
+ if !args.is_present("force") && theme_dir.exists() {
+ println!("This could potentially overwrite files already present in that directory.");
+ print!("\nAre you sure you want to continue? (y/n) ");
+
+ // Read answer from user and exit if it's not 'yes'
+ if confirm() {
+ builder.copy_theme(true);
+ }
+ } else {
+ builder.copy_theme(true);
+ }
+ }
+
+ if let Some(ignore) = args.value_of("ignore") {
+ match ignore {
+ "git" => builder.create_gitignore(true),
+ _ => builder.create_gitignore(false),
+ };
+ } else {
+ println!("\nDo you want a .gitignore to be created? (y/n)");
+ if confirm() {
+ builder.create_gitignore(true);
+ }
+ }
+
+ config.book.title = if args.is_present("title") {
+ args.value_of("title").map(String::from)
+ } else {
+ request_book_title()
+ };
+
+ if let Some(author) = get_author_name() {
+ debug!("Obtained user name from gitconfig: {:?}", author);
+ config.book.authors.push(author);
+ builder.with_config(config);
+ }
+
+ builder.build()?;
+ println!("\nAll done, no errors...");
+
+ Ok(())
+}
+
+/// Obtains author name from git config file by running the `git config` command.
+fn get_author_name() -> Option<String> {
+ let output = Command::new("git")
+ .args(&["config", "--get", "user.name"])
+ .output()
+ .ok()?;
+
+ if output.status.success() {
+ Some(String::from_utf8_lossy(&output.stdout).trim().to_owned())
+ } else {
+ None
+ }
+}
+
+/// Request book title from user and return if provided.
+fn request_book_title() -> Option<String> {
+ println!("What title would you like to give the book? ");
+ io::stdout().flush().unwrap();
+ let mut resp = String::new();
+ io::stdin().read_line(&mut resp).unwrap();
+ let resp = resp.trim();
+ if resp.is_empty() {
+ None
+ } else {
+ Some(resp.into())
+ }
+}
+
+// Simple function for user confirmation
+fn confirm() -> bool {
+ io::stdout().flush().unwrap();
+ let mut s = String::new();
+ io::stdin().read_line(&mut s).ok();
+ matches!(&*s.trim(), "Y" | "y" | "yes" | "Yes")
+}
diff --git a/vendor/mdbook/src/cmd/mod.rs b/vendor/mdbook/src/cmd/mod.rs
new file mode 100644
index 000000000..c5b6730f1
--- /dev/null
+++ b/vendor/mdbook/src/cmd/mod.rs
@@ -0,0 +1,10 @@
+//! Subcommand modules for the `mdbook` binary.
+
+pub mod build;
+pub mod clean;
+pub mod init;
+#[cfg(feature = "serve")]
+pub mod serve;
+pub mod test;
+#[cfg(feature = "watch")]
+pub mod watch;
diff --git a/vendor/mdbook/src/cmd/serve.rs b/vendor/mdbook/src/cmd/serve.rs
new file mode 100644
index 000000000..bafbfd52e
--- /dev/null
+++ b/vendor/mdbook/src/cmd/serve.rs
@@ -0,0 +1,177 @@
+#[cfg(feature = "watch")]
+use super::watch;
+use crate::{get_book_dir, open};
+use clap::{arg, App, Arg, ArgMatches};
+use futures_util::sink::SinkExt;
+use futures_util::StreamExt;
+use mdbook::errors::*;
+use mdbook::utils;
+use mdbook::utils::fs::get_404_output_file;
+use mdbook::MDBook;
+use std::net::{SocketAddr, ToSocketAddrs};
+use std::path::PathBuf;
+use tokio::sync::broadcast;
+use warp::ws::Message;
+use warp::Filter;
+
+/// The HTTP endpoint for the websocket used to trigger reloads when a file changes.
+const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
+
+// Create clap subcommand arguments
+pub fn make_subcommand<'help>() -> App<'help> {
+ App::new("serve")
+ .about("Serves a book at http://localhost:3000, and rebuilds it on changes")
+ .arg(
+ Arg::new("dest-dir")
+ .short('d')
+ .long("dest-dir")
+ .value_name("dest-dir")
+ .help(
+ "Output directory for the book{n}\
+ Relative paths are interpreted relative to the book's root directory.{n}\
+ If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
+ ),
+ )
+ .arg(arg!([dir]
+ "Root directory for the book{n}\
+ (Defaults to the Current Directory when omitted)"
+ ))
+ .arg(
+ Arg::new("hostname")
+ .short('n')
+ .long("hostname")
+ .takes_value(true)
+ .default_value("localhost")
+ .forbid_empty_values(true)
+ .help("Hostname to listen on for HTTP connections"),
+ )
+ .arg(
+ Arg::new("port")
+ .short('p')
+ .long("port")
+ .takes_value(true)
+ .default_value("3000")
+ .forbid_empty_values(true)
+ .help("Port to use for HTTP connections"),
+ )
+ .arg(arg!(-o --open "Opens the compiled book in a web browser"))
+}
+
+// Serve command implementation
+pub fn execute(args: &ArgMatches) -> Result<()> {
+ let book_dir = get_book_dir(args);
+ let mut book = MDBook::load(&book_dir)?;
+
+ let port = args.value_of("port").unwrap();
+ let hostname = args.value_of("hostname").unwrap();
+ let open_browser = args.is_present("open");
+
+ let address = format!("{}:{}", hostname, port);
+
+ let update_config = |book: &mut MDBook| {
+ book.config
+ .set("output.html.live-reload-endpoint", &LIVE_RELOAD_ENDPOINT)
+ .expect("live-reload-endpoint update failed");
+ if let Some(dest_dir) = args.value_of("dest-dir") {
+ book.config.build.build_dir = dest_dir.into();
+ }
+ // Override site-url for local serving of the 404 file
+ book.config.set("output.html.site-url", "/").unwrap();
+ };
+ update_config(&mut book);
+ book.build()?;
+
+ let sockaddr: SocketAddr = address
+ .to_socket_addrs()?
+ .next()
+ .ok_or_else(|| anyhow::anyhow!("no address found for {}", address))?;
+ let build_dir = book.build_dir_for("html");
+ let input_404 = book
+ .config
+ .get("output.html.input-404")
+ .map(toml::Value::as_str)
+ .and_then(std::convert::identity) // flatten
+ .map(ToString::to_string);
+ let file_404 = get_404_output_file(&input_404);
+
+ // A channel used to broadcast to any websockets to reload when a file changes.
+ let (tx, _rx) = tokio::sync::broadcast::channel::<Message>(100);
+
+ let reload_tx = tx.clone();
+ let thread_handle = std::thread::spawn(move || {
+ serve(build_dir, sockaddr, reload_tx, &file_404);
+ });
+
+ let serving_url = format!("http://{}", address);
+ info!("Serving on: {}", serving_url);
+
+ if open_browser {
+ open(serving_url);
+ }
+
+ #[cfg(feature = "watch")]
+ watch::trigger_on_change(&book, move |paths, book_dir| {
+ info!("Files changed: {:?}", paths);
+ info!("Building book...");
+
+ // FIXME: This area is really ugly because we need to re-set livereload :(
+ let result = MDBook::load(&book_dir).and_then(|mut b| {
+ update_config(&mut b);
+ b.build()
+ });
+
+ if let Err(e) = result {
+ error!("Unable to load the book");
+ utils::log_backtrace(&e);
+ } else {
+ let _ = tx.send(Message::text("reload"));
+ }
+ });
+
+ let _ = thread_handle.join();
+
+ Ok(())
+}
+
+#[tokio::main]
+async fn serve(
+ build_dir: PathBuf,
+ address: SocketAddr,
+ reload_tx: broadcast::Sender<Message>,
+ file_404: &str,
+) {
+ // A warp Filter which captures `reload_tx` and provides an `rx` copy to
+ // receive reload messages.
+ let sender = warp::any().map(move || reload_tx.subscribe());
+
+ // A warp Filter to handle the livereload endpoint. This upgrades to a
+ // websocket, and then waits for any filesystem change notifications, and
+ // relays them over the websocket.
+ let livereload = warp::path(LIVE_RELOAD_ENDPOINT)
+ .and(warp::ws())
+ .and(sender)
+ .map(|ws: warp::ws::Ws, mut rx: broadcast::Receiver<Message>| {
+ ws.on_upgrade(move |ws| async move {
+ let (mut user_ws_tx, _user_ws_rx) = ws.split();
+ trace!("websocket got connection");
+ if let Ok(m) = rx.recv().await {
+ trace!("notify of reload");
+ let _ = user_ws_tx.send(m).await;
+ }
+ })
+ });
+ // A warp Filter that serves from the filesystem.
+ let book_route = warp::fs::dir(build_dir.clone());
+ // The fallback route for 404 errors
+ let fallback_route = warp::fs::file(build_dir.join(file_404))
+ .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
+ let routes = livereload.or(book_route).or(fallback_route);
+
+ std::panic::set_hook(Box::new(move |panic_info| {
+ // exit if serve panics
+ error!("Unable to serve: {}", panic_info);
+ std::process::exit(1);
+ }));
+
+ warp::serve(routes).run(address).await;
+}
diff --git a/vendor/mdbook/src/cmd/test.rs b/vendor/mdbook/src/cmd/test.rs
new file mode 100644
index 000000000..02f982a49
--- /dev/null
+++ b/vendor/mdbook/src/cmd/test.rs
@@ -0,0 +1,54 @@
+use crate::get_book_dir;
+use clap::{arg, App, Arg, ArgMatches};
+use mdbook::errors::Result;
+use mdbook::MDBook;
+
+// Create clap subcommand arguments
+pub fn make_subcommand<'help>() -> App<'help> {
+ App::new("test")
+ .about("Tests that a book's Rust code samples compile")
+ .arg(
+ Arg::new("dest-dir")
+ .short('d')
+ .long("dest-dir")
+ .value_name("dest-dir")
+ .help(
+ "Output directory for the book{n}\
+ Relative paths are interpreted relative to the book's root directory.{n}\
+ If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
+ ),
+ )
+ .arg(arg!([dir]
+ "Root directory for the book{n}\
+ (Defaults to the Current Directory when omitted)"
+ ))
+ .arg(Arg::new("library-path")
+ .short('L')
+ .long("library-path")
+ .value_name("dir")
+ .takes_value(true)
+ .use_delimiter(true)
+ .require_delimiter(true)
+ .multiple_values(true)
+ .multiple_occurrences(true)
+ .forbid_empty_values(true)
+ .help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
+}
+
+// test command implementation
+pub fn execute(args: &ArgMatches) -> Result<()> {
+ let library_paths: Vec<&str> = args
+ .values_of("library-path")
+ .map(std::iter::Iterator::collect)
+ .unwrap_or_default();
+ let book_dir = get_book_dir(args);
+ let mut book = MDBook::load(&book_dir)?;
+
+ if let Some(dest_dir) = args.value_of("dest-dir") {
+ book.config.build.build_dir = dest_dir.into();
+ }
+
+ book.test(library_paths)?;
+
+ Ok(())
+}
diff --git a/vendor/mdbook/src/cmd/watch.rs b/vendor/mdbook/src/cmd/watch.rs
new file mode 100644
index 000000000..9336af779
--- /dev/null
+++ b/vendor/mdbook/src/cmd/watch.rs
@@ -0,0 +1,175 @@
+use crate::{get_book_dir, open};
+use clap::{arg, App, Arg, ArgMatches};
+use mdbook::errors::Result;
+use mdbook::utils;
+use mdbook::MDBook;
+use notify::Watcher;
+use std::path::{Path, PathBuf};
+use std::sync::mpsc::channel;
+use std::thread::sleep;
+use std::time::Duration;
+
+// Create clap subcommand arguments
+pub fn make_subcommand<'help>() -> App<'help> {
+ App::new("watch")
+ .about("Watches a book's files and rebuilds it on changes")
+ .arg(
+ Arg::new("dest-dir")
+ .short('d')
+ .long("dest-dir")
+ .value_name("dest-dir")
+ .help(
+ "Output directory for the book{n}\
+ Relative paths are interpreted relative to the book's root directory.{n}\
+ If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
+ ),
+ )
+ .arg(arg!([dir]
+ "Root directory for the book{n}\
+ (Defaults to the Current Directory when omitted)"
+ ))
+ .arg(arg!(-o --open "Opens the compiled book in a web browser"))
+}
+
+// Watch command implementation
+pub fn execute(args: &ArgMatches) -> Result<()> {
+ let book_dir = get_book_dir(args);
+ let mut book = MDBook::load(&book_dir)?;
+
+ let update_config = |book: &mut MDBook| {
+ if let Some(dest_dir) = args.value_of("dest-dir") {
+ book.config.build.build_dir = dest_dir.into();
+ }
+ };
+ update_config(&mut book);
+
+ if args.is_present("open") {
+ book.build()?;
+ let path = book.build_dir_for("html").join("index.html");
+ if !path.exists() {
+ error!("No chapter available to open");
+ std::process::exit(1)
+ }
+ open(path);
+ }
+
+ trigger_on_change(&book, |paths, book_dir| {
+ info!("Files changed: {:?}\nBuilding book...\n", paths);
+ let result = MDBook::load(&book_dir).and_then(|mut b| {
+ update_config(&mut b);
+ b.build()
+ });
+
+ if let Err(e) = result {
+ error!("Unable to build the book");
+ utils::log_backtrace(&e);
+ }
+ });
+
+ Ok(())
+}
+
+fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
+ if paths.is_empty() {
+ return vec![];
+ }
+
+ match find_gitignore(book_root) {
+ Some(gitignore_path) => {
+ match gitignore::File::new(gitignore_path.as_path()) {
+ Ok(exclusion_checker) => filter_ignored_files(exclusion_checker, paths),
+ Err(_) => {
+ // We're unable to read the .gitignore file, so we'll silently allow everything.
+ // Please see discussion: https://github.com/rust-lang/mdBook/pull/1051
+ paths.iter().map(|path| path.to_path_buf()).collect()
+ }
+ }
+ }
+ None => {
+ // There is no .gitignore file.
+ paths.iter().map(|path| path.to_path_buf()).collect()
+ }
+ }
+}
+
+fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
+ book_root
+ .ancestors()
+ .map(|p| p.join(".gitignore"))
+ .find(|p| p.exists())
+}
+
+fn filter_ignored_files(exclusion_checker: gitignore::File, paths: &[PathBuf]) -> Vec<PathBuf> {
+ paths
+ .iter()
+ .filter(|path| match exclusion_checker.is_excluded(path) {
+ Ok(exclude) => !exclude,
+ Err(error) => {
+ warn!(
+ "Unable to determine if {:?} is excluded: {:?}. Including it.",
+ &path, error
+ );
+ true
+ }
+ })
+ .map(|path| path.to_path_buf())
+ .collect()
+}
+
+/// Calls the closure when a book source file is changed, blocking indefinitely.
+pub fn trigger_on_change<F>(book: &MDBook, closure: F)
+where
+ F: Fn(Vec<PathBuf>, &Path),
+{
+ use notify::DebouncedEvent::*;
+ use notify::RecursiveMode::*;
+
+ // Create a channel to receive the events.
+ let (tx, rx) = channel();
+
+ let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) {
+ Ok(w) => w,
+ Err(e) => {
+ error!("Error while trying to watch the files:\n\n\t{:?}", e);
+ std::process::exit(1)
+ }
+ };
+
+ // Add the source directory to the watcher
+ if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
+ error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
+ std::process::exit(1);
+ };
+
+ let _ = watcher.watch(book.theme_dir(), Recursive);
+
+ // Add the book.toml file to the watcher if it exists
+ let _ = watcher.watch(book.root.join("book.toml"), NonRecursive);
+
+ info!("Listening for changes...");
+
+ loop {
+ let first_event = rx.recv().unwrap();
+ sleep(Duration::from_millis(50));
+ let other_events = rx.try_iter();
+
+ let all_events = std::iter::once(first_event).chain(other_events);
+
+ let paths = all_events
+ .filter_map(|event| {
+ debug!("Received filesystem event: {:?}", event);
+
+ match event {
+ Create(path) | Write(path) | Remove(path) | Rename(_, path) => Some(path),
+ _ => None,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let paths = remove_ignored_files(&book.root, &paths[..]);
+
+ if !paths.is_empty() {
+ closure(paths, &book.root);
+ }
+ }
+}
diff --git a/vendor/mdbook/src/config.rs b/vendor/mdbook/src/config.rs
new file mode 100644
index 000000000..b7d03d1a2
--- /dev/null
+++ b/vendor/mdbook/src/config.rs
@@ -0,0 +1,1190 @@
+//! Mdbook's configuration system.
+//!
+//! The main entrypoint of the `config` module is the `Config` struct. This acts
+//! essentially as a bag of configuration information, with a couple
+//! pre-determined tables ([`BookConfig`] and [`BuildConfig`]) as well as support
+//! for arbitrary data which is exposed to plugins and alternative backends.
+//!
+//!
+//! # Examples
+//!
+//! ```rust
+//! # use mdbook::errors::*;
+//! use std::path::PathBuf;
+//! use std::str::FromStr;
+//! use mdbook::Config;
+//! use toml::Value;
+//!
+//! # fn run() -> Result<()> {
+//! let src = r#"
+//! [book]
+//! title = "My Book"
+//! authors = ["Michael-F-Bryan"]
+//!
+//! [build]
+//! src = "out"
+//!
+//! [other-table.foo]
+//! bar = 123
+//! "#;
+//!
+//! // load the `Config` from a toml string
+//! let mut cfg = Config::from_str(src)?;
+//!
+//! // retrieve a nested value
+//! let bar = cfg.get("other-table.foo.bar").cloned();
+//! assert_eq!(bar, Some(Value::Integer(123)));
+//!
+//! // Set the `output.html.theme` directory
+//! assert!(cfg.get("output.html").is_none());
+//! cfg.set("output.html.theme", "./themes");
+//!
+//! // then load it again, automatically deserializing to a `PathBuf`.
+//! let got: Option<PathBuf> = cfg.get_deserialized_opt("output.html.theme")?;
+//! assert_eq!(got, Some(PathBuf::from("./themes")));
+//! # Ok(())
+//! # }
+//! # run().unwrap()
+//! ```
+
+#![deny(missing_docs)]
+
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use std::collections::HashMap;
+use std::env;
+use std::fs::File;
+use std::io::Read;
+use std::path::{Path, PathBuf};
+use std::str::FromStr;
+use toml::value::Table;
+use toml::{self, Value};
+
+use crate::errors::*;
+use crate::utils::{self, toml_ext::TomlExt};
+
+/// The overall configuration object for MDBook, essentially an in-memory
+/// representation of `book.toml`.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Config {
+ /// Metadata about the book.
+ pub book: BookConfig,
+ /// Information about the build environment.
+ pub build: BuildConfig,
+ /// Information about Rust language support.
+ pub rust: RustConfig,
+ rest: Value,
+}
+
+impl FromStr for Config {
+ type Err = Error;
+
+ /// Load a `Config` from some string.
+ fn from_str(src: &str) -> Result<Self> {
+ toml::from_str(src).with_context(|| "Invalid configuration file")
+ }
+}
+
+impl Config {
+ /// Load the configuration file from disk.
+ pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
+ let mut buffer = String::new();
+ File::open(config_file)
+ .with_context(|| "Unable to open the configuration file")?
+ .read_to_string(&mut buffer)
+ .with_context(|| "Couldn't read the file")?;
+
+ Config::from_str(&buffer)
+ }
+
+ /// Updates the `Config` from the available environment variables.
+ ///
+ /// Variables starting with `MDBOOK_` are used for configuration. The key is
+ /// created by removing the `MDBOOK_` prefix and turning the resulting
+ /// string into `kebab-case`. Double underscores (`__`) separate nested
+ /// keys, while a single underscore (`_`) is replaced with a dash (`-`).
+ ///
+ /// For example:
+ ///
+ /// - `MDBOOK_foo` -> `foo`
+ /// - `MDBOOK_FOO` -> `foo`
+ /// - `MDBOOK_FOO__BAR` -> `foo.bar`
+ /// - `MDBOOK_FOO_BAR` -> `foo-bar`
+ /// - `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
+ ///
+ /// So by setting the `MDBOOK_BOOK__TITLE` environment variable you can
+ /// override the book's title without needing to touch your `book.toml`.
+ ///
+ /// > **Note:** To facilitate setting more complex config items, the value
+ /// > of an environment variable is first parsed as JSON, falling back to a
+ /// > string if the parse fails.
+ /// >
+ /// > This means, if you so desired, you could override all book metadata
+ /// > when building the book with something like
+ /// >
+ /// > ```text
+ /// > $ export MDBOOK_BOOK='{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}'
+ /// > $ mdbook build
+ /// > ```
+ ///
+ /// The latter case may be useful in situations where `mdbook` is invoked
+ /// from a script or CI, where it sometimes isn't possible to update the
+ /// `book.toml` before building.
+ pub fn update_from_env(&mut self) {
+ debug!("Updating the config from environment variables");
+
+ let overrides =
+ env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value)));
+
+ for (key, value) in overrides {
+ trace!("{} => {}", key, value);
+ let parsed_value = serde_json::from_str(&value)
+ .unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
+
+ if key == "book" || key == "build" {
+ if let serde_json::Value::Object(ref map) = parsed_value {
+ // To `set` each `key`, we wrap them as `prefix.key`
+ for (k, v) in map {
+ let full_key = format!("{}.{}", key, k);
+ self.set(&full_key, v).expect("unreachable");
+ }
+ return;
+ }
+ }
+
+ self.set(key, parsed_value).expect("unreachable");
+ }
+ }
+
+ /// Fetch an arbitrary item from the `Config` as a `toml::Value`.
+ ///
+ /// You can use dotted indices to access nested items (e.g.
+ /// `output.html.playground` will fetch the "playground" out of the html output
+ /// table).
+ pub fn get(&self, key: &str) -> Option<&Value> {
+ self.rest.read(key)
+ }
+
+ /// Fetch a value from the `Config` so you can mutate it.
+ pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
+ self.rest.read_mut(key)
+ }
+
+ /// Convenience method for getting the html renderer's configuration.
+ ///
+ /// # Note
+ ///
+ /// This is for compatibility only. It will be removed completely once the
+ /// HTML renderer is refactored to be less coupled to `mdbook` internals.
+ #[doc(hidden)]
+ pub fn html_config(&self) -> Option<HtmlConfig> {
+ match self
+ .get_deserialized_opt("output.html")
+ .with_context(|| "Parsing configuration [output.html]")
+ {
+ Ok(Some(config)) => Some(config),
+ Ok(None) => None,
+ Err(e) => {
+ utils::log_backtrace(&e);
+ None
+ }
+ }
+ }
+
+ /// Deprecated, use get_deserialized_opt instead.
+ #[deprecated = "use get_deserialized_opt instead"]
+ pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
+ let name = name.as_ref();
+ match self.get_deserialized_opt(name)? {
+ Some(value) => Ok(value),
+ None => bail!("Key not found, {:?}", name),
+ }
+ }
+
+ /// Convenience function to fetch a value from the config and deserialize it
+ /// into some arbitrary type.
+ pub fn get_deserialized_opt<'de, T: Deserialize<'de>, S: AsRef<str>>(
+ &self,
+ name: S,
+ ) -> Result<Option<T>> {
+ let name = name.as_ref();
+ self.get(name)
+ .map(|value| {
+ value
+ .clone()
+ .try_into()
+ .with_context(|| "Couldn't deserialize the value")
+ })
+ .transpose()
+ }
+
+ /// Set a config key, clobbering any existing values along the way.
+ ///
+ /// The only way this can fail is if we can't serialize `value` into a
+ /// `toml::Value`.
+ pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
+ let index = index.as_ref();
+
+ let value = Value::try_from(value)
+ .with_context(|| "Unable to represent the item as a JSON Value")?;
+
+ if let Some(key) = index.strip_prefix("book.") {
+ self.book.update_value(key, value);
+ } else if let Some(key) = index.strip_prefix("build.") {
+ self.build.update_value(key, value);
+ } else {
+ self.rest.insert(index, value);
+ }
+
+ Ok(())
+ }
+
+ /// Get the table associated with a particular renderer.
+ pub fn get_renderer<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
+ let key = format!("output.{}", index.as_ref());
+ self.get(&key).and_then(Value::as_table)
+ }
+
+ /// Get the table associated with a particular preprocessor.
+ pub fn get_preprocessor<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
+ let key = format!("preprocessor.{}", index.as_ref());
+ self.get(&key).and_then(Value::as_table)
+ }
+
+ fn from_legacy(mut table: Value) -> Config {
+ let mut cfg = Config::default();
+
+ // we use a macro here instead of a normal loop because the $out
+ // variable can be different types. This way we can make type inference
+ // figure out what try_into() deserializes to.
+ macro_rules! get_and_insert {
+ ($table:expr, $key:expr => $out:expr) => {
+ let got = $table
+ .as_table_mut()
+ .and_then(|t| t.remove($key))
+ .and_then(|v| v.try_into().ok());
+ if let Some(value) = got {
+ $out = value;
+ }
+ };
+ }
+
+ get_and_insert!(table, "title" => cfg.book.title);
+ get_and_insert!(table, "authors" => cfg.book.authors);
+ get_and_insert!(table, "source" => cfg.book.src);
+ get_and_insert!(table, "description" => cfg.book.description);
+
+ if let Some(dest) = table.delete("output.html.destination") {
+ if let Ok(destination) = dest.try_into() {
+ cfg.build.build_dir = destination;
+ }
+ }
+
+ cfg.rest = table;
+ cfg
+ }
+}
+
+impl Default for Config {
+ fn default() -> Config {
+ Config {
+ book: BookConfig::default(),
+ build: BuildConfig::default(),
+ rust: RustConfig::default(),
+ rest: Value::Table(Table::default()),
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for Config {
+ fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
+ let raw = Value::deserialize(de)?;
+
+ if is_legacy_format(&raw) {
+ warn!("It looks like you are using the legacy book.toml format.");
+ warn!("We'll parse it for now, but you should probably convert to the new format.");
+ warn!("See the mdbook documentation for more details, although as a rule of thumb");
+ warn!("just move all top level configuration entries like `title`, `author` and");
+ warn!("`description` under a table called `[book]`, move the `destination` entry");
+ warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
+ warn!("`[build]`, and it should all work.");
+ warn!("Documentation: http://rust-lang.github.io/mdBook/format/config.html");
+ return Ok(Config::from_legacy(raw));
+ }
+
+ use serde::de::Error;
+ let mut table = match raw {
+ Value::Table(t) => t,
+ _ => {
+ return Err(D::Error::custom(
+ "A config file should always be a toml table",
+ ));
+ }
+ };
+
+ let book: BookConfig = table
+ .remove("book")
+ .map(|book| book.try_into().map_err(D::Error::custom))
+ .transpose()?
+ .unwrap_or_default();
+
+ let build: BuildConfig = table
+ .remove("build")
+ .map(|build| build.try_into().map_err(D::Error::custom))
+ .transpose()?
+ .unwrap_or_default();
+
+ let rust: RustConfig = table
+ .remove("rust")
+ .map(|rust| rust.try_into().map_err(D::Error::custom))
+ .transpose()?
+ .unwrap_or_default();
+
+ Ok(Config {
+ book,
+ build,
+ rust,
+ rest: Value::Table(table),
+ })
+ }
+}
+
+impl Serialize for Config {
+ fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
+ // TODO: This should probably be removed and use a derive instead.
+ let mut table = self.rest.clone();
+
+ let book_config = Value::try_from(&self.book).expect("should always be serializable");
+ table.insert("book", book_config);
+
+ if self.build != BuildConfig::default() {
+ let build_config = Value::try_from(&self.build).expect("should always be serializable");
+ table.insert("build", build_config);
+ }
+
+ if self.rust != RustConfig::default() {
+ let rust_config = Value::try_from(&self.rust).expect("should always be serializable");
+ table.insert("rust", rust_config);
+ }
+
+ table.serialize(s)
+ }
+}
+
+fn parse_env(key: &str) -> Option<String> {
+ key.strip_prefix("MDBOOK_")
+ .map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
+}
+
+fn is_legacy_format(table: &Value) -> bool {
+ let legacy_items = [
+ "title",
+ "authors",
+ "source",
+ "description",
+ "output.html.destination",
+ ];
+
+ for item in &legacy_items {
+ if table.read(item).is_some() {
+ return true;
+ }
+ }
+
+ false
+}
+
+/// Configuration options which are specific to the book and required for
+/// loading it from disk.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct BookConfig {
+ /// The book's title.
+ pub title: Option<String>,
+ /// The book's authors.
+ pub authors: Vec<String>,
+ /// An optional description for the book.
+ pub description: Option<String>,
+ /// Location of the book source relative to the book's root directory.
+ pub src: PathBuf,
+ /// Does this book support more than one language?
+ pub multilingual: bool,
+ /// The main language of the book.
+ pub language: Option<String>,
+}
+
+impl Default for BookConfig {
+ fn default() -> BookConfig {
+ BookConfig {
+ title: None,
+ authors: Vec::new(),
+ description: None,
+ src: PathBuf::from("src"),
+ multilingual: false,
+ language: Some(String::from("en")),
+ }
+ }
+}
+
+/// Configuration for the build procedure.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct BuildConfig {
+ /// Where to put built artefacts relative to the book's root directory.
+ pub build_dir: PathBuf,
+ /// Should non-existent markdown files specified in `SUMMARY.md` be created
+ /// if they don't exist?
+ pub create_missing: bool,
+ /// Should the default preprocessors always be used when they are
+ /// compatible with the renderer?
+ pub use_default_preprocessors: bool,
+}
+
+impl Default for BuildConfig {
+ fn default() -> BuildConfig {
+ BuildConfig {
+ build_dir: PathBuf::from("book"),
+ create_missing: true,
+ use_default_preprocessors: true,
+ }
+ }
+}
+
+/// Configuration for the Rust compiler(e.g., for playground)
+#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct RustConfig {
+ /// Rust edition used in playground
+ pub edition: Option<RustEdition>,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
+/// Rust edition to use for the code.
+pub enum RustEdition {
+ /// The 2021 edition of Rust
+ #[serde(rename = "2021")]
+ E2021,
+ /// The 2018 edition of Rust
+ #[serde(rename = "2018")]
+ E2018,
+ /// The 2015 edition of Rust
+ #[serde(rename = "2015")]
+ E2015,
+}
+
+/// Configuration for the HTML renderer.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct HtmlConfig {
+ /// The theme directory, if specified.
+ pub theme: Option<PathBuf>,
+ /// The default theme to use, defaults to 'light'
+ pub default_theme: Option<String>,
+ /// The theme to use if the browser requests the dark version of the site.
+ /// Defaults to 'navy'.
+ pub preferred_dark_theme: Option<String>,
+ /// Use "smart quotes" instead of the usual `"` character.
+ pub curly_quotes: bool,
+ /// Should mathjax be enabled?
+ pub mathjax_support: bool,
+ /// Whether to fonts.css and respective font files to the output directory.
+ pub copy_fonts: bool,
+ /// An optional google analytics code.
+ pub google_analytics: Option<String>,
+ /// Additional CSS stylesheets to include in the rendered page's `<head>`.
+ pub additional_css: Vec<PathBuf>,
+ /// Additional JS scripts to include at the bottom of the rendered page's
+ /// `<body>`.
+ pub additional_js: Vec<PathBuf>,
+ /// Fold settings.
+ pub fold: Fold,
+ /// Playground settings.
+ #[serde(alias = "playpen")]
+ pub playground: Playground,
+ /// Print settings.
+ pub print: Print,
+ /// Don't render section labels.
+ pub no_section_label: bool,
+ /// Search settings. If `None`, the default will be used.
+ pub search: Option<Search>,
+ /// Git repository url. If `None`, the git button will not be shown.
+ pub git_repository_url: Option<String>,
+ /// FontAwesome icon class to use for the Git repository link.
+ /// Defaults to `fa-github` if `None`.
+ pub git_repository_icon: Option<String>,
+ /// Input path for the 404 file, defaults to 404.md, set to "" to disable 404 file output
+ pub input_404: Option<String>,
+ /// Absolute url to site, used to emit correct paths for the 404 page, which might be accessed in a deeply nested directory
+ pub site_url: Option<String>,
+ /// The DNS subdomain or apex domain at which your book will be hosted. This
+ /// string will be written to a file named CNAME in the root of your site,
+ /// as required by GitHub Pages (see [*Managing a custom domain for your
+ /// GitHub Pages site*][custom domain]).
+ ///
+ /// [custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
+ pub cname: Option<String>,
+ /// Edit url template, when set shows a "Suggest an edit" button for
+ /// directly jumping to editing the currently viewed page.
+ /// Contains {path} that is replaced with chapter source file path
+ pub edit_url_template: Option<String>,
+ /// Endpoint of websocket, for livereload usage. Value loaded from .toml file
+ /// is ignored, because our code overrides this field with the value [`LIVE_RELOAD_ENDPOINT`]
+ ///
+ /// [`LIVE_RELOAD_ENDPOINT`]: cmd::serve::LIVE_RELOAD_ENDPOINT
+ ///
+ /// This config item *should not be edited* by the end user.
+ #[doc(hidden)]
+ pub live_reload_endpoint: Option<String>,
+ /// The mapping from old pages to new pages/URLs to use when generating
+ /// redirects.
+ pub redirect: HashMap<String, String>,
+}
+
+impl Default for HtmlConfig {
+ fn default() -> HtmlConfig {
+ HtmlConfig {
+ theme: None,
+ default_theme: None,
+ preferred_dark_theme: None,
+ curly_quotes: false,
+ mathjax_support: false,
+ copy_fonts: true,
+ google_analytics: None,
+ additional_css: Vec::new(),
+ additional_js: Vec::new(),
+ fold: Fold::default(),
+ playground: Playground::default(),
+ print: Print::default(),
+ no_section_label: false,
+ search: None,
+ git_repository_url: None,
+ git_repository_icon: None,
+ edit_url_template: None,
+ input_404: None,
+ site_url: None,
+ cname: None,
+ live_reload_endpoint: None,
+ redirect: HashMap::new(),
+ }
+ }
+}
+
+impl HtmlConfig {
+ /// Returns the directory of theme from the provided root directory. If the
+ /// directory is not present it will append the default directory of "theme"
+ pub fn theme_dir(&self, root: &Path) -> PathBuf {
+ match self.theme {
+ Some(ref d) => root.join(d),
+ None => root.join("theme"),
+ }
+ }
+}
+
+/// Configuration for how to render the print icon, print.html, and print.css.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct Print {
+ /// Whether print support is enabled.
+ pub enable: bool,
+ /// Insert page breaks between chapters. Default: `true`.
+ pub page_break: bool,
+}
+
+impl Default for Print {
+ fn default() -> Self {
+ Self {
+ enable: true,
+ page_break: true,
+ }
+ }
+}
+
+/// Configuration for how to fold chapters of sidebar.
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct Fold {
+ /// When off, all folds are open. Default: `false`.
+ pub enable: bool,
+ /// The higher the more folded regions are open. When level is 0, all folds
+ /// are closed.
+ /// Default: `0`.
+ pub level: u8,
+}
+
+/// Configuration for tweaking how the the HTML renderer handles the playground.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct Playground {
+ /// Should playground snippets be editable? Default: `false`.
+ pub editable: bool,
+ /// Display the copy button. Default: `true`.
+ pub copyable: bool,
+ /// Copy JavaScript files for the editor to the output directory?
+ /// Default: `true`.
+ pub copy_js: bool,
+ /// Display line numbers on playground snippets. Default: `false`.
+ pub line_numbers: bool,
+ /// Display the run button. Default: `true`
+ pub runnable: bool,
+}
+
+impl Default for Playground {
+ fn default() -> Playground {
+ Playground {
+ editable: false,
+ copyable: true,
+ copy_js: true,
+ line_numbers: false,
+ runnable: true,
+ }
+ }
+}
+
+/// Configuration of the search functionality of the HTML renderer.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct Search {
+ /// Enable the search feature. Default: `true`.
+ pub enable: bool,
+ /// Maximum number of visible results. Default: `30`.
+ pub limit_results: u32,
+ /// The number of words used for a search result teaser. Default: `30`.
+ pub teaser_word_count: u32,
+ /// Define the logical link between multiple search words.
+ /// If true, all search words must appear in each result. Default: `false`.
+ pub use_boolean_and: bool,
+ /// Boost factor for the search result score if a search word appears in the header.
+ /// Default: `2`.
+ pub boost_title: u8,
+ /// Boost factor for the search result score if a search word appears in the hierarchy.
+ /// The hierarchy contains all titles of the parent documents and all parent headings.
+ /// Default: `1`.
+ pub boost_hierarchy: u8,
+ /// Boost factor for the search result score if a search word appears in the text.
+ /// Default: `1`.
+ pub boost_paragraph: u8,
+ /// True if the searchword `micro` should match `microwave`. Default: `true`.
+ pub expand: bool,
+ /// Documents are split into smaller parts, separated by headings. This defines, until which
+ /// level of heading documents should be split. Default: `3`. (`### This is a level 3 heading`)
+ pub heading_split_level: u8,
+ /// Copy JavaScript files for the search functionality to the output directory?
+ /// Default: `true`.
+ pub copy_js: bool,
+}
+
+impl Default for Search {
+ fn default() -> Search {
+ // Please update the documentation of `Search` when changing values!
+ Search {
+ enable: true,
+ limit_results: 30,
+ teaser_word_count: 30,
+ use_boolean_and: false,
+ boost_title: 2,
+ boost_hierarchy: 1,
+ boost_paragraph: 1,
+ expand: true,
+ heading_split_level: 3,
+ copy_js: true,
+ }
+ }
+}
+
+/// Allows you to "update" any arbitrary field in a struct by round-tripping via
+/// a `toml::Value`.
+///
+/// This is definitely not the most performant way to do things, which means you
+/// should probably keep it away from tight loops...
+trait Updateable<'de>: Serialize + Deserialize<'de> {
+ fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
+ let mut raw = Value::try_from(&self).expect("unreachable");
+
+ if let Ok(value) = Value::try_from(value) {
+ let _ = raw.insert(key, value);
+ } else {
+ return;
+ }
+
+ if let Ok(updated) = raw.try_into() {
+ *self = updated;
+ }
+ }
+}
+
+impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::utils::fs::get_404_output_file;
+
+ const COMPLEX_CONFIG: &str = r#"
+ [book]
+ title = "Some Book"
+ authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
+ description = "A completely useless book"
+ multilingual = true
+ src = "source"
+ language = "ja"
+
+ [build]
+ build-dir = "outputs"
+ create-missing = false
+ use-default-preprocessors = true
+
+ [output.html]
+ theme = "./themedir"
+ default-theme = "rust"
+ curly-quotes = true
+ google-analytics = "123456"
+ additional-css = ["./foo/bar/baz.css"]
+ git-repository-url = "https://foo.com/"
+ git-repository-icon = "fa-code-fork"
+
+ [output.html.playground]
+ editable = true
+ editor = "ace"
+
+ [output.html.redirect]
+ "index.html" = "overview.html"
+ "nexted/page.md" = "https://rust-lang.org/"
+
+ [preprocessor.first]
+
+ [preprocessor.second]
+ "#;
+
+ #[test]
+ fn load_a_complex_config_file() {
+ let src = COMPLEX_CONFIG;
+
+ let book_should_be = BookConfig {
+ title: Some(String::from("Some Book")),
+ authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
+ description: Some(String::from("A completely useless book")),
+ multilingual: true,
+ src: PathBuf::from("source"),
+ language: Some(String::from("ja")),
+ };
+ let build_should_be = BuildConfig {
+ build_dir: PathBuf::from("outputs"),
+ create_missing: false,
+ use_default_preprocessors: true,
+ };
+ let rust_should_be = RustConfig { edition: None };
+ let playground_should_be = Playground {
+ editable: true,
+ copyable: true,
+ copy_js: true,
+ line_numbers: false,
+ runnable: true,
+ };
+ let html_should_be = HtmlConfig {
+ curly_quotes: true,
+ google_analytics: Some(String::from("123456")),
+ additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
+ theme: Some(PathBuf::from("./themedir")),
+ default_theme: Some(String::from("rust")),
+ playground: playground_should_be,
+ git_repository_url: Some(String::from("https://foo.com/")),
+ git_repository_icon: Some(String::from("fa-code-fork")),
+ redirect: vec![
+ (String::from("index.html"), String::from("overview.html")),
+ (
+ String::from("nexted/page.md"),
+ String::from("https://rust-lang.org/"),
+ ),
+ ]
+ .into_iter()
+ .collect(),
+ ..Default::default()
+ };
+
+ let got = Config::from_str(src).unwrap();
+
+ assert_eq!(got.book, book_should_be);
+ assert_eq!(got.build, build_should_be);
+ assert_eq!(got.rust, rust_should_be);
+ assert_eq!(got.html_config().unwrap(), html_should_be);
+ }
+
+ #[test]
+ fn disable_runnable() {
+ let src = r#"
+ [book]
+ title = "Some Book"
+ description = "book book book"
+ authors = ["Shogo Takata"]
+
+ [output.html.playground]
+ runnable = false
+ "#;
+
+ let got = Config::from_str(src).unwrap();
+ assert!(!got.html_config().unwrap().playground.runnable);
+ }
+
+ #[test]
+ fn edition_2015() {
+ let src = r#"
+ [book]
+ title = "mdBook Documentation"
+ description = "Create book from markdown files. Like Gitbook but implemented in Rust"
+ authors = ["Mathieu David"]
+ src = "./source"
+ [rust]
+ edition = "2015"
+ "#;
+
+ let book_should_be = BookConfig {
+ title: Some(String::from("mdBook Documentation")),
+ description: Some(String::from(
+ "Create book from markdown files. Like Gitbook but implemented in Rust",
+ )),
+ authors: vec![String::from("Mathieu David")],
+ src: PathBuf::from("./source"),
+ ..Default::default()
+ };
+
+ let got = Config::from_str(src).unwrap();
+ assert_eq!(got.book, book_should_be);
+
+ let rust_should_be = RustConfig {
+ edition: Some(RustEdition::E2015),
+ };
+ let got = Config::from_str(src).unwrap();
+ assert_eq!(got.rust, rust_should_be);
+ }
+
+ #[test]
+ fn edition_2018() {
+ let src = r#"
+ [book]
+ title = "mdBook Documentation"
+ description = "Create book from markdown files. Like Gitbook but implemented in Rust"
+ authors = ["Mathieu David"]
+ src = "./source"
+ [rust]
+ edition = "2018"
+ "#;
+
+ let rust_should_be = RustConfig {
+ edition: Some(RustEdition::E2018),
+ };
+
+ let got = Config::from_str(src).unwrap();
+ assert_eq!(got.rust, rust_should_be);
+ }
+
+ #[test]
+ fn edition_2021() {
+ let src = r#"
+ [book]
+ title = "mdBook Documentation"
+ description = "Create book from markdown files. Like Gitbook but implemented in Rust"
+ authors = ["Mathieu David"]
+ src = "./source"
+ [rust]
+ edition = "2021"
+ "#;
+
+ let rust_should_be = RustConfig {
+ edition: Some(RustEdition::E2021),
+ };
+
+ let got = Config::from_str(src).unwrap();
+ assert_eq!(got.rust, rust_should_be);
+ }
+
+ #[test]
+ fn load_arbitrary_output_type() {
+ #[derive(Debug, Deserialize, PartialEq)]
+ struct RandomOutput {
+ foo: u32,
+ bar: String,
+ baz: Vec<bool>,
+ }
+
+ let src = r#"
+ [output.random]
+ foo = 5
+ bar = "Hello World"
+ baz = [true, true, false]
+ "#;
+
+ let should_be = RandomOutput {
+ foo: 5,
+ bar: String::from("Hello World"),
+ baz: vec![true, true, false],
+ };
+
+ let cfg = Config::from_str(src).unwrap();
+ let got: RandomOutput = cfg.get_deserialized_opt("output.random").unwrap().unwrap();
+
+ assert_eq!(got, should_be);
+
+ let got_baz: Vec<bool> = cfg
+ .get_deserialized_opt("output.random.baz")
+ .unwrap()
+ .unwrap();
+ let baz_should_be = vec![true, true, false];
+
+ assert_eq!(got_baz, baz_should_be);
+ }
+
+ #[test]
+ fn mutate_some_stuff() {
+ // really this is just a sanity check to make sure the borrow checker
+ // is happy...
+ let src = COMPLEX_CONFIG;
+ let mut config = Config::from_str(src).unwrap();
+ let key = "output.html.playground.editable";
+
+ assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
+ *config.get_mut(key).unwrap() = Value::Boolean(false);
+ assert_eq!(config.get(key).unwrap(), &Value::Boolean(false));
+ }
+
+ /// The config file format has slightly changed (metadata stuff is now under
+ /// the `book` table instead of being at the top level) so we're adding a
+ /// **temporary** compatibility check. You should be able to still load the
+ /// old format, emitting a warning.
+ #[test]
+ fn can_still_load_the_previous_format() {
+ let src = r#"
+ title = "mdBook Documentation"
+ description = "Create book from markdown files. Like Gitbook but implemented in Rust"
+ authors = ["Mathieu David"]
+ source = "./source"
+
+ [output.html]
+ destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
+ theme = "my-theme"
+ curly-quotes = true
+ google-analytics = "123456"
+ additional-css = ["custom.css", "custom2.css"]
+ additional-js = ["custom.js"]
+ "#;
+
+ let book_should_be = BookConfig {
+ title: Some(String::from("mdBook Documentation")),
+ description: Some(String::from(
+ "Create book from markdown files. Like Gitbook but implemented in Rust",
+ )),
+ authors: vec![String::from("Mathieu David")],
+ src: PathBuf::from("./source"),
+ ..Default::default()
+ };
+
+ let build_should_be = BuildConfig {
+ build_dir: PathBuf::from("my-book"),
+ create_missing: true,
+ use_default_preprocessors: true,
+ };
+
+ let html_should_be = HtmlConfig {
+ theme: Some(PathBuf::from("my-theme")),
+ curly_quotes: true,
+ google_analytics: Some(String::from("123456")),
+ additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
+ additional_js: vec![PathBuf::from("custom.js")],
+ ..Default::default()
+ };
+
+ let got = Config::from_str(src).unwrap();
+ assert_eq!(got.book, book_should_be);
+ assert_eq!(got.build, build_should_be);
+ assert_eq!(got.html_config().unwrap(), html_should_be);
+ }
+
+ #[test]
+ fn set_a_config_item() {
+ let mut cfg = Config::default();
+ let key = "foo.bar.baz";
+ let value = "Something Interesting";
+
+ assert!(cfg.get(key).is_none());
+ cfg.set(key, value).unwrap();
+
+ let got: String = cfg.get_deserialized_opt(key).unwrap().unwrap();
+ assert_eq!(got, value);
+ }
+
+ #[test]
+ fn parse_env_vars() {
+ let inputs = vec![
+ ("FOO", None),
+ ("MDBOOK_foo", Some("foo")),
+ ("MDBOOK_FOO__bar__baz", Some("foo.bar.baz")),
+ ("MDBOOK_FOO_bar__baz", Some("foo-bar.baz")),
+ ];
+
+ for (src, should_be) in inputs {
+ let got = parse_env(src);
+ let should_be = should_be.map(ToString::to_string);
+
+ assert_eq!(got, should_be);
+ }
+ }
+
+ fn encode_env_var(key: &str) -> String {
+ format!(
+ "MDBOOK_{}",
+ key.to_uppercase().replace('.', "__").replace('-', "_")
+ )
+ }
+
+ #[test]
+ fn update_config_using_env_var() {
+ let mut cfg = Config::default();
+ let key = "foo.bar";
+ let value = "baz";
+
+ assert!(cfg.get(key).is_none());
+
+ let encoded_key = encode_env_var(key);
+ env::set_var(encoded_key, value);
+
+ cfg.update_from_env();
+
+ assert_eq!(
+ cfg.get_deserialized_opt::<String, _>(key).unwrap().unwrap(),
+ value
+ );
+ }
+
+ #[test]
+ fn update_config_using_env_var_and_complex_value() {
+ let mut cfg = Config::default();
+ let key = "foo-bar.baz";
+ let value = json!({"array": [1, 2, 3], "number": 13.37});
+ let value_str = serde_json::to_string(&value).unwrap();
+
+ assert!(cfg.get(key).is_none());
+
+ let encoded_key = encode_env_var(key);
+ env::set_var(encoded_key, value_str);
+
+ cfg.update_from_env();
+
+ assert_eq!(
+ cfg.get_deserialized_opt::<serde_json::Value, _>(key)
+ .unwrap()
+ .unwrap(),
+ value
+ );
+ }
+
+ #[test]
+ fn update_book_title_via_env() {
+ let mut cfg = Config::default();
+ let should_be = "Something else".to_string();
+
+ assert_ne!(cfg.book.title, Some(should_be.clone()));
+
+ env::set_var("MDBOOK_BOOK__TITLE", &should_be);
+ cfg.update_from_env();
+
+ assert_eq!(cfg.book.title, Some(should_be));
+ }
+
+ #[test]
+ fn file_404_default() {
+ let src = r#"
+ [output.html]
+ destination = "my-book"
+ "#;
+
+ let got = Config::from_str(src).unwrap();
+ let html_config = got.html_config().unwrap();
+ assert_eq!(html_config.input_404, None);
+ assert_eq!(&get_404_output_file(&html_config.input_404), "404.html");
+ }
+
+ #[test]
+ fn file_404_custom() {
+ let src = r#"
+ [output.html]
+ input-404= "missing.md"
+ output-404= "missing.html"
+ "#;
+
+ let got = Config::from_str(src).unwrap();
+ let html_config = got.html_config().unwrap();
+ assert_eq!(html_config.input_404, Some("missing.md".to_string()));
+ assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html");
+ }
+
+ #[test]
+ #[should_panic(expected = "Invalid configuration file")]
+ fn invalid_language_type_error() {
+ let src = r#"
+ [book]
+ title = "mdBook Documentation"
+ language = ["en", "pt-br"]
+ description = "Create book from markdown files. Like Gitbook but implemented in Rust"
+ authors = ["Mathieu David"]
+ src = "./source"
+ "#;
+
+ Config::from_str(src).unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "Invalid configuration file")]
+ fn invalid_title_type() {
+ let src = r#"
+ [book]
+ title = 20
+ language = "en"
+ description = "Create book from markdown files. Like Gitbook but implemented in Rust"
+ authors = ["Mathieu David"]
+ src = "./source"
+ "#;
+
+ Config::from_str(src).unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "Invalid configuration file")]
+ fn invalid_build_dir_type() {
+ let src = r#"
+ [build]
+ build-dir = 99
+ create-missing = false
+ "#;
+
+ Config::from_str(src).unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "Invalid configuration file")]
+ fn invalid_rust_edition() {
+ let src = r#"
+ [rust]
+ edition = "1999"
+ "#;
+
+ Config::from_str(src).unwrap();
+ }
+
+ #[test]
+ fn print_config() {
+ let src = r#"
+ [output.html.print]
+ enable = false
+ "#;
+ let got = Config::from_str(src).unwrap();
+ let html_config = got.html_config().unwrap();
+ assert!(!html_config.print.enable);
+ assert!(html_config.print.page_break);
+ let src = r#"
+ [output.html.print]
+ page-break = false
+ "#;
+ let got = Config::from_str(src).unwrap();
+ let html_config = got.html_config().unwrap();
+ assert!(html_config.print.enable);
+ assert!(!html_config.print.page_break);
+ }
+}
diff --git a/vendor/mdbook/src/lib.rs b/vendor/mdbook/src/lib.rs
new file mode 100644
index 000000000..cc62b0abd
--- /dev/null
+++ b/vendor/mdbook/src/lib.rs
@@ -0,0 +1,119 @@
+//! # mdBook
+//!
+//! **mdBook** is a tool for rendering a collection of markdown documents into
+//! a form more suitable for end users like HTML or EPUB. It offers a command
+//! line interface, but this crate can be used if more control is required.
+//!
+//! This is the API doc, the [user guide] is also available if you want
+//! information about the command line tool, format, structure etc. It is also
+//! rendered with mdBook to showcase the features and default theme.
+//!
+//! Some reasons why you would want to use the crate (over the cli):
+//!
+//! - Integrate mdbook in a current project
+//! - Extend the capabilities of mdBook
+//! - Do some processing or test before building your book
+//! - Accessing the public API to help create a new Renderer
+//! - ...
+//!
+//! > **Note:** While we try to ensure `mdbook`'s command-line interface and
+//! > behaviour are backwards compatible, the tool's internals are still
+//! > evolving and being iterated on. If you wish to prevent accidental
+//! > breakages it is recommended to pin any tools building on top of the
+//! > `mdbook` crate to a specific release.
+//!
+//! # Examples
+//!
+//! If creating a new book from scratch, you'll want to get a `BookBuilder` via
+//! the `MDBook::init()` method.
+//!
+//! ```rust,no_run
+//! use mdbook::MDBook;
+//! use mdbook::config::Config;
+//!
+//! let root_dir = "/path/to/book/root";
+//!
+//! // create a default config and change a couple things
+//! let mut cfg = Config::default();
+//! cfg.book.title = Some("My Book".to_string());
+//! cfg.book.authors.push("Michael-F-Bryan".to_string());
+//!
+//! MDBook::init(root_dir)
+//! .create_gitignore(true)
+//! .with_config(cfg)
+//! .build()
+//! .expect("Book generation failed");
+//! ```
+//!
+//! You can also load an existing book and build it.
+//!
+//! ```rust,no_run
+//! use mdbook::MDBook;
+//!
+//! let root_dir = "/path/to/book/root";
+//!
+//! let mut md = MDBook::load(root_dir)
+//! .expect("Unable to load the book");
+//! md.build().expect("Building failed");
+//! ```
+//!
+//! ## Implementing a new Backend
+//!
+//! `mdbook` has a fairly flexible mechanism for creating additional backends
+//! for your book. The general idea is you'll add an extra table in the book's
+//! `book.toml` which specifies an executable to be invoked by `mdbook`. This
+//! executable will then be called during a build, with an in-memory
+//! representation ([`RenderContext`]) of the book being passed to the
+//! subprocess via `stdin`.
+//!
+//! The [`RenderContext`] gives the backend access to the contents of
+//! `book.toml` and lets it know which directory all generated artefacts should
+//! be placed in. For a much more in-depth explanation, consult the [relevant
+//! chapter] in the *For Developers* section of the user guide.
+//!
+//! To make creating a backend easier, the `mdbook` crate can be imported
+//! directly, making deserializing the `RenderContext` easy and giving you
+//! access to the various methods for working with the [`Config`].
+//!
+//! [user guide]: https://rust-lang.github.io/mdBook/
+//! [`RenderContext`]: renderer::RenderContext
+//! [relevant chapter]: https://rust-lang.github.io/mdBook/for_developers/backends.html
+//! [`Config`]: config::Config
+
+#![deny(missing_docs)]
+#![deny(rust_2018_idioms)]
+
+#[macro_use]
+extern crate lazy_static;
+#[macro_use]
+extern crate log;
+#[macro_use]
+extern crate serde_json;
+
+#[cfg(test)]
+#[macro_use]
+extern crate pretty_assertions;
+
+pub mod book;
+pub mod config;
+pub mod preprocess;
+pub mod renderer;
+pub mod theme;
+pub mod utils;
+
+/// The current version of `mdbook`.
+///
+/// This is provided as a way for custom preprocessors and renderers to do
+/// compatibility checks.
+pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+pub use crate::book::BookItem;
+pub use crate::book::MDBook;
+pub use crate::config::Config;
+pub use crate::renderer::Renderer;
+
+/// The error types used through out this crate.
+pub mod errors {
+ pub(crate) use anyhow::{bail, ensure, Context};
+ pub use anyhow::{Error, Result};
+}
diff --git a/vendor/mdbook/src/main.rs b/vendor/mdbook/src/main.rs
new file mode 100644
index 000000000..35562e64b
--- /dev/null
+++ b/vendor/mdbook/src/main.rs
@@ -0,0 +1,150 @@
+#[macro_use]
+extern crate clap;
+#[macro_use]
+extern crate log;
+
+use anyhow::anyhow;
+use chrono::Local;
+use clap::{App, AppSettings, Arg, ArgMatches};
+use clap_complete::Shell;
+use env_logger::Builder;
+use log::LevelFilter;
+use mdbook::utils;
+use std::env;
+use std::ffi::OsStr;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+
+mod cmd;
+
+const VERSION: &str = concat!("v", crate_version!());
+
+fn main() {
+ init_logger();
+
+ let app = create_clap_app();
+
+ // Check which subcomamnd the user ran...
+ let res = match app.get_matches().subcommand() {
+ Some(("init", sub_matches)) => cmd::init::execute(sub_matches),
+ Some(("build", sub_matches)) => cmd::build::execute(sub_matches),
+ Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches),
+ #[cfg(feature = "watch")]
+ Some(("watch", sub_matches)) => cmd::watch::execute(sub_matches),
+ #[cfg(feature = "serve")]
+ Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches),
+ Some(("test", sub_matches)) => cmd::test::execute(sub_matches),
+ Some(("completions", sub_matches)) => (|| {
+ let shell: Shell = sub_matches
+ .value_of("shell")
+ .ok_or_else(|| anyhow!("Shell name missing."))?
+ .parse()
+ .map_err(|s| anyhow!("Invalid shell: {}", s))?;
+
+ let mut complete_app = create_clap_app();
+ clap_complete::generate(
+ shell,
+ &mut complete_app,
+ "mdbook",
+ &mut std::io::stdout().lock(),
+ );
+ Ok(())
+ })(),
+ _ => unreachable!(),
+ };
+
+ if let Err(e) = res {
+ utils::log_backtrace(&e);
+
+ std::process::exit(101);
+ }
+}
+
+/// Create a list of valid arguments and sub-commands
+fn create_clap_app() -> App<'static> {
+ let app = App::new(crate_name!())
+ .about(crate_description!())
+ .author("Mathieu David <mathieudavid@mathieudavid.org>")
+ .version(VERSION)
+ .setting(AppSettings::PropagateVersion)
+ .setting(AppSettings::ArgRequiredElseHelp)
+ .after_help(
+ "For more information about a specific command, try `mdbook <command> --help`\n\
+ The source code for mdBook is available at: https://github.com/rust-lang/mdBook",
+ )
+ .subcommand(cmd::init::make_subcommand())
+ .subcommand(cmd::build::make_subcommand())
+ .subcommand(cmd::test::make_subcommand())
+ .subcommand(cmd::clean::make_subcommand())
+ .subcommand(
+ App::new("completions")
+ .about("Generate shell completions for your shell to stdout")
+ .arg(
+ Arg::new("shell")
+ .takes_value(true)
+ .possible_values(Shell::possible_values())
+ .help("the shell to generate completions for")
+ .value_name("SHELL")
+ .required(true),
+ ),
+ );
+
+ #[cfg(feature = "watch")]
+ let app = app.subcommand(cmd::watch::make_subcommand());
+ #[cfg(feature = "serve")]
+ let app = app.subcommand(cmd::serve::make_subcommand());
+
+ app
+}
+
+fn init_logger() {
+ let mut builder = Builder::new();
+
+ builder.format(|formatter, record| {
+ writeln!(
+ formatter,
+ "{} [{}] ({}): {}",
+ Local::now().format("%Y-%m-%d %H:%M:%S"),
+ record.level(),
+ record.target(),
+ record.args()
+ )
+ });
+
+ if let Ok(var) = env::var("RUST_LOG") {
+ builder.parse_filters(&var);
+ } else {
+ // if no RUST_LOG provided, default to logging at the Info level
+ builder.filter(None, LevelFilter::Info);
+ // Filter extraneous html5ever not-implemented messages
+ builder.filter(Some("html5ever"), LevelFilter::Error);
+ }
+
+ builder.init();
+}
+
+fn get_book_dir(args: &ArgMatches) -> PathBuf {
+ if let Some(dir) = args.value_of("dir") {
+ // Check if path is relative from current dir, or absolute...
+ let p = Path::new(dir);
+ if p.is_relative() {
+ env::current_dir().unwrap().join(dir)
+ } else {
+ p.to_path_buf()
+ }
+ } else {
+ env::current_dir().expect("Unable to determine the current directory")
+ }
+}
+
+fn open<P: AsRef<OsStr>>(path: P) {
+ info!("Opening web browser");
+ if let Err(e) = opener::open(path) {
+ error!("Error opening web browser: {}", e);
+ }
+}
+
+#[test]
+fn verify_app() {
+ create_clap_app().debug_assert();
+}
diff --git a/vendor/mdbook/src/preprocess/cmd.rs b/vendor/mdbook/src/preprocess/cmd.rs
new file mode 100644
index 000000000..c47fd5d22
--- /dev/null
+++ b/vendor/mdbook/src/preprocess/cmd.rs
@@ -0,0 +1,207 @@
+use super::{Preprocessor, PreprocessorContext};
+use crate::book::Book;
+use crate::errors::*;
+use shlex::Shlex;
+use std::io::{self, Read, Write};
+use std::process::{Child, Command, Stdio};
+
+/// A custom preprocessor which will shell out to a 3rd-party program.
+///
+/// # Preprocessing Protocol
+///
+/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
+/// execute the shell command `$cmd supports $renderer`. If the renderer is
+/// supported, custom preprocessors should exit with a exit code of `0`,
+/// any other exit code be considered as unsupported.
+///
+/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
+/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
+/// should then "return" a processed book by printing it to `stdout` as JSON.
+/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
+/// to parse the input provided by `mdbook`.
+///
+/// Exiting with a non-zero exit code while preprocessing is considered an
+/// error. `stderr` is passed directly through to the user, so it can be used
+/// for logging or emitting warnings if desired.
+///
+/// # Examples
+///
+/// An example preprocessor is available in this project's `examples/`
+/// directory.
+#[derive(Debug, Clone, PartialEq)]
+pub struct CmdPreprocessor {
+ name: String,
+ cmd: String,
+}
+
+impl CmdPreprocessor {
+ /// Create a new `CmdPreprocessor`.
+ pub fn new(name: String, cmd: String) -> CmdPreprocessor {
+ CmdPreprocessor { name, cmd }
+ }
+
+ /// A convenience function custom preprocessors can use to parse the input
+ /// written to `stdin` by a `CmdRenderer`.
+ pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
+ serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
+ }
+
+ fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
+ let stdin = child.stdin.take().expect("Child has stdin");
+
+ if let Err(e) = self.write_input(stdin, book, ctx) {
+ // Looks like the backend hung up before we could finish
+ // sending it the render context. Log the error and keep going
+ warn!("Error writing the RenderContext to the backend, {}", e);
+ }
+ }
+
+ fn write_input<W: Write>(
+ &self,
+ writer: W,
+ book: &Book,
+ ctx: &PreprocessorContext,
+ ) -> Result<()> {
+ serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
+ }
+
+ /// The command this `Preprocessor` will invoke.
+ pub fn cmd(&self) -> &str {
+ &self.cmd
+ }
+
+ fn command(&self) -> Result<Command> {
+ let mut words = Shlex::new(&self.cmd);
+ let executable = match words.next() {
+ Some(e) => e,
+ None => bail!("Command string was empty"),
+ };
+
+ let mut cmd = Command::new(executable);
+
+ for arg in words {
+ cmd.arg(arg);
+ }
+
+ Ok(cmd)
+ }
+}
+
+impl Preprocessor for CmdPreprocessor {
+ fn name(&self) -> &str {
+ &self.name
+ }
+
+ fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
+ let mut cmd = self.command()?;
+
+ let mut child = cmd
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::inherit())
+ .spawn()
+ .with_context(|| {
+ format!(
+ "Unable to start the \"{}\" preprocessor. Is it installed?",
+ self.name()
+ )
+ })?;
+
+ self.write_input_to_child(&mut child, &book, ctx);
+
+ let output = child.wait_with_output().with_context(|| {
+ format!(
+ "Error waiting for the \"{}\" preprocessor to complete",
+ self.name
+ )
+ })?;
+
+ trace!("{} exited with output: {:?}", self.cmd, output);
+ ensure!(
+ output.status.success(),
+ format!(
+ "The \"{}\" preprocessor exited unsuccessfully with {} status",
+ self.name, output.status
+ )
+ );
+
+ serde_json::from_slice(&output.stdout).with_context(|| {
+ format!(
+ "Unable to parse the preprocessed book from \"{}\" processor",
+ self.name
+ )
+ })
+ }
+
+ fn supports_renderer(&self, renderer: &str) -> bool {
+ debug!(
+ "Checking if the \"{}\" preprocessor supports \"{}\"",
+ self.name(),
+ renderer
+ );
+
+ let mut cmd = match self.command() {
+ Ok(c) => c,
+ Err(e) => {
+ warn!(
+ "Unable to create the command for the \"{}\" preprocessor, {}",
+ self.name(),
+ e
+ );
+ return false;
+ }
+ };
+
+ let outcome = cmd
+ .arg("supports")
+ .arg(renderer)
+ .stdin(Stdio::null())
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit())
+ .status()
+ .map(|status| status.code() == Some(0));
+
+ if let Err(ref e) = outcome {
+ if e.kind() == io::ErrorKind::NotFound {
+ warn!(
+ "The command wasn't found, is the \"{}\" preprocessor installed?",
+ self.name
+ );
+ warn!("\tCommand: {}", self.cmd);
+ }
+ }
+
+ outcome.unwrap_or(false)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::MDBook;
+ use std::path::Path;
+
+ fn guide() -> MDBook {
+ let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
+ MDBook::load(example).unwrap()
+ }
+
+ #[test]
+ fn round_trip_write_and_parse_input() {
+ let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
+ let md = guide();
+ let ctx = PreprocessorContext::new(
+ md.root.clone(),
+ md.config.clone(),
+ "some-renderer".to_string(),
+ );
+
+ let mut buffer = Vec::new();
+ cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
+
+ let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
+
+ assert_eq!(got_book, md.book);
+ assert_eq!(got_ctx, ctx);
+ }
+}
diff --git a/vendor/mdbook/src/preprocess/index.rs b/vendor/mdbook/src/preprocess/index.rs
new file mode 100644
index 000000000..fd60ad4da
--- /dev/null
+++ b/vendor/mdbook/src/preprocess/index.rs
@@ -0,0 +1,105 @@
+use regex::Regex;
+use std::path::Path;
+
+use crate::errors::*;
+
+use super::{Preprocessor, PreprocessorContext};
+use crate::book::{Book, BookItem};
+
+/// A preprocessor for converting file name `README.md` to `index.md` since
+/// `README.md` is the de facto index file in markdown-based documentation.
+#[derive(Default)]
+pub struct IndexPreprocessor;
+
+impl IndexPreprocessor {
+ pub(crate) const NAME: &'static str = "index";
+
+ /// Create a new `IndexPreprocessor`.
+ pub fn new() -> Self {
+ IndexPreprocessor
+ }
+}
+
+impl Preprocessor for IndexPreprocessor {
+ fn name(&self) -> &str {
+ Self::NAME
+ }
+
+ fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
+ let source_dir = ctx.root.join(&ctx.config.book.src);
+ book.for_each_mut(|section: &mut BookItem| {
+ if let BookItem::Chapter(ref mut ch) = *section {
+ if let Some(ref mut path) = ch.path {
+ if is_readme_file(&path) {
+ let mut index_md = source_dir.join(path.with_file_name("index.md"));
+ if index_md.exists() {
+ warn_readme_name_conflict(&path, &&mut index_md);
+ }
+
+ path.set_file_name("index.md");
+ }
+ }
+ }
+ });
+
+ Ok(book)
+ }
+}
+
+fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
+ let file_name = readme_path.as_ref().file_name().unwrap_or_default();
+ let parent_dir = index_path
+ .as_ref()
+ .parent()
+ .unwrap_or_else(|| index_path.as_ref());
+ warn!(
+ "It seems that there are both {:?} and index.md under \"{}\".",
+ file_name,
+ parent_dir.display()
+ );
+ warn!(
+ "mdbook converts {:?} into index.html by default. It may cause",
+ file_name
+ );
+ warn!("unexpected behavior if putting both files under the same directory.");
+ warn!("To solve the warning, try to rearrange the book structure or disable");
+ warn!("\"index\" preprocessor to stop the conversion.");
+}
+
+fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
+ lazy_static! {
+ static ref RE: Regex = Regex::new(r"(?i)^readme$").unwrap();
+ }
+ RE.is_match(
+ path.as_ref()
+ .file_stem()
+ .and_then(std::ffi::OsStr::to_str)
+ .unwrap_or_default(),
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn file_stem_exactly_matches_readme_case_insensitively() {
+ let path = "path/to/Readme.md";
+ assert!(is_readme_file(path));
+
+ let path = "path/to/README.md";
+ assert!(is_readme_file(path));
+
+ let path = "path/to/rEaDmE.md";
+ assert!(is_readme_file(path));
+
+ let path = "path/to/README.markdown";
+ assert!(is_readme_file(path));
+
+ let path = "path/to/README";
+ assert!(is_readme_file(path));
+
+ let path = "path/to/README-README.md";
+ assert!(!is_readme_file(path));
+ }
+}
diff --git a/vendor/mdbook/src/preprocess/links.rs b/vendor/mdbook/src/preprocess/links.rs
new file mode 100644
index 000000000..7ca6fd345
--- /dev/null
+++ b/vendor/mdbook/src/preprocess/links.rs
@@ -0,0 +1,937 @@
+use crate::errors::*;
+use crate::utils::{
+ take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
+ take_rustdoc_include_lines,
+};
+use regex::{CaptureMatches, Captures, Regex};
+use std::fs;
+use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
+use std::path::{Path, PathBuf};
+
+use super::{Preprocessor, PreprocessorContext};
+use crate::book::{Book, BookItem};
+
+const ESCAPE_CHAR: char = '\\';
+const MAX_LINK_NESTED_DEPTH: usize = 10;
+
+/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
+///
+/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
+///. lines, or only between the specified anchors.
+/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
+///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
+/// This hides the lines from initial display but shows them when the reader expands the code
+/// block and provides them to Rustdoc for testing.
+/// - `{{# playground}}` - Insert runnable Rust files
+/// - `{{# title}}` - Override \<title\> of a webpage.
+#[derive(Default)]
+pub struct LinkPreprocessor;
+
+impl LinkPreprocessor {
+ pub(crate) const NAME: &'static str = "links";
+
+ /// Create a new `LinkPreprocessor`.
+ pub fn new() -> Self {
+ LinkPreprocessor
+ }
+}
+
+impl Preprocessor for LinkPreprocessor {
+ fn name(&self) -> &str {
+ Self::NAME
+ }
+
+ fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
+ let src_dir = ctx.root.join(&ctx.config.book.src);
+
+ book.for_each_mut(|section: &mut BookItem| {
+ if let BookItem::Chapter(ref mut ch) = *section {
+ if let Some(ref chapter_path) = ch.path {
+ let base = chapter_path
+ .parent()
+ .map(|dir| src_dir.join(dir))
+ .expect("All book items have a parent");
+
+ let mut chapter_title = ch.name.clone();
+ let content =
+ replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title);
+ ch.content = content;
+ if chapter_title != ch.name {
+ ctx.chapter_titles
+ .borrow_mut()
+ .insert(chapter_path.clone(), chapter_title);
+ }
+ }
+ }
+ });
+
+ Ok(book)
+ }
+}
+
+fn replace_all<P1, P2>(
+ s: &str,
+ path: P1,
+ source: P2,
+ depth: usize,
+ chapter_title: &mut String,
+) -> String
+where
+ P1: AsRef<Path>,
+ P2: AsRef<Path>,
+{
+ // When replacing one thing in a string by something with a different length,
+ // the indices after that will not correspond,
+ // we therefore have to store the difference to correct this
+ let path = path.as_ref();
+ let source = source.as_ref();
+ let mut previous_end_index = 0;
+ let mut replaced = String::new();
+
+ for link in find_links(s) {
+ replaced.push_str(&s[previous_end_index..link.start_index]);
+
+ match link.render_with_path(&path, chapter_title) {
+ Ok(new_content) => {
+ if depth < MAX_LINK_NESTED_DEPTH {
+ if let Some(rel_path) = link.link_type.relative_path(path) {
+ replaced.push_str(&replace_all(
+ &new_content,
+ rel_path,
+ source,
+ depth + 1,
+ chapter_title,
+ ));
+ } else {
+ replaced.push_str(&new_content);
+ }
+ } else {
+ error!(
+ "Stack depth exceeded in {}. Check for cyclic includes",
+ source.display()
+ );
+ }
+ previous_end_index = link.end_index;
+ }
+ Err(e) => {
+ error!("Error updating \"{}\", {}", link.link_text, e);
+ for cause in e.chain().skip(1) {
+ warn!("Caused By: {}", cause);
+ }
+
+ // This should make sure we include the raw `{{# ... }}` snippet
+ // in the page content if there are any errors.
+ previous_end_index = link.start_index;
+ }
+ }
+ }
+
+ replaced.push_str(&s[previous_end_index..]);
+ replaced
+}
+
+#[derive(PartialEq, Debug, Clone)]
+enum LinkType<'a> {
+ Escaped,
+ Include(PathBuf, RangeOrAnchor),
+ Playground(PathBuf, Vec<&'a str>),
+ RustdocInclude(PathBuf, RangeOrAnchor),
+ Title(&'a str),
+}
+
+#[derive(PartialEq, Debug, Clone)]
+enum RangeOrAnchor {
+ Range(LineRange),
+ Anchor(String),
+}
+
+// A range of lines specified with some include directive.
+#[allow(clippy::enum_variant_names)] // The prefix can't be removed, and is meant to mirror the contained type
+#[derive(PartialEq, Debug, Clone)]
+enum LineRange {
+ Range(Range<usize>),
+ RangeFrom(RangeFrom<usize>),
+ RangeTo(RangeTo<usize>),
+ RangeFull(RangeFull),
+}
+
+impl RangeBounds<usize> for LineRange {
+ fn start_bound(&self) -> Bound<&usize> {
+ match self {
+ LineRange::Range(r) => r.start_bound(),
+ LineRange::RangeFrom(r) => r.start_bound(),
+ LineRange::RangeTo(r) => r.start_bound(),
+ LineRange::RangeFull(r) => r.start_bound(),
+ }
+ }
+
+ fn end_bound(&self) -> Bound<&usize> {
+ match self {
+ LineRange::Range(r) => r.end_bound(),
+ LineRange::RangeFrom(r) => r.end_bound(),
+ LineRange::RangeTo(r) => r.end_bound(),
+ LineRange::RangeFull(r) => r.end_bound(),
+ }
+ }
+}
+
+impl From<Range<usize>> for LineRange {
+ fn from(r: Range<usize>) -> LineRange {
+ LineRange::Range(r)
+ }
+}
+
+impl From<RangeFrom<usize>> for LineRange {
+ fn from(r: RangeFrom<usize>) -> LineRange {
+ LineRange::RangeFrom(r)
+ }
+}
+
+impl From<RangeTo<usize>> for LineRange {
+ fn from(r: RangeTo<usize>) -> LineRange {
+ LineRange::RangeTo(r)
+ }
+}
+
+impl From<RangeFull> for LineRange {
+ fn from(r: RangeFull) -> LineRange {
+ LineRange::RangeFull(r)
+ }
+}
+
+impl<'a> LinkType<'a> {
+ fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> {
+ let base = base.as_ref();
+ match self {
+ LinkType::Escaped => None,
+ LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
+ LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
+ LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
+ LinkType::Title(_) => None,
+ }
+ }
+}
+fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
+ base.as_ref()
+ .join(relative)
+ .parent()
+ .expect("Included file should not be /")
+ .to_path_buf()
+}
+
+fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor {
+ let mut parts = parts.unwrap_or("").splitn(3, ':').fuse();
+
+ let next_element = parts.next();
+ let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
+ // subtract 1 since line numbers usually begin with 1
+ Some(value.saturating_sub(1))
+ } else if let Some("") = next_element {
+ None
+ } else if let Some(anchor) = next_element {
+ return RangeOrAnchor::Anchor(String::from(anchor));
+ } else {
+ None
+ };
+
+ let end = parts.next();
+ // If `end` is empty string or any other value that can't be parsed as a usize, treat this
+ // include as a range with only a start bound. However, if end isn't specified, include only
+ // the single line specified by `start`.
+ let end = end.map(|s| s.parse::<usize>());
+
+ match (start, end) {
+ (Some(start), Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(start..end)),
+ (Some(start), Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(start..)),
+ (Some(start), None) => RangeOrAnchor::Range(LineRange::from(start..start + 1)),
+ (None, Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(..end)),
+ (None, None) | (None, Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(RangeFull)),
+ }
+}
+
+fn parse_include_path(path: &str) -> LinkType<'static> {
+ let mut parts = path.splitn(2, ':');
+
+ let path = parts.next().unwrap().into();
+ let range_or_anchor = parse_range_or_anchor(parts.next());
+
+ LinkType::Include(path, range_or_anchor)
+}
+
+fn parse_rustdoc_include_path(path: &str) -> LinkType<'static> {
+ let mut parts = path.splitn(2, ':');
+
+ let path = parts.next().unwrap().into();
+ let range_or_anchor = parse_range_or_anchor(parts.next());
+
+ LinkType::RustdocInclude(path, range_or_anchor)
+}
+
+#[derive(PartialEq, Debug, Clone)]
+struct Link<'a> {
+ start_index: usize,
+ end_index: usize,
+ link_type: LinkType<'a>,
+ link_text: &'a str,
+}
+
+impl<'a> Link<'a> {
+ fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
+ let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
+ (_, Some(typ), Some(title)) if typ.as_str() == "title" => {
+ Some(LinkType::Title(title.as_str()))
+ }
+ (_, Some(typ), Some(rest)) => {
+ let mut path_props = rest.as_str().split_whitespace();
+ let file_arg = path_props.next();
+ let props: Vec<&str> = path_props.collect();
+
+ match (typ.as_str(), file_arg) {
+ ("include", Some(pth)) => Some(parse_include_path(pth)),
+ ("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)),
+ ("playpen", Some(pth)) => {
+ warn!(
+ "the {{{{#playpen}}}} expression has been \
+ renamed to {{{{#playground}}}}, \
+ please update your book to use the new name"
+ );
+ Some(LinkType::Playground(pth.into(), props))
+ }
+ ("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
+ _ => None,
+ }
+ }
+ (Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
+ Some(LinkType::Escaped)
+ }
+ _ => None,
+ };
+
+ link_type.and_then(|lnk_type| {
+ cap.get(0).map(|mat| Link {
+ start_index: mat.start(),
+ end_index: mat.end(),
+ link_type: lnk_type,
+ link_text: mat.as_str(),
+ })
+ })
+ }
+
+ fn render_with_path<P: AsRef<Path>>(
+ &self,
+ base: P,
+ chapter_title: &mut String,
+ ) -> Result<String> {
+ let base = base.as_ref();
+ match self.link_type {
+ // omit the escape char
+ LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
+ LinkType::Include(ref pat, ref range_or_anchor) => {
+ let target = base.join(pat);
+
+ fs::read_to_string(&target)
+ .map(|s| match range_or_anchor {
+ RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
+ RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
+ })
+ .with_context(|| {
+ format!(
+ "Could not read file for link {} ({})",
+ self.link_text,
+ target.display(),
+ )
+ })
+ }
+ LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
+ let target = base.join(pat);
+
+ fs::read_to_string(&target)
+ .map(|s| match range_or_anchor {
+ RangeOrAnchor::Range(range) => {
+ take_rustdoc_include_lines(&s, range.clone())
+ }
+ RangeOrAnchor::Anchor(anchor) => {
+ take_rustdoc_include_anchored_lines(&s, anchor)
+ }
+ })
+ .with_context(|| {
+ format!(
+ "Could not read file for link {} ({})",
+ self.link_text,
+ target.display(),
+ )
+ })
+ }
+ LinkType::Playground(ref pat, ref attrs) => {
+ let target = base.join(pat);
+
+ let mut contents = fs::read_to_string(&target).with_context(|| {
+ format!(
+ "Could not read file for link {} ({})",
+ self.link_text,
+ target.display()
+ )
+ })?;
+ let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
+ if !contents.ends_with('\n') {
+ contents.push('\n');
+ }
+ Ok(format!(
+ "```{}{}\n{}```\n",
+ ftype,
+ attrs.join(","),
+ contents
+ ))
+ }
+ LinkType::Title(title) => {
+ *chapter_title = title.to_owned();
+ Ok(String::new())
+ }
+ }
+ }
+}
+
+struct LinkIter<'a>(CaptureMatches<'a, 'a>);
+
+impl<'a> Iterator for LinkIter<'a> {
+ type Item = Link<'a>;
+ fn next(&mut self) -> Option<Link<'a>> {
+ for cap in &mut self.0 {
+ if let Some(inc) = Link::from_capture(cap) {
+ return Some(inc);
+ }
+ }
+ None
+ }
+}
+
+fn find_links(contents: &str) -> LinkIter<'_> {
+ // lazily compute following regex
+ // r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
+ lazy_static! {
+ static ref RE: Regex = Regex::new(
+ r"(?x) # insignificant whitespace mode
+ \\\{\{\#.*\}\} # match escaped link
+ | # or
+ \{\{\s* # link opening parens and whitespace
+ \#([a-zA-Z0-9_]+) # link type
+ \s+ # separating whitespace
+ ([^}]+) # link target path and space separated properties
+ \}\} # link closing parens"
+ )
+ .unwrap();
+ }
+ LinkIter(RE.captures_iter(contents))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_replace_all_escaped() {
+ let start = r"
+ Some text over here.
+ ```hbs
+ \{{#include file.rs}} << an escaped link!
+ ```";
+ let end = r"
+ Some text over here.
+ ```hbs
+ {{#include file.rs}} << an escaped link!
+ ```";
+ let mut chapter_title = "test_replace_all_escaped".to_owned();
+ assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
+ }
+
+ #[test]
+ fn test_set_chapter_title() {
+ let start = r"{{#title My Title}}
+ # My Chapter
+ ";
+ let end = r"
+ # My Chapter
+ ";
+ let mut chapter_title = "test_set_chapter_title".to_owned();
+ assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
+ assert_eq!(chapter_title, "My Title");
+ }
+
+ #[test]
+ fn test_find_links_no_link() {
+ let s = "Some random text without link...";
+ assert!(find_links(s).collect::<Vec<_>>() == vec![]);
+ }
+
+ #[test]
+ fn test_find_links_partial_link() {
+ let s = "Some random text with {{#playground...";
+ assert!(find_links(s).collect::<Vec<_>>() == vec![]);
+ let s = "Some random text with {{#include...";
+ assert!(find_links(s).collect::<Vec<_>>() == vec![]);
+ let s = "Some random text with \\{{#include...";
+ assert!(find_links(s).collect::<Vec<_>>() == vec![]);
+ }
+
+ #[test]
+ fn test_find_links_empty_link() {
+ let s = "Some random text with {{#playground}} and {{#playground }} {{}} {{#}}...";
+ assert!(find_links(s).collect::<Vec<_>>() == vec![]);
+ }
+
+ #[test]
+ fn test_find_links_unknown_link_type() {
+ let s = "Some random text with {{#playgroundz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
+ assert!(find_links(s).collect::<Vec<_>>() == vec![]);
+ }
+
+ #[test]
+ fn test_find_links_simple_link() {
+ let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
+
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+
+ assert_eq!(
+ res,
+ vec![
+ Link {
+ start_index: 22,
+ end_index: 45,
+ link_type: LinkType::Playground(PathBuf::from("file.rs"), vec![]),
+ link_text: "{{#playground file.rs}}",
+ },
+ Link {
+ start_index: 50,
+ end_index: 74,
+ link_type: LinkType::Playground(PathBuf::from("test.rs"), vec![]),
+ link_text: "{{#playground test.rs }}",
+ },
+ ]
+ );
+ }
+
+ #[test]
+ fn test_find_links_with_special_characters() {
+ let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
+
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+
+ assert_eq!(
+ res,
+ vec![Link {
+ start_index: 22,
+ end_index: 57,
+ link_type: LinkType::Playground(PathBuf::from("foo-bar\\baz/_c++.rs"), vec![]),
+ link_text: "{{#playground foo-bar\\baz/_c++.rs}}",
+ },]
+ );
+ }
+
+ #[test]
+ fn test_find_links_with_range() {
+ let s = "Some random text with {{#include file.rs:10:20}}...";
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+ assert_eq!(
+ res,
+ vec![Link {
+ start_index: 22,
+ end_index: 48,
+ link_type: LinkType::Include(
+ PathBuf::from("file.rs"),
+ RangeOrAnchor::Range(LineRange::from(9..20))
+ ),
+ link_text: "{{#include file.rs:10:20}}",
+ }]
+ );
+ }
+
+ #[test]
+ fn test_find_links_with_line_number() {
+ let s = "Some random text with {{#include file.rs:10}}...";
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+ assert_eq!(
+ res,
+ vec![Link {
+ start_index: 22,
+ end_index: 45,
+ link_type: LinkType::Include(
+ PathBuf::from("file.rs"),
+ RangeOrAnchor::Range(LineRange::from(9..10))
+ ),
+ link_text: "{{#include file.rs:10}}",
+ }]
+ );
+ }
+
+ #[test]
+ fn test_find_links_with_from_range() {
+ let s = "Some random text with {{#include file.rs:10:}}...";
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+ assert_eq!(
+ res,
+ vec![Link {
+ start_index: 22,
+ end_index: 46,
+ link_type: LinkType::Include(
+ PathBuf::from("file.rs"),
+ RangeOrAnchor::Range(LineRange::from(9..))
+ ),
+ link_text: "{{#include file.rs:10:}}",
+ }]
+ );
+ }
+
+ #[test]
+ fn test_find_links_with_to_range() {
+ let s = "Some random text with {{#include file.rs::20}}...";
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+ assert_eq!(
+ res,
+ vec![Link {
+ start_index: 22,
+ end_index: 46,
+ link_type: LinkType::Include(
+ PathBuf::from("file.rs"),
+ RangeOrAnchor::Range(LineRange::from(..20))
+ ),
+ link_text: "{{#include file.rs::20}}",
+ }]
+ );
+ }
+
+ #[test]
+ fn test_find_links_with_full_range() {
+ let s = "Some random text with {{#include file.rs::}}...";
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+ assert_eq!(
+ res,
+ vec![Link {
+ start_index: 22,
+ end_index: 44,
+ link_type: LinkType::Include(
+ PathBuf::from("file.rs"),
+ RangeOrAnchor::Range(LineRange::from(..))
+ ),
+ link_text: "{{#include file.rs::}}",
+ }]
+ );
+ }
+
+ #[test]
+ fn test_find_links_with_no_range_specified() {
+ let s = "Some random text with {{#include file.rs}}...";
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+ assert_eq!(
+ res,
+ vec![Link {
+ start_index: 22,
+ end_index: 42,
+ link_type: LinkType::Include(
+ PathBuf::from("file.rs"),
+ RangeOrAnchor::Range(LineRange::from(..))
+ ),
+ link_text: "{{#include file.rs}}",
+ }]
+ );
+ }
+
+ #[test]
+ fn test_find_links_with_anchor() {
+ let s = "Some random text with {{#include file.rs:anchor}}...";
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+ assert_eq!(
+ res,
+ vec![Link {
+ start_index: 22,
+ end_index: 49,
+ link_type: LinkType::Include(
+ PathBuf::from("file.rs"),
+ RangeOrAnchor::Anchor(String::from("anchor"))
+ ),
+ link_text: "{{#include file.rs:anchor}}",
+ }]
+ );
+ }
+
+ #[test]
+ fn test_find_links_escaped_link() {
+ let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
+
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+
+ assert_eq!(
+ res,
+ vec![Link {
+ start_index: 41,
+ end_index: 74,
+ link_type: LinkType::Escaped,
+ link_text: "\\{{#playground file.rs editable}}",
+ }]
+ );
+ }
+
+ #[test]
+ fn test_find_playgrounds_with_properties() {
+ let s =
+ "Some random text with escaped playground {{#playground file.rs editable }} and some \
+ more\n text {{#playground my.rs editable no_run should_panic}} ...";
+
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+ assert_eq!(
+ res,
+ vec![
+ Link {
+ start_index: 41,
+ end_index: 74,
+ link_type: LinkType::Playground(PathBuf::from("file.rs"), vec!["editable"]),
+ link_text: "{{#playground file.rs editable }}",
+ },
+ Link {
+ start_index: 95,
+ end_index: 145,
+ link_type: LinkType::Playground(
+ PathBuf::from("my.rs"),
+ vec!["editable", "no_run", "should_panic"],
+ ),
+ link_text: "{{#playground my.rs editable no_run should_panic}}",
+ },
+ ]
+ );
+ }
+
+ #[test]
+ fn test_find_all_link_types() {
+ let s =
+ "Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
+ insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
+ no_run should_panic}} ...";
+
+ let res = find_links(s).collect::<Vec<_>>();
+ println!("\nOUTPUT: {:?}\n", res);
+ assert_eq!(res.len(), 3);
+ assert_eq!(
+ res[0],
+ Link {
+ start_index: 41,
+ end_index: 61,
+ link_type: LinkType::Include(
+ PathBuf::from("file.rs"),
+ RangeOrAnchor::Range(LineRange::from(..))
+ ),
+ link_text: "{{#include file.rs}}",
+ }
+ );
+ assert_eq!(
+ res[1],
+ Link {
+ start_index: 66,
+ end_index: 115,
+ link_type: LinkType::Escaped,
+ link_text: "\\{{#contents are insignifficant in escaped link}}",
+ }
+ );
+ assert_eq!(
+ res[2],
+ Link {
+ start_index: 133,
+ end_index: 183,
+ link_type: LinkType::Playground(
+ PathBuf::from("my.rs"),
+ vec!["editable", "no_run", "should_panic"]
+ ),
+ link_text: "{{#playground my.rs editable no_run should_panic}}",
+ }
+ );
+ }
+
+ #[test]
+ fn parse_without_colon_includes_all() {
+ let link_type = parse_include_path("arbitrary");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(RangeFull))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_with_nothing_after_colon_includes_all() {
+ let link_type = parse_include_path("arbitrary:");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(RangeFull))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_with_two_colons_includes_all() {
+ let link_type = parse_include_path("arbitrary::");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(RangeFull))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_with_garbage_after_two_colons_includes_all() {
+ let link_type = parse_include_path("arbitrary::NaN");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(RangeFull))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_with_one_number_after_colon_only_that_line() {
+ let link_type = parse_include_path("arbitrary:5");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(4..5))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_with_one_based_start_becomes_zero_based() {
+ let link_type = parse_include_path("arbitrary:1");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(0..1))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_with_zero_based_start_stays_zero_based_but_is_probably_an_error() {
+ let link_type = parse_include_path("arbitrary:0");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(0..1))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_start_only_range() {
+ let link_type = parse_include_path("arbitrary:5:");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(4..))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_start_with_garbage_interpreted_as_start_only_range() {
+ let link_type = parse_include_path("arbitrary:5:NaN");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(4..))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_end_only_range() {
+ let link_type = parse_include_path("arbitrary::5");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(..5))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_start_and_end_range() {
+ let link_type = parse_include_path("arbitrary:5:10");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(4..10))
+ )
+ );
+ }
+
+ #[test]
+ fn parse_with_negative_interpreted_as_anchor() {
+ let link_type = parse_include_path("arbitrary:-5");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Anchor("-5".to_string())
+ )
+ );
+ }
+
+ #[test]
+ fn parse_with_floating_point_interpreted_as_anchor() {
+ let link_type = parse_include_path("arbitrary:-5.7");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Anchor("-5.7".to_string())
+ )
+ );
+ }
+
+ #[test]
+ fn parse_with_anchor_followed_by_colon() {
+ let link_type = parse_include_path("arbitrary:some-anchor:this-gets-ignored");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Anchor("some-anchor".to_string())
+ )
+ );
+ }
+
+ #[test]
+ fn parse_with_more_than_three_colons_ignores_everything_after_third_colon() {
+ let link_type = parse_include_path("arbitrary:5:10:17:anything:");
+ assert_eq!(
+ link_type,
+ LinkType::Include(
+ PathBuf::from("arbitrary"),
+ RangeOrAnchor::Range(LineRange::from(4..10))
+ )
+ );
+ }
+}
diff --git a/vendor/mdbook/src/preprocess/mod.rs b/vendor/mdbook/src/preprocess/mod.rs
new file mode 100644
index 000000000..894e20035
--- /dev/null
+++ b/vendor/mdbook/src/preprocess/mod.rs
@@ -0,0 +1,70 @@
+//! Book preprocessing.
+
+pub use self::cmd::CmdPreprocessor;
+pub use self::index::IndexPreprocessor;
+pub use self::links::LinkPreprocessor;
+
+mod cmd;
+mod index;
+mod links;
+
+use crate::book::Book;
+use crate::config::Config;
+use crate::errors::*;
+
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
+
+/// Extra information for a `Preprocessor` to give them more context when
+/// processing a book.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct PreprocessorContext {
+ /// The location of the book directory on disk.
+ pub root: PathBuf,
+ /// The book configuration (`book.toml`).
+ pub config: Config,
+ /// The `Renderer` this preprocessor is being used with.
+ pub renderer: String,
+ /// The calling `mdbook` version.
+ pub mdbook_version: String,
+ #[serde(skip)]
+ pub(crate) chapter_titles: RefCell<HashMap<PathBuf, String>>,
+ #[serde(skip)]
+ __non_exhaustive: (),
+}
+
+impl PreprocessorContext {
+ /// Create a new `PreprocessorContext`.
+ pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self {
+ PreprocessorContext {
+ root,
+ config,
+ renderer,
+ mdbook_version: crate::MDBOOK_VERSION.to_string(),
+ chapter_titles: RefCell::new(HashMap::new()),
+ __non_exhaustive: (),
+ }
+ }
+}
+
+/// An operation which is run immediately after loading a book into memory and
+/// before it gets rendered.
+pub trait Preprocessor {
+ /// Get the `Preprocessor`'s name.
+ fn name(&self) -> &str;
+
+ /// Run this `Preprocessor`, allowing it to update the book before it is
+ /// given to a renderer.
+ fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
+
+ /// A hint to `MDBook` whether this preprocessor is compatible with a
+ /// particular renderer.
+ ///
+ /// By default, always returns `true`.
+ fn supports_renderer(&self, _renderer: &str) -> bool {
+ true
+ }
+}
diff --git a/vendor/mdbook/src/renderer/html_handlebars/hbs_renderer.rs b/vendor/mdbook/src/renderer/html_handlebars/hbs_renderer.rs
new file mode 100644
index 000000000..b933a359a
--- /dev/null
+++ b/vendor/mdbook/src/renderer/html_handlebars/hbs_renderer.rs
@@ -0,0 +1,1106 @@
+use crate::book::{Book, BookItem};
+use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition};
+use crate::errors::*;
+use crate::renderer::html_handlebars::helpers;
+use crate::renderer::{RenderContext, Renderer};
+use crate::theme::{self, playground_editor, Theme};
+use crate::utils;
+
+use std::borrow::Cow;
+use std::collections::BTreeMap;
+use std::collections::HashMap;
+use std::fs::{self, File};
+use std::path::{Path, PathBuf};
+
+use crate::utils::fs::get_404_output_file;
+use handlebars::Handlebars;
+use regex::{Captures, Regex};
+
+#[derive(Default)]
+pub struct HtmlHandlebars;
+
+impl HtmlHandlebars {
+ pub fn new() -> Self {
+ HtmlHandlebars
+ }
+
+ fn render_item(
+ &self,
+ item: &BookItem,
+ mut ctx: RenderItemContext<'_>,
+ print_content: &mut String,
+ ) -> Result<()> {
+ // FIXME: This should be made DRY-er and rely less on mutable state
+
+ let (ch, path) = match item {
+ BookItem::Chapter(ch) if !ch.is_draft_chapter() => (ch, ch.path.as_ref().unwrap()),
+ _ => return Ok(()),
+ };
+
+ if let Some(ref edit_url_template) = ctx.html_config.edit_url_template {
+ let full_path = ctx.book_config.src.to_str().unwrap_or_default().to_owned()
+ + "/"
+ + ch.source_path
+ .clone()
+ .unwrap_or_default()
+ .to_str()
+ .unwrap_or_default();
+
+ let edit_url = edit_url_template.replace("{path}", &full_path);
+ ctx.data
+ .insert("git_repository_edit_url".to_owned(), json!(edit_url));
+ }
+
+ let content = ch.content.clone();
+ let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
+
+ let fixed_content =
+ utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
+ if !ctx.is_index && ctx.html_config.print.page_break {
+ // Add page break between chapters
+ // See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
+ // Add both two CSS properties because of the compatibility issue
+ print_content
+ .push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
+ }
+ print_content.push_str(&fixed_content);
+
+ // Update the context with data for this file
+ let ctx_path = path
+ .to_str()
+ .with_context(|| "Could not convert path to str")?;
+ let filepath = Path::new(&ctx_path).with_extension("html");
+
+ // "print.html" is used for the print page.
+ if path == Path::new("print.md") {
+ bail!("{} is reserved for internal use", path.display());
+ };
+
+ let book_title = ctx
+ .data
+ .get("book_title")
+ .and_then(serde_json::Value::as_str)
+ .unwrap_or("");
+
+ let title = if let Some(title) = ctx.chapter_titles.get(path) {
+ title.clone()
+ } else if book_title.is_empty() {
+ ch.name.clone()
+ } else {
+ ch.name.clone() + " - " + book_title
+ };
+
+ ctx.data.insert("path".to_owned(), json!(path));
+ ctx.data.insert("content".to_owned(), json!(content));
+ ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
+ ctx.data.insert("title".to_owned(), json!(title));
+ ctx.data.insert(
+ "path_to_root".to_owned(),
+ json!(utils::fs::path_to_root(&path)),
+ );
+ if let Some(ref section) = ch.number {
+ ctx.data
+ .insert("section".to_owned(), json!(section.to_string()));
+ }
+
+ // Render the handlebars template with the data
+ debug!("Render template");
+ let rendered = ctx.handlebars.render("index", &ctx.data)?;
+
+ let rendered = self.post_process(rendered, &ctx.html_config.playground, ctx.edition);
+
+ // Write to file
+ debug!("Creating {}", filepath.display());
+ utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
+
+ if ctx.is_index {
+ ctx.data.insert("path".to_owned(), json!("index.md"));
+ ctx.data.insert("path_to_root".to_owned(), json!(""));
+ ctx.data.insert("is_index".to_owned(), json!(true));
+ let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
+ let rendered_index =
+ self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition);
+ debug!("Creating index.html from {}", ctx_path);
+ utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
+ }
+
+ Ok(())
+ }
+
+ fn render_404(
+ &self,
+ ctx: &RenderContext,
+ html_config: &HtmlConfig,
+ src_dir: &Path,
+ handlebars: &mut Handlebars<'_>,
+ data: &mut serde_json::Map<String, serde_json::Value>,
+ ) -> Result<()> {
+ let destination = &ctx.destination;
+ let content_404 = if let Some(ref filename) = html_config.input_404 {
+ let path = src_dir.join(filename);
+ std::fs::read_to_string(&path)
+ .with_context(|| format!("unable to open 404 input file {:?}", path))?
+ } else {
+ // 404 input not explicitly configured try the default file 404.md
+ let default_404_location = src_dir.join("404.md");
+ if default_404_location.exists() {
+ std::fs::read_to_string(&default_404_location).with_context(|| {
+ format!("unable to open 404 input file {:?}", default_404_location)
+ })?
+ } else {
+ "# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
+ navigation bar or search to continue."
+ .to_string()
+ }
+ };
+ let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes);
+
+ let mut data_404 = data.clone();
+ let base_url = if let Some(site_url) = &html_config.site_url {
+ site_url
+ } else {
+ debug!(
+ "HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
+ this to ensure the 404 page work correctly, especially if your site is hosted in a \
+ subdirectory on the HTTP server."
+ );
+ "/"
+ };
+ data_404.insert("base_url".to_owned(), json!(base_url));
+ // Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
+ data_404.insert("path".to_owned(), json!("404.md"));
+ data_404.insert("content".to_owned(), json!(html_content_404));
+
+ let mut title = String::from("Page not found");
+ if let Some(book_title) = &ctx.config.book.title {
+ title.push_str(" - ");
+ title.push_str(book_title);
+ }
+ data_404.insert("title".to_owned(), json!(title));
+ let rendered = handlebars.render("index", &data_404)?;
+
+ let rendered =
+ self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
+ let output_file = get_404_output_file(&html_config.input_404);
+ utils::fs::write_file(destination, output_file, rendered.as_bytes())?;
+ debug!("Creating 404.html ✓");
+ Ok(())
+ }
+
+ #[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
+ fn post_process(
+ &self,
+ rendered: String,
+ playground_config: &Playground,
+ edition: Option<RustEdition>,
+ ) -> String {
+ let rendered = build_header_links(&rendered);
+ let rendered = fix_code_blocks(&rendered);
+ let rendered = add_playground_pre(&rendered, playground_config, edition);
+
+ rendered
+ }
+
+ fn copy_static_files(
+ &self,
+ destination: &Path,
+ theme: &Theme,
+ html_config: &HtmlConfig,
+ ) -> Result<()> {
+ use crate::utils::fs::write_file;
+
+ write_file(
+ destination,
+ ".nojekyll",
+ b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
+ )?;
+
+ if let Some(cname) = &html_config.cname {
+ write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?;
+ }
+
+ write_file(destination, "book.js", &theme.js)?;
+ write_file(destination, "css/general.css", &theme.general_css)?;
+ write_file(destination, "css/chrome.css", &theme.chrome_css)?;
+ if html_config.print.enable {
+ write_file(destination, "css/print.css", &theme.print_css)?;
+ }
+ write_file(destination, "css/variables.css", &theme.variables_css)?;
+ if let Some(contents) = &theme.favicon_png {
+ write_file(destination, "favicon.png", contents)?;
+ }
+ if let Some(contents) = &theme.favicon_svg {
+ write_file(destination, "favicon.svg", contents)?;
+ }
+ write_file(destination, "highlight.css", &theme.highlight_css)?;
+ write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
+ write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
+ write_file(destination, "highlight.js", &theme.highlight_js)?;
+ write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
+ write_file(
+ destination,
+ "FontAwesome/css/font-awesome.css",
+ theme::FONT_AWESOME,
+ )?;
+ write_file(
+ destination,
+ "FontAwesome/fonts/fontawesome-webfont.eot",
+ theme::FONT_AWESOME_EOT,
+ )?;
+ write_file(
+ destination,
+ "FontAwesome/fonts/fontawesome-webfont.svg",
+ theme::FONT_AWESOME_SVG,
+ )?;
+ write_file(
+ destination,
+ "FontAwesome/fonts/fontawesome-webfont.ttf",
+ theme::FONT_AWESOME_TTF,
+ )?;
+ write_file(
+ destination,
+ "FontAwesome/fonts/fontawesome-webfont.woff",
+ theme::FONT_AWESOME_WOFF,
+ )?;
+ write_file(
+ destination,
+ "FontAwesome/fonts/fontawesome-webfont.woff2",
+ theme::FONT_AWESOME_WOFF2,
+ )?;
+ write_file(
+ destination,
+ "FontAwesome/fonts/FontAwesome.ttf",
+ theme::FONT_AWESOME_TTF,
+ )?;
+ if html_config.copy_fonts {
+ write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
+ for (file_name, contents) in theme::fonts::LICENSES.iter() {
+ write_file(destination, file_name, contents)?;
+ }
+ for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
+ write_file(destination, file_name, contents)?;
+ }
+ write_file(
+ destination,
+ theme::fonts::SOURCE_CODE_PRO.0,
+ theme::fonts::SOURCE_CODE_PRO.1,
+ )?;
+ }
+
+ let playground_config = &html_config.playground;
+
+ // Ace is a very large dependency, so only load it when requested
+ if playground_config.editable && playground_config.copy_js {
+ // Load the editor
+ write_file(destination, "editor.js", playground_editor::JS)?;
+ write_file(destination, "ace.js", playground_editor::ACE_JS)?;
+ write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?;
+ write_file(
+ destination,
+ "theme-dawn.js",
+ playground_editor::THEME_DAWN_JS,
+ )?;
+ write_file(
+ destination,
+ "theme-tomorrow_night.js",
+ playground_editor::THEME_TOMORROW_NIGHT_JS,
+ )?;
+ }
+
+ Ok(())
+ }
+
+ /// Update the context with data for this file
+ fn configure_print_version(
+ &self,
+ data: &mut serde_json::Map<String, serde_json::Value>,
+ print_content: &str,
+ ) {
+ // Make sure that the Print chapter does not display the title from
+ // the last rendered chapter by removing it from its context
+ data.remove("title");
+ data.insert("is_print".to_owned(), json!(true));
+ data.insert("path".to_owned(), json!("print.md"));
+ data.insert("content".to_owned(), json!(print_content));
+ data.insert(
+ "path_to_root".to_owned(),
+ json!(utils::fs::path_to_root(Path::new("print.md"))),
+ );
+ }
+
+ fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) {
+ handlebars.register_helper(
+ "toc",
+ Box::new(helpers::toc::RenderToc {
+ no_section_label: html_config.no_section_label,
+ }),
+ );
+ handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
+ handlebars.register_helper("next", Box::new(helpers::navigation::next));
+ handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
+ }
+
+ /// Copy across any additional CSS and JavaScript files which the book
+ /// has been configured to use.
+ fn copy_additional_css_and_js(
+ &self,
+ html: &HtmlConfig,
+ root: &Path,
+ destination: &Path,
+ ) -> Result<()> {
+ let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
+
+ debug!("Copying additional CSS and JS");
+
+ for custom_file in custom_files {
+ let input_location = root.join(custom_file);
+ let output_location = destination.join(custom_file);
+ if let Some(parent) = output_location.parent() {
+ fs::create_dir_all(parent)
+ .with_context(|| format!("Unable to create {}", parent.display()))?;
+ }
+ debug!(
+ "Copying {} -> {}",
+ input_location.display(),
+ output_location.display()
+ );
+
+ fs::copy(&input_location, &output_location).with_context(|| {
+ format!(
+ "Unable to copy {} to {}",
+ input_location.display(),
+ output_location.display()
+ )
+ })?;
+ }
+
+ Ok(())
+ }
+
+ fn emit_redirects(
+ &self,
+ root: &Path,
+ handlebars: &Handlebars<'_>,
+ redirects: &HashMap<String, String>,
+ ) -> Result<()> {
+ if redirects.is_empty() {
+ return Ok(());
+ }
+
+ log::debug!("Emitting redirects");
+
+ for (original, new) in redirects {
+ log::debug!("Redirecting \"{}\" → \"{}\"", original, new);
+ // Note: all paths are relative to the build directory, so the
+ // leading slash in an absolute path means nothing (and would mess
+ // up `root.join(original)`).
+ let original = original.trim_start_matches('/');
+ let filename = root.join(original);
+ self.emit_redirect(handlebars, &filename, new)?;
+ }
+
+ Ok(())
+ }
+
+ fn emit_redirect(
+ &self,
+ handlebars: &Handlebars<'_>,
+ original: &Path,
+ destination: &str,
+ ) -> Result<()> {
+ if original.exists() {
+ // sanity check to avoid accidentally overwriting a real file.
+ let msg = format!(
+ "Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?",
+ original.display(),
+ destination,
+ );
+ return Err(Error::msg(msg));
+ }
+
+ if let Some(parent) = original.parent() {
+ std::fs::create_dir_all(parent)
+ .with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
+ }
+
+ let ctx = json!({
+ "url": destination,
+ });
+ let f = File::create(original)?;
+ handlebars
+ .render_to_write("redirect", &ctx, f)
+ .with_context(|| {
+ format!(
+ "Unable to create a redirect file at \"{}\"",
+ original.display()
+ )
+ })?;
+
+ Ok(())
+ }
+}
+
+// TODO(mattico): Remove some time after the 0.1.8 release
+fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
+ fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
+ Ok(entry.file_type()?.is_file()
+ && entry.path().extension().map_or(false, |ext| ext == "md"))
+ }
+
+ if dir.is_dir() {
+ for entry in fs::read_dir(dir)? {
+ if entry_is_maybe_book_file(entry?).unwrap_or(false) {
+ return Ok(false);
+ }
+ }
+ Ok(true)
+ } else {
+ Ok(false)
+ }
+}
+
+impl Renderer for HtmlHandlebars {
+ fn name(&self) -> &str {
+ "html"
+ }
+
+ fn render(&self, ctx: &RenderContext) -> Result<()> {
+ let book_config = &ctx.config.book;
+ let html_config = ctx.config.html_config().unwrap_or_default();
+ let src_dir = ctx.root.join(&ctx.config.book.src);
+ let destination = &ctx.destination;
+ let book = &ctx.book;
+ let build_dir = ctx.root.join(&ctx.config.build.build_dir);
+
+ if destination.exists() {
+ utils::fs::remove_dir_content(destination)
+ .with_context(|| "Unable to remove stale HTML output")?;
+ }
+
+ trace!("render");
+ let mut handlebars = Handlebars::new();
+
+ let theme_dir = match html_config.theme {
+ Some(ref theme) => {
+ let dir = ctx.root.join(theme);
+ if !dir.is_dir() {
+ bail!("theme dir {} does not exist", dir.display());
+ }
+ dir
+ }
+ None => ctx.root.join("theme"),
+ };
+
+ if html_config.theme.is_none()
+ && maybe_wrong_theme_dir(&src_dir.join("theme")).unwrap_or(false)
+ {
+ warn!(
+ "Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
+ theme directory"
+ );
+ warn!("Please move your theme files to `./theme` for them to continue being used");
+ }
+
+ let theme = theme::Theme::new(theme_dir);
+
+ debug!("Register the index handlebars template");
+ handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
+
+ debug!("Register the head handlebars template");
+ handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?;
+
+ debug!("Register the redirect handlebars template");
+ handlebars
+ .register_template_string("redirect", String::from_utf8(theme.redirect.clone())?)?;
+
+ debug!("Register the header handlebars template");
+ handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
+
+ debug!("Register handlebars helpers");
+ self.register_hbs_helpers(&mut handlebars, &html_config);
+
+ let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?;
+
+ // Print version
+ let mut print_content = String::new();
+
+ fs::create_dir_all(&destination)
+ .with_context(|| "Unexpected error when constructing destination path")?;
+
+ let mut is_index = true;
+ for item in book.iter() {
+ let ctx = RenderItemContext {
+ handlebars: &handlebars,
+ destination: destination.to_path_buf(),
+ data: data.clone(),
+ is_index,
+ book_config: book_config.clone(),
+ html_config: html_config.clone(),
+ edition: ctx.config.rust.edition,
+ chapter_titles: &ctx.chapter_titles,
+ };
+ self.render_item(item, ctx, &mut print_content)?;
+ // Only the first non-draft chapter item should be treated as the "index"
+ is_index &= !matches!(item, BookItem::Chapter(ch) if !ch.is_draft_chapter());
+ }
+
+ // Render 404 page
+ if html_config.input_404 != Some("".to_string()) {
+ self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?;
+ }
+
+ // Print version
+ self.configure_print_version(&mut data, &print_content);
+ if let Some(ref title) = ctx.config.book.title {
+ data.insert("title".to_owned(), json!(title));
+ }
+
+ // Render the handlebars template with the data
+ if html_config.print.enable {
+ debug!("Render template");
+ let rendered = handlebars.render("index", &data)?;
+
+ let rendered =
+ self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
+
+ utils::fs::write_file(destination, "print.html", rendered.as_bytes())?;
+ debug!("Creating print.html ✓");
+ }
+
+ debug!("Copy static files");
+ self.copy_static_files(destination, &theme, &html_config)
+ .with_context(|| "Unable to copy across static files")?;
+ self.copy_additional_css_and_js(&html_config, &ctx.root, destination)
+ .with_context(|| "Unable to copy across additional CSS and JS")?;
+
+ // Render search index
+ #[cfg(feature = "search")]
+ {
+ let search = html_config.search.unwrap_or_default();
+ if search.enable {
+ super::search::create_files(&search, destination, book)?;
+ }
+ }
+
+ self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
+ .context("Unable to emit redirects")?;
+
+ // Copy all remaining files, avoid a recursive copy from/to the book build dir
+ utils::fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?;
+
+ Ok(())
+ }
+}
+
+fn make_data(
+ root: &Path,
+ book: &Book,
+ config: &Config,
+ html_config: &HtmlConfig,
+ theme: &Theme,
+) -> Result<serde_json::Map<String, serde_json::Value>> {
+ trace!("make_data");
+
+ let mut data = serde_json::Map::new();
+ data.insert(
+ "language".to_owned(),
+ json!(config.book.language.clone().unwrap_or_default()),
+ );
+ data.insert(
+ "book_title".to_owned(),
+ json!(config.book.title.clone().unwrap_or_default()),
+ );
+ data.insert(
+ "description".to_owned(),
+ json!(config.book.description.clone().unwrap_or_default()),
+ );
+ if theme.favicon_png.is_some() {
+ data.insert("favicon_png".to_owned(), json!("favicon.png"));
+ }
+ if theme.favicon_svg.is_some() {
+ data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
+ }
+ if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint {
+ data.insert(
+ "live_reload_endpoint".to_owned(),
+ json!(live_reload_endpoint),
+ );
+ }
+
+ let default_theme = match html_config.default_theme {
+ Some(ref theme) => theme.to_lowercase(),
+ None => "light".to_string(),
+ };
+ data.insert("default_theme".to_owned(), json!(default_theme));
+
+ let preferred_dark_theme = match html_config.preferred_dark_theme {
+ Some(ref theme) => theme.to_lowercase(),
+ None => "navy".to_string(),
+ };
+ data.insert(
+ "preferred_dark_theme".to_owned(),
+ json!(preferred_dark_theme),
+ );
+
+ // Add google analytics tag
+ if let Some(ref ga) = html_config.google_analytics {
+ data.insert("google_analytics".to_owned(), json!(ga));
+ }
+
+ if html_config.mathjax_support {
+ data.insert("mathjax_support".to_owned(), json!(true));
+ }
+
+ if html_config.copy_fonts {
+ data.insert("copy_fonts".to_owned(), json!(true));
+ }
+
+ // Add check to see if there is an additional style
+ if !html_config.additional_css.is_empty() {
+ let mut css = Vec::new();
+ for style in &html_config.additional_css {
+ match style.strip_prefix(root) {
+ Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
+ Err(_) => css.push(style.to_str().expect("Could not convert to str")),
+ }
+ }
+ data.insert("additional_css".to_owned(), json!(css));
+ }
+
+ // Add check to see if there is an additional script
+ if !html_config.additional_js.is_empty() {
+ let mut js = Vec::new();
+ for script in &html_config.additional_js {
+ match script.strip_prefix(root) {
+ Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
+ Err(_) => js.push(script.to_str().expect("Could not convert to str")),
+ }
+ }
+ data.insert("additional_js".to_owned(), json!(js));
+ }
+
+ if html_config.playground.editable && html_config.playground.copy_js {
+ data.insert("playground_js".to_owned(), json!(true));
+ if html_config.playground.line_numbers {
+ data.insert("playground_line_numbers".to_owned(), json!(true));
+ }
+ }
+ if html_config.playground.copyable {
+ data.insert("playground_copyable".to_owned(), json!(true));
+ }
+
+ data.insert("print_enable".to_owned(), json!(html_config.print.enable));
+ data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
+ data.insert("fold_level".to_owned(), json!(html_config.fold.level));
+
+ let search = html_config.search.clone();
+ if cfg!(feature = "search") {
+ let search = search.unwrap_or_default();
+ data.insert("search_enabled".to_owned(), json!(search.enable));
+ data.insert(
+ "search_js".to_owned(),
+ json!(search.enable && search.copy_js),
+ );
+ } else if search.is_some() {
+ warn!("mdBook compiled without search support, ignoring `output.html.search` table");
+ warn!(
+ "please reinstall with `cargo install mdbook --force --features search`to use the \
+ search feature"
+ )
+ }
+
+ if let Some(ref git_repository_url) = html_config.git_repository_url {
+ data.insert("git_repository_url".to_owned(), json!(git_repository_url));
+ }
+
+ let git_repository_icon = match html_config.git_repository_icon {
+ Some(ref git_repository_icon) => git_repository_icon,
+ None => "fa-github",
+ };
+ data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
+
+ let mut chapters = vec![];
+
+ for item in book.iter() {
+ // Create the data to inject in the template
+ let mut chapter = BTreeMap::new();
+
+ match *item {
+ BookItem::PartTitle(ref title) => {
+ chapter.insert("part".to_owned(), json!(title));
+ }
+ BookItem::Chapter(ref ch) => {
+ if let Some(ref section) = ch.number {
+ chapter.insert("section".to_owned(), json!(section.to_string()));
+ }
+
+ chapter.insert(
+ "has_sub_items".to_owned(),
+ json!((!ch.sub_items.is_empty()).to_string()),
+ );
+
+ chapter.insert("name".to_owned(), json!(ch.name));
+ if let Some(ref path) = ch.path {
+ let p = path
+ .to_str()
+ .with_context(|| "Could not convert path to str")?;
+ chapter.insert("path".to_owned(), json!(p));
+ }
+ }
+ BookItem::Separator => {
+ chapter.insert("spacer".to_owned(), json!("_spacer_"));
+ }
+ }
+
+ chapters.push(chapter);
+ }
+
+ data.insert("chapters".to_owned(), json!(chapters));
+
+ debug!("[*]: JSON constructed");
+ Ok(data)
+}
+
+/// Goes through the rendered HTML, making sure all header tags have
+/// an anchor respectively so people can link to sections directly.
+fn build_header_links(html: &str) -> String {
+ lazy_static! {
+ static ref BUILD_HEADER_LINKS: Regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
+ }
+
+ let mut id_counter = HashMap::new();
+
+ BUILD_HEADER_LINKS
+ .replace_all(html, |caps: &Captures<'_>| {
+ let level = caps[1]
+ .parse()
+ .expect("Regex should ensure we only ever get numbers here");
+
+ insert_link_into_header(level, &caps[2], &mut id_counter)
+ })
+ .into_owned()
+}
+
+/// Insert a sinle link into a header, making sure each link gets its own
+/// unique ID by appending an auto-incremented number (if necessary).
+fn insert_link_into_header(
+ level: usize,
+ content: &str,
+ id_counter: &mut HashMap<String, usize>,
+) -> String {
+ let id = utils::unique_id_from_content(content, id_counter);
+
+ format!(
+ r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##,
+ level = level,
+ id = id,
+ text = content
+ )
+}
+
+// The rust book uses annotations for rustdoc to test code snippets,
+// like the following:
+// ```rust,should_panic
+// fn main() {
+// // Code here
+// }
+// ```
+// This function replaces all commas by spaces in the code block classes
+fn fix_code_blocks(html: &str) -> String {
+ lazy_static! {
+ static ref FIX_CODE_BLOCKS: Regex =
+ Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
+ }
+
+ FIX_CODE_BLOCKS
+ .replace_all(html, |caps: &Captures<'_>| {
+ let before = &caps[1];
+ let classes = &caps[2].replace(',', " ");
+ let after = &caps[3];
+
+ format!(
+ r#"<code{before}class="{classes}"{after}>"#,
+ before = before,
+ classes = classes,
+ after = after
+ )
+ })
+ .into_owned()
+}
+
+fn add_playground_pre(
+ html: &str,
+ playground_config: &Playground,
+ edition: Option<RustEdition>,
+) -> String {
+ lazy_static! {
+ static ref ADD_PLAYGROUND_PRE: Regex =
+ Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
+ }
+ ADD_PLAYGROUND_PRE
+ .replace_all(html, |caps: &Captures<'_>| {
+ let text = &caps[1];
+ let classes = &caps[2];
+ let code = &caps[3];
+
+ if classes.contains("language-rust") {
+ if (!classes.contains("ignore")
+ && !classes.contains("noplayground")
+ && !classes.contains("noplaypen")
+ && playground_config.runnable)
+ || classes.contains("mdbook-runnable")
+ {
+ let contains_e2015 = classes.contains("edition2015");
+ let contains_e2018 = classes.contains("edition2018");
+ let contains_e2021 = classes.contains("edition2021");
+ let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
+ // the user forced edition, we should not overwrite it
+ ""
+ } else {
+ match edition {
+ Some(RustEdition::E2015) => " edition2015",
+ Some(RustEdition::E2018) => " edition2018",
+ Some(RustEdition::E2021) => " edition2021",
+ None => "",
+ }
+ };
+
+ // wrap the contents in an external pre block
+ format!(
+ "<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
+ classes,
+ edition_class,
+ {
+ let content: Cow<'_, str> = if playground_config.editable
+ && classes.contains("editable")
+ || text.contains("fn main")
+ || text.contains("quick_main!")
+ {
+ code.into()
+ } else {
+ // we need to inject our own main
+ let (attrs, code) = partition_source(code);
+
+ format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
+ .into()
+ };
+ hide_lines(&content)
+ }
+ )
+ } else {
+ format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
+ }
+ } else {
+ // not language-rust, so no-op
+ text.to_owned()
+ }
+ })
+ .into_owned()
+}
+
+fn hide_lines(content: &str) -> String {
+ lazy_static! {
+ static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
+ }
+
+ let mut result = String::with_capacity(content.len());
+ for line in content.lines() {
+ if let Some(caps) = BORING_LINES_REGEX.captures(line) {
+ if &caps[2] == "#" {
+ result += &caps[1];
+ result += &caps[2];
+ result += &caps[3];
+ result += "\n";
+ continue;
+ } else if &caps[2] != "!" && &caps[2] != "[" {
+ result += "<span class=\"boring\">";
+ result += &caps[1];
+ if &caps[2] != " " {
+ result += &caps[2];
+ }
+ result += &caps[3];
+ result += "\n";
+ result += "</span>";
+ continue;
+ }
+ }
+ result += line;
+ result += "\n";
+ }
+ result
+}
+
+fn partition_source(s: &str) -> (String, String) {
+ let mut after_header = false;
+ let mut before = String::new();
+ let mut after = String::new();
+
+ for line in s.lines() {
+ let trimline = line.trim();
+ let header = trimline.chars().all(char::is_whitespace) || trimline.starts_with("#![");
+ if !header || after_header {
+ after_header = true;
+ after.push_str(line);
+ after.push('\n');
+ } else {
+ before.push_str(line);
+ before.push('\n');
+ }
+ }
+
+ (before, after)
+}
+
+struct RenderItemContext<'a> {
+ handlebars: &'a Handlebars<'a>,
+ destination: PathBuf,
+ data: serde_json::Map<String, serde_json::Value>,
+ is_index: bool,
+ book_config: BookConfig,
+ html_config: HtmlConfig,
+ edition: Option<RustEdition>,
+ chapter_titles: &'a HashMap<PathBuf, String>,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn original_build_header_links() {
+ let inputs = vec![
+ (
+ "blah blah <h1>Foo</h1>",
+ r##"blah blah <h1 id="foo"><a class="header" href="#foo">Foo</a></h1>"##,
+ ),
+ (
+ "<h1>Foo</h1>",
+ r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1>"##,
+ ),
+ (
+ "<h3>Foo^bar</h3>",
+ r##"<h3 id="foobar"><a class="header" href="#foobar">Foo^bar</a></h3>"##,
+ ),
+ (
+ "<h4></h4>",
+ r##"<h4 id=""><a class="header" href="#"></a></h4>"##,
+ ),
+ (
+ "<h4><em>Hï</em></h4>",
+ r##"<h4 id="hï"><a class="header" href="#hï"><em>Hï</em></a></h4>"##,
+ ),
+ (
+ "<h1>Foo</h1><h3>Foo</h3>",
+ r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1><h3 id="foo-1"><a class="header" href="#foo-1">Foo</a></h3>"##,
+ ),
+ ];
+
+ for (src, should_be) in inputs {
+ let got = build_header_links(src);
+ assert_eq!(got, should_be);
+ }
+ }
+
+ #[test]
+ fn add_playground() {
+ let inputs = [
+ ("<code class=\"language-rust\">x()</code>",
+ "<pre class=\"playground\"><code class=\"language-rust\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
+ ("<code class=\"language-rust\">fn main() {}</code>",
+ "<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
+ ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
+ "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
+ ("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
+ "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
+ ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
+ "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"),
+ ("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
+ "<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code>"),
+ ("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
+ "<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"),
+ ];
+ for (src, should_be) in &inputs {
+ let got = add_playground_pre(
+ src,
+ &Playground {
+ editable: true,
+ ..Playground::default()
+ },
+ None,
+ );
+ assert_eq!(&*got, *should_be);
+ }
+ }
+ #[test]
+ fn add_playground_edition2015() {
+ let inputs = [
+ ("<code class=\"language-rust\">x()</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2015\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
+ ("<code class=\"language-rust\">fn main() {}</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
+ ("<code class=\"language-rust edition2015\">fn main() {}</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
+ ("<code class=\"language-rust edition2018\">fn main() {}</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
+ ];
+ for (src, should_be) in &inputs {
+ let got = add_playground_pre(
+ src,
+ &Playground {
+ editable: true,
+ ..Playground::default()
+ },
+ Some(RustEdition::E2015),
+ );
+ assert_eq!(&*got, *should_be);
+ }
+ }
+ #[test]
+ fn add_playground_edition2018() {
+ let inputs = [
+ ("<code class=\"language-rust\">x()</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2018\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
+ ("<code class=\"language-rust\">fn main() {}</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
+ ("<code class=\"language-rust edition2015\">fn main() {}</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
+ ("<code class=\"language-rust edition2018\">fn main() {}</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
+ ];
+ for (src, should_be) in &inputs {
+ let got = add_playground_pre(
+ src,
+ &Playground {
+ editable: true,
+ ..Playground::default()
+ },
+ Some(RustEdition::E2018),
+ );
+ assert_eq!(&*got, *should_be);
+ }
+ }
+ #[test]
+ fn add_playground_edition2021() {
+ let inputs = [
+ ("<code class=\"language-rust\">x()</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2021\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
+ ("<code class=\"language-rust\">fn main() {}</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}\n</code></pre>"),
+ ("<code class=\"language-rust edition2015\">fn main() {}</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
+ ("<code class=\"language-rust edition2018\">fn main() {}</code>",
+ "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
+ ];
+ for (src, should_be) in &inputs {
+ let got = add_playground_pre(
+ src,
+ &Playground {
+ editable: true,
+ ..Playground::default()
+ },
+ Some(RustEdition::E2021),
+ );
+ assert_eq!(&*got, *should_be);
+ }
+ }
+}
diff --git a/vendor/mdbook/src/renderer/html_handlebars/helpers/mod.rs b/vendor/mdbook/src/renderer/html_handlebars/helpers/mod.rs
new file mode 100644
index 000000000..52be6d204
--- /dev/null
+++ b/vendor/mdbook/src/renderer/html_handlebars/helpers/mod.rs
@@ -0,0 +1,3 @@
+pub mod navigation;
+pub mod theme;
+pub mod toc;
diff --git a/vendor/mdbook/src/renderer/html_handlebars/helpers/navigation.rs b/vendor/mdbook/src/renderer/html_handlebars/helpers/navigation.rs
new file mode 100644
index 000000000..65929bbfc
--- /dev/null
+++ b/vendor/mdbook/src/renderer/html_handlebars/helpers/navigation.rs
@@ -0,0 +1,290 @@
+use std::collections::BTreeMap;
+use std::path::Path;
+
+use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
+
+use crate::utils;
+
+type StringMap = BTreeMap<String, String>;
+
+/// Target for `find_chapter`.
+enum Target {
+ Previous,
+ Next,
+}
+
+impl Target {
+ /// Returns target if found.
+ fn find(
+ &self,
+ base_path: &str,
+ current_path: &str,
+ current_item: &StringMap,
+ previous_item: &StringMap,
+ ) -> Result<Option<StringMap>, RenderError> {
+ match *self {
+ Target::Next => {
+ let previous_path = previous_item
+ .get("path")
+ .ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
+
+ if previous_path == base_path {
+ return Ok(Some(current_item.clone()));
+ }
+ }
+
+ Target::Previous => {
+ if current_path == base_path {
+ return Ok(Some(previous_item.clone()));
+ }
+ }
+ }
+
+ Ok(None)
+ }
+}
+
+fn find_chapter(
+ ctx: &Context,
+ rc: &mut RenderContext<'_, '_>,
+ target: Target,
+) -> Result<Option<StringMap>, RenderError> {
+ debug!("Get data from context");
+
+ let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
+ serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone())
+ .map_err(|_| RenderError::new("Could not decode the JSON data"))
+ })?;
+
+ let base_path = rc
+ .evaluate(ctx, "@root/path")?
+ .as_json()
+ .as_str()
+ .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
+ .replace('\"', "");
+
+ if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
+ // Special case for index.md which may be a synthetic page.
+ // Target::find won't match because there is no page with the path
+ // "index.md" (unless there really is an index.md in SUMMARY.md).
+ match target {
+ Target::Previous => return Ok(None),
+ Target::Next => match chapters
+ .iter()
+ .filter(|chapter| {
+ // Skip things like "spacer"
+ chapter.contains_key("path")
+ })
+ .nth(1)
+ {
+ Some(chapter) => return Ok(Some(chapter.clone())),
+ None => return Ok(None),
+ },
+ }
+ }
+
+ let mut previous: Option<StringMap> = None;
+
+ debug!("Search for chapter");
+
+ for item in chapters {
+ match item.get("path") {
+ Some(path) if !path.is_empty() => {
+ if let Some(previous) = previous {
+ if let Some(item) = target.find(&base_path, path, &item, &previous)? {
+ return Ok(Some(item));
+ }
+ }
+
+ previous = Some(item.clone());
+ }
+ _ => continue,
+ }
+ }
+
+ Ok(None)
+}
+
+fn render(
+ _h: &Helper<'_, '_>,
+ r: &Handlebars<'_>,
+ ctx: &Context,
+ rc: &mut RenderContext<'_, '_>,
+ out: &mut dyn Output,
+ chapter: &StringMap,
+) -> Result<(), RenderError> {
+ trace!("Creating BTreeMap to inject in context");
+
+ let mut context = BTreeMap::new();
+ let base_path = rc
+ .evaluate(ctx, "@root/path")?
+ .as_json()
+ .as_str()
+ .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
+ .replace('\"', "");
+
+ context.insert(
+ "path_to_root".to_owned(),
+ json!(utils::fs::path_to_root(&base_path)),
+ );
+
+ chapter
+ .get("name")
+ .ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
+ .map(|name| context.insert("title".to_owned(), json!(name)))?;
+
+ chapter
+ .get("path")
+ .ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
+ .and_then(|p| {
+ Path::new(p)
+ .with_extension("html")
+ .to_str()
+ .ok_or_else(|| RenderError::new("Link could not be converted to str"))
+ .map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/"))))
+ })?;
+
+ trace!("Render template");
+
+ _h.template()
+ .ok_or_else(|| RenderError::new("Error with the handlebars template"))
+ .and_then(|t| {
+ let local_ctx = Context::wraps(&context)?;
+ let mut local_rc = rc.clone();
+ t.render(r, &local_ctx, &mut local_rc, out)
+ })?;
+
+ Ok(())
+}
+
+pub fn previous(
+ _h: &Helper<'_, '_>,
+ r: &Handlebars<'_>,
+ ctx: &Context,
+ rc: &mut RenderContext<'_, '_>,
+ out: &mut dyn Output,
+) -> Result<(), RenderError> {
+ trace!("previous (handlebars helper)");
+
+ if let Some(previous) = find_chapter(ctx, rc, Target::Previous)? {
+ render(_h, r, ctx, rc, out, &previous)?;
+ }
+
+ Ok(())
+}
+
+pub fn next(
+ _h: &Helper<'_, '_>,
+ r: &Handlebars<'_>,
+ ctx: &Context,
+ rc: &mut RenderContext<'_, '_>,
+ out: &mut dyn Output,
+) -> Result<(), RenderError> {
+ trace!("next (handlebars helper)");
+
+ if let Some(next) = find_chapter(ctx, rc, Target::Next)? {
+ render(_h, r, ctx, rc, out, &next)?;
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ static TEMPLATE: &str =
+ "{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
+
+ #[test]
+ fn test_next_previous() {
+ let data = json!({
+ "name": "two",
+ "path": "two.path",
+ "chapters": [
+ {
+ "name": "one",
+ "path": "one.path"
+ },
+ {
+ "name": "two",
+ "path": "two.path",
+ },
+ {
+ "name": "three",
+ "path": "three.path"
+ }
+ ]
+ });
+
+ let mut h = Handlebars::new();
+ h.register_helper("previous", Box::new(previous));
+ h.register_helper("next", Box::new(next));
+
+ assert_eq!(
+ h.render_template(TEMPLATE, &data).unwrap(),
+ "one: one.html|three: three.html"
+ );
+ }
+
+ #[test]
+ fn test_first() {
+ let data = json!({
+ "name": "one",
+ "path": "one.path",
+ "chapters": [
+ {
+ "name": "one",
+ "path": "one.path"
+ },
+ {
+ "name": "two",
+ "path": "two.path",
+ },
+ {
+ "name": "three",
+ "path": "three.path"
+ }
+ ]
+ });
+
+ let mut h = Handlebars::new();
+ h.register_helper("previous", Box::new(previous));
+ h.register_helper("next", Box::new(next));
+
+ assert_eq!(
+ h.render_template(TEMPLATE, &data).unwrap(),
+ "|two: two.html"
+ );
+ }
+ #[test]
+ fn test_last() {
+ let data = json!({
+ "name": "three",
+ "path": "three.path",
+ "chapters": [
+ {
+ "name": "one",
+ "path": "one.path"
+ },
+ {
+ "name": "two",
+ "path": "two.path",
+ },
+ {
+ "name": "three",
+ "path": "three.path"
+ }
+ ]
+ });
+
+ let mut h = Handlebars::new();
+ h.register_helper("previous", Box::new(previous));
+ h.register_helper("next", Box::new(next));
+
+ assert_eq!(
+ h.render_template(TEMPLATE, &data).unwrap(),
+ "two: two.html|"
+ );
+ }
+}
diff --git a/vendor/mdbook/src/renderer/html_handlebars/helpers/theme.rs b/vendor/mdbook/src/renderer/html_handlebars/helpers/theme.rs
new file mode 100644
index 000000000..809ee1176
--- /dev/null
+++ b/vendor/mdbook/src/renderer/html_handlebars/helpers/theme.rs
@@ -0,0 +1,28 @@
+use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
+
+pub fn theme_option(
+ h: &Helper<'_, '_>,
+ _r: &Handlebars<'_>,
+ ctx: &Context,
+ rc: &mut RenderContext<'_, '_>,
+ out: &mut dyn Output,
+) -> Result<(), RenderError> {
+ trace!("theme_option (handlebars helper)");
+
+ let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
+ RenderError::new("Param 0 with String type is required for theme_option helper.")
+ })?;
+
+ let default_theme = rc.evaluate(ctx, "@root/default_theme")?;
+ let default_theme_name = default_theme
+ .as_json()
+ .as_str()
+ .ok_or_else(|| RenderError::new("Type error for `default_theme`, string expected"))?;
+
+ out.write(param)?;
+ if param.to_lowercase() == default_theme_name.to_lowercase() {
+ out.write(" (default)")?;
+ }
+
+ Ok(())
+}
diff --git a/vendor/mdbook/src/renderer/html_handlebars/helpers/toc.rs b/vendor/mdbook/src/renderer/html_handlebars/helpers/toc.rs
new file mode 100644
index 000000000..0884d30ad
--- /dev/null
+++ b/vendor/mdbook/src/renderer/html_handlebars/helpers/toc.rs
@@ -0,0 +1,203 @@
+use std::path::Path;
+use std::{cmp::Ordering, collections::BTreeMap};
+
+use crate::utils;
+use crate::utils::bracket_escape;
+
+use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
+
+// Handlebars helper to construct TOC
+#[derive(Clone, Copy)]
+pub struct RenderToc {
+ pub no_section_label: bool,
+}
+
+impl HelperDef for RenderToc {
+ fn call<'reg: 'rc, 'rc>(
+ &self,
+ _h: &Helper<'reg, 'rc>,
+ _r: &'reg Handlebars<'_>,
+ ctx: &'rc Context,
+ rc: &mut RenderContext<'reg, 'rc>,
+ out: &mut dyn Output,
+ ) -> Result<(), RenderError> {
+ // get value from context data
+ // rc.get_path() is current json parent path, you should always use it like this
+ // param is the key of value you want to display
+ let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
+ serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone())
+ .map_err(|_| RenderError::new("Could not decode the JSON data"))
+ })?;
+ let current_path = rc
+ .evaluate(ctx, "@root/path")?
+ .as_json()
+ .as_str()
+ .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
+ .replace('\"', "");
+
+ let current_section = rc
+ .evaluate(ctx, "@root/section")?
+ .as_json()
+ .as_str()
+ .map(str::to_owned)
+ .unwrap_or_default();
+
+ let fold_enable = rc
+ .evaluate(ctx, "@root/fold_enable")?
+ .as_json()
+ .as_bool()
+ .ok_or_else(|| RenderError::new("Type error for `fold_enable`, bool expected"))?;
+
+ let fold_level = rc
+ .evaluate(ctx, "@root/fold_level")?
+ .as_json()
+ .as_u64()
+ .ok_or_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?;
+
+ out.write("<ol class=\"chapter\">")?;
+
+ let mut current_level = 1;
+ // The "index" page, which has this attribute set, is supposed to alias the first chapter in
+ // the book, i.e. the first link. There seems to be no easy way to determine which chapter
+ // the "index" is aliasing from within the renderer, so this is used instead to force the
+ // first link to be active. See further below.
+ let mut is_first_chapter = ctx.data().get("is_index").is_some();
+
+ for item in chapters {
+ // Spacer
+ if item.get("spacer").is_some() {
+ out.write("<li class=\"spacer\"></li>")?;
+ continue;
+ }
+
+ let (section, level) = if let Some(s) = item.get("section") {
+ (s.as_str(), s.matches('.').count())
+ } else {
+ ("", 1)
+ };
+
+ let is_expanded =
+ if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) {
+ // Expand if folding is disabled, or if the section is an
+ // ancestor or the current section itself.
+ true
+ } else {
+ // Levels that are larger than this would be folded.
+ level - 1 < fold_level as usize
+ };
+
+ match level.cmp(&current_level) {
+ Ordering::Greater => {
+ while level > current_level {
+ out.write("<li>")?;
+ out.write("<ol class=\"section\">")?;
+ current_level += 1;
+ }
+ write_li_open_tag(out, is_expanded, false)?;
+ }
+ Ordering::Less => {
+ while level < current_level {
+ out.write("</ol>")?;
+ out.write("</li>")?;
+ current_level -= 1;
+ }
+ write_li_open_tag(out, is_expanded, false)?;
+ }
+ Ordering::Equal => {
+ write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
+ }
+ }
+
+ // Part title
+ if let Some(title) = item.get("part") {
+ out.write("<li class=\"part-title\">")?;
+ out.write(&bracket_escape(title))?;
+ out.write("</li>")?;
+ continue;
+ }
+
+ // Link
+ let path_exists = if let Some(path) =
+ item.get("path")
+ .and_then(|p| if p.is_empty() { None } else { Some(p) })
+ {
+ out.write("<a href=\"")?;
+
+ let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
+ .with_extension("html")
+ .to_str()
+ .unwrap()
+ // Hack for windows who tends to use `\` as separator instead of `/`
+ .replace('\\', "/");
+
+ // Add link
+ out.write(&utils::fs::path_to_root(&current_path))?;
+ out.write(&tmp)?;
+ out.write("\"")?;
+
+ if path == &current_path || is_first_chapter {
+ is_first_chapter = false;
+ out.write(" class=\"active\"")?;
+ }
+
+ out.write(">")?;
+ true
+ } else {
+ out.write("<div>")?;
+ false
+ };
+
+ if !self.no_section_label {
+ // Section does not necessarily exist
+ if let Some(section) = item.get("section") {
+ out.write("<strong aria-hidden=\"true\">")?;
+ out.write(section)?;
+ out.write("</strong> ")?;
+ }
+ }
+
+ if let Some(name) = item.get("name") {
+ out.write(&bracket_escape(name))?
+ }
+
+ if path_exists {
+ out.write("</a>")?;
+ } else {
+ out.write("</div>")?;
+ }
+
+ // Render expand/collapse toggle
+ if let Some(flag) = item.get("has_sub_items") {
+ let has_sub_items = flag.parse::<bool>().unwrap_or_default();
+ if fold_enable && has_sub_items {
+ out.write("<a class=\"toggle\"><div>❱</div></a>")?;
+ }
+ }
+ out.write("</li>")?;
+ }
+ while current_level > 1 {
+ out.write("</ol>")?;
+ out.write("</li>")?;
+ current_level -= 1;
+ }
+
+ out.write("</ol>")?;
+ Ok(())
+ }
+}
+
+fn write_li_open_tag(
+ out: &mut dyn Output,
+ is_expanded: bool,
+ is_affix: bool,
+) -> Result<(), std::io::Error> {
+ let mut li = String::from("<li class=\"chapter-item ");
+ if is_expanded {
+ li.push_str("expanded ");
+ }
+ if is_affix {
+ li.push_str("affix ");
+ }
+ li.push_str("\">");
+ out.write(&li)
+}
diff --git a/vendor/mdbook/src/renderer/html_handlebars/mod.rs b/vendor/mdbook/src/renderer/html_handlebars/mod.rs
new file mode 100644
index 000000000..f1155ed75
--- /dev/null
+++ b/vendor/mdbook/src/renderer/html_handlebars/mod.rs
@@ -0,0 +1,9 @@
+#![allow(missing_docs)] // FIXME: Document this
+
+pub use self::hbs_renderer::HtmlHandlebars;
+
+mod hbs_renderer;
+mod helpers;
+
+#[cfg(feature = "search")]
+mod search;
diff --git a/vendor/mdbook/src/renderer/html_handlebars/search.rs b/vendor/mdbook/src/renderer/html_handlebars/search.rs
new file mode 100644
index 000000000..c3b944c9d
--- /dev/null
+++ b/vendor/mdbook/src/renderer/html_handlebars/search.rs
@@ -0,0 +1,286 @@
+use std::borrow::Cow;
+use std::collections::{HashMap, HashSet};
+use std::path::Path;
+
+use elasticlunr::{Index, IndexBuilder};
+use pulldown_cmark::*;
+
+use crate::book::{Book, BookItem};
+use crate::config::Search;
+use crate::errors::*;
+use crate::theme::searcher;
+use crate::utils;
+
+use serde::Serialize;
+
+const MAX_WORD_LENGTH_TO_INDEX: usize = 80;
+
+/// Tokenizes in the same way as elasticlunr-rs (for English), but also drops long tokens.
+fn tokenize(text: &str) -> Vec<String> {
+ text.split(|c: char| c.is_whitespace() || c == '-')
+ .filter(|s| !s.is_empty())
+ .map(|s| s.trim().to_lowercase())
+ .filter(|s| s.len() <= MAX_WORD_LENGTH_TO_INDEX)
+ .collect()
+}
+
+/// Creates all files required for search.
+pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
+ let mut index = IndexBuilder::new()
+ .add_field_with_tokenizer("title", Box::new(&tokenize))
+ .add_field_with_tokenizer("body", Box::new(&tokenize))
+ .add_field_with_tokenizer("breadcrumbs", Box::new(&tokenize))
+ .build();
+
+ let mut doc_urls = Vec::with_capacity(book.sections.len());
+
+ for item in book.iter() {
+ render_item(&mut index, search_config, &mut doc_urls, item)?;
+ }
+
+ let index = write_to_json(index, search_config, doc_urls)?;
+ debug!("Writing search index ✓");
+ if index.len() > 10_000_000 {
+ warn!("searchindex.json is very large ({} bytes)", index.len());
+ }
+
+ if search_config.copy_js {
+ utils::fs::write_file(destination, "searchindex.json", index.as_bytes())?;
+ utils::fs::write_file(
+ destination,
+ "searchindex.js",
+ format!("Object.assign(window.search, {});", index).as_bytes(),
+ )?;
+ utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
+ utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
+ utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
+ debug!("Copying search files ✓");
+ }
+
+ Ok(())
+}
+
+/// Uses the given arguments to construct a search document, then inserts it to the given index.
+fn add_doc(
+ index: &mut Index,
+ doc_urls: &mut Vec<String>,
+ anchor_base: &str,
+ section_id: &Option<String>,
+ items: &[&str],
+) {
+ let url = if let Some(ref id) = *section_id {
+ Cow::Owned(format!("{}#{}", anchor_base, id))
+ } else {
+ Cow::Borrowed(anchor_base)
+ };
+ let url = utils::collapse_whitespace(url.trim());
+ let doc_ref = doc_urls.len().to_string();
+ doc_urls.push(url.into());
+
+ let items = items.iter().map(|&x| utils::collapse_whitespace(x.trim()));
+ index.add_doc(&doc_ref, items);
+}
+
+/// Renders markdown into flat unformatted text and adds it to the search index.
+fn render_item(
+ index: &mut Index,
+ search_config: &Search,
+ doc_urls: &mut Vec<String>,
+ item: &BookItem,
+) -> Result<()> {
+ let chapter = match *item {
+ BookItem::Chapter(ref ch) if !ch.is_draft_chapter() => ch,
+ _ => return Ok(()),
+ };
+
+ let chapter_path = chapter
+ .path
+ .as_ref()
+ .expect("Checked that path exists above");
+ let filepath = Path::new(&chapter_path).with_extension("html");
+ let filepath = filepath
+ .to_str()
+ .with_context(|| "Could not convert HTML path to str")?;
+ let anchor_base = utils::fs::normalize_path(filepath);
+
+ let mut p = utils::new_cmark_parser(&chapter.content, false).peekable();
+
+ let mut in_heading = false;
+ let max_section_depth = u32::from(search_config.heading_split_level);
+ let mut section_id = None;
+ let mut heading = String::new();
+ let mut body = String::new();
+ let mut breadcrumbs = chapter.parent_names.clone();
+ let mut footnote_numbers = HashMap::new();
+
+ breadcrumbs.push(chapter.name.clone());
+
+ let mut id_counter = HashMap::new();
+ while let Some(event) = p.next() {
+ match event {
+ Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
+ if !heading.is_empty() {
+ // Section finished, the next heading is following now
+ // Write the data to the index, and clear it for the next section
+ add_doc(
+ index,
+ doc_urls,
+ &anchor_base,
+ &section_id,
+ &[&heading, &body, &breadcrumbs.join(" » ")],
+ );
+ section_id = None;
+ heading.clear();
+ body.clear();
+ breadcrumbs.pop();
+ }
+
+ in_heading = true;
+ }
+ Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
+ in_heading = false;
+ section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter));
+ breadcrumbs.push(heading.clone());
+ }
+ Event::Start(Tag::FootnoteDefinition(name)) => {
+ let number = footnote_numbers.len() + 1;
+ footnote_numbers.entry(name).or_insert(number);
+ }
+ Event::Html(html) => {
+ let mut html_block = html.into_string();
+
+ // As of pulldown_cmark 0.6, html events are no longer contained
+ // in an HtmlBlock tag. We must collect consecutive Html events
+ // into a block ourselves.
+ while let Some(Event::Html(html)) = p.peek() {
+ html_block.push_str(html);
+ p.next();
+ }
+
+ body.push_str(&clean_html(&html_block));
+ }
+ Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => {
+ // Insert spaces where HTML output would usually separate text
+ // to ensure words don't get merged together
+ if in_heading {
+ heading.push(' ');
+ } else {
+ body.push(' ');
+ }
+ }
+ Event::Text(text) | Event::Code(text) => {
+ if in_heading {
+ heading.push_str(&text);
+ } else {
+ body.push_str(&text);
+ }
+ }
+ Event::FootnoteReference(name) => {
+ let len = footnote_numbers.len() + 1;
+ let number = footnote_numbers.entry(name).or_insert(len);
+ body.push_str(&format!(" [{}] ", number));
+ }
+ Event::TaskListMarker(_checked) => {}
+ }
+ }
+
+ if !body.is_empty() || !heading.is_empty() {
+ if heading.is_empty() {
+ if let Some(chapter) = breadcrumbs.first() {
+ heading = chapter.clone();
+ }
+ }
+ // Make sure the last section is added to the index
+ add_doc(
+ index,
+ doc_urls,
+ &anchor_base,
+ &section_id,
+ &[&heading, &body, &breadcrumbs.join(" » ")],
+ );
+ }
+
+ Ok(())
+}
+
+fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
+ use elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
+ use std::collections::BTreeMap;
+
+ #[derive(Serialize)]
+ struct ResultsOptions {
+ limit_results: u32,
+ teaser_word_count: u32,
+ }
+
+ #[derive(Serialize)]
+ struct SearchindexJson {
+ /// The options used for displaying search results
+ results_options: ResultsOptions,
+ /// The searchoptions for elasticlunr.js
+ search_options: SearchOptions,
+ /// Used to lookup a document's URL from an integer document ref.
+ doc_urls: Vec<String>,
+ /// The index for elasticlunr.js
+ index: elasticlunr::Index,
+ }
+
+ let mut fields = BTreeMap::new();
+ let mut opt = SearchOptionsField::default();
+ let mut insert_boost = |key: &str, boost| {
+ opt.boost = Some(boost);
+ fields.insert(key.into(), opt);
+ };
+ insert_boost("title", search_config.boost_title);
+ insert_boost("body", search_config.boost_paragraph);
+ insert_boost("breadcrumbs", search_config.boost_hierarchy);
+
+ let search_options = SearchOptions {
+ bool: if search_config.use_boolean_and {
+ SearchBool::And
+ } else {
+ SearchBool::Or
+ },
+ expand: search_config.expand,
+ fields,
+ };
+
+ let results_options = ResultsOptions {
+ limit_results: search_config.limit_results,
+ teaser_word_count: search_config.teaser_word_count,
+ };
+
+ let json_contents = SearchindexJson {
+ results_options,
+ search_options,
+ doc_urls,
+ index,
+ };
+
+ // By converting to serde_json::Value as an intermediary, we use a
+ // BTreeMap internally and can force a stable ordering of map keys.
+ let json_contents = serde_json::to_value(&json_contents)?;
+ let json_contents = serde_json::to_string(&json_contents)?;
+
+ Ok(json_contents)
+}
+
+fn clean_html(html: &str) -> String {
+ lazy_static! {
+ static ref AMMONIA: ammonia::Builder<'static> = {
+ let mut clean_content = HashSet::new();
+ clean_content.insert("script");
+ clean_content.insert("style");
+ let mut builder = ammonia::Builder::new();
+ builder
+ .tags(HashSet::new())
+ .tag_attributes(HashMap::new())
+ .generic_attributes(HashSet::new())
+ .link_rel(None)
+ .allowed_classes(HashMap::new())
+ .clean_content_tags(clean_content);
+ builder
+ };
+ }
+ AMMONIA.clean(html).to_string()
+}
diff --git a/vendor/mdbook/src/renderer/markdown_renderer.rs b/vendor/mdbook/src/renderer/markdown_renderer.rs
new file mode 100644
index 000000000..bd5def1f4
--- /dev/null
+++ b/vendor/mdbook/src/renderer/markdown_renderer.rs
@@ -0,0 +1,52 @@
+use crate::book::BookItem;
+use crate::errors::*;
+use crate::renderer::{RenderContext, Renderer};
+use crate::utils;
+
+use std::fs;
+
+#[derive(Default)]
+/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
+/// when debugging preprocessors.
+pub struct MarkdownRenderer;
+
+impl MarkdownRenderer {
+ /// Create a new `MarkdownRenderer` instance.
+ pub fn new() -> Self {
+ MarkdownRenderer
+ }
+}
+
+impl Renderer for MarkdownRenderer {
+ fn name(&self) -> &str {
+ "markdown"
+ }
+
+ fn render(&self, ctx: &RenderContext) -> Result<()> {
+ let destination = &ctx.destination;
+ let book = &ctx.book;
+
+ if destination.exists() {
+ utils::fs::remove_dir_content(destination)
+ .with_context(|| "Unable to remove stale Markdown output")?;
+ }
+
+ trace!("markdown render");
+ for item in book.iter() {
+ if let BookItem::Chapter(ref ch) = *item {
+ if !ch.is_draft_chapter() {
+ utils::fs::write_file(
+ &ctx.destination,
+ &ch.path.as_ref().expect("Checked path exists before"),
+ ch.content.as_bytes(),
+ )?;
+ }
+ }
+ }
+
+ fs::create_dir_all(&destination)
+ .with_context(|| "Unexpected error when constructing destination path")?;
+
+ Ok(())
+ }
+}
diff --git a/vendor/mdbook/src/renderer/mod.rs b/vendor/mdbook/src/renderer/mod.rs
new file mode 100644
index 000000000..15465fbce
--- /dev/null
+++ b/vendor/mdbook/src/renderer/mod.rs
@@ -0,0 +1,265 @@
+//! `mdbook`'s low level rendering interface.
+//!
+//! # Note
+//!
+//! You usually don't need to work with this module directly. If you want to
+//! implement your own backend, then check out the [For Developers] section of
+//! the user guide.
+//!
+//! The definition for [RenderContext] may be useful though.
+//!
+//! [For Developers]: https://rust-lang.github.io/mdBook/for_developers/index.html
+//! [RenderContext]: struct.RenderContext.html
+
+pub use self::html_handlebars::HtmlHandlebars;
+pub use self::markdown_renderer::MarkdownRenderer;
+
+mod html_handlebars;
+mod markdown_renderer;
+
+use shlex::Shlex;
+use std::collections::HashMap;
+use std::fs;
+use std::io::{self, ErrorKind, Read};
+use std::path::{Path, PathBuf};
+use std::process::{Command, Stdio};
+
+use crate::book::Book;
+use crate::config::Config;
+use crate::errors::*;
+use toml::Value;
+
+use serde::{Deserialize, Serialize};
+
+/// An arbitrary `mdbook` backend.
+///
+/// Although it's quite possible for you to import `mdbook` as a library and
+/// provide your own renderer, there are two main renderer implementations that
+/// 99% of users will ever use:
+///
+/// - [`HtmlHandlebars`] - the built-in HTML renderer
+/// - [`CmdRenderer`] - a generic renderer which shells out to a program to do the
+/// actual rendering
+pub trait Renderer {
+ /// The `Renderer`'s name.
+ fn name(&self) -> &str;
+
+ /// Invoke the `Renderer`, passing in all the necessary information for
+ /// describing a book.
+ fn render(&self, ctx: &RenderContext) -> Result<()>;
+}
+
+/// The context provided to all renderers.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct RenderContext {
+ /// Which version of `mdbook` did this come from (as written in `mdbook`'s
+ /// `Cargo.toml`). Useful if you know the renderer is only compatible with
+ /// certain versions of `mdbook`.
+ pub version: String,
+ /// The book's root directory.
+ pub root: PathBuf,
+ /// A loaded representation of the book itself.
+ pub book: Book,
+ /// The loaded configuration file.
+ pub config: Config,
+ /// Where the renderer *must* put any build artefacts generated. To allow
+ /// renderers to cache intermediate results, this directory is not
+ /// guaranteed to be empty or even exist.
+ pub destination: PathBuf,
+ #[serde(skip)]
+ pub(crate) chapter_titles: HashMap<PathBuf, String>,
+ #[serde(skip)]
+ __non_exhaustive: (),
+}
+
+impl RenderContext {
+ /// Create a new `RenderContext`.
+ pub fn new<P, Q>(root: P, book: Book, config: Config, destination: Q) -> RenderContext
+ where
+ P: Into<PathBuf>,
+ Q: Into<PathBuf>,
+ {
+ RenderContext {
+ book,
+ config,
+ version: crate::MDBOOK_VERSION.to_string(),
+ root: root.into(),
+ destination: destination.into(),
+ chapter_titles: HashMap::new(),
+ __non_exhaustive: (),
+ }
+ }
+
+ /// Get the source directory's (absolute) path on disk.
+ pub fn source_dir(&self) -> PathBuf {
+ self.root.join(&self.config.book.src)
+ }
+
+ /// Load a `RenderContext` from its JSON representation.
+ pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> {
+ serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`")
+ }
+}
+
+/// A generic renderer which will shell out to an arbitrary executable.
+///
+/// # Rendering Protocol
+///
+/// When the renderer's `render()` method is invoked, `CmdRenderer` will spawn
+/// the `cmd` as a subprocess. The `RenderContext` is passed to the subprocess
+/// as a JSON string (using `serde_json`).
+///
+/// > **Note:** The command used doesn't necessarily need to be a single
+/// > executable (i.e. `/path/to/renderer`). The `cmd` string lets you pass
+/// > in command line arguments, so there's no reason why it couldn't be
+/// > `python /path/to/renderer --from mdbook --to epub`.
+///
+/// Anything the subprocess writes to `stdin` or `stdout` will be passed through
+/// to the user. While this gives the renderer maximum flexibility to output
+/// whatever it wants, to avoid spamming users it is recommended to avoid
+/// unnecessary output.
+///
+/// To help choose the appropriate output level, the `RUST_LOG` environment
+/// variable will be passed through to the subprocess, if set.
+///
+/// If the subprocess wishes to indicate that rendering failed, it should exit
+/// with a non-zero return code.
+#[derive(Debug, Clone, PartialEq)]
+pub struct CmdRenderer {
+ name: String,
+ cmd: String,
+}
+
+impl CmdRenderer {
+ /// Create a new `CmdRenderer` which will invoke the provided `cmd` string.
+ pub fn new(name: String, cmd: String) -> CmdRenderer {
+ CmdRenderer { name, cmd }
+ }
+
+ fn compose_command(&self, root: &Path, destination: &Path) -> Result<Command> {
+ let mut words = Shlex::new(&self.cmd);
+ let exe = match words.next() {
+ Some(e) => PathBuf::from(e),
+ None => bail!("Command string was empty"),
+ };
+
+ let exe = if exe.components().count() == 1 {
+ // Search PATH for the executable.
+ exe
+ } else {
+ // Relative paths are preferred to be relative to the book root.
+ let abs_exe = root.join(&exe);
+ if abs_exe.exists() {
+ abs_exe
+ } else {
+ // Historically paths were relative to the destination, but
+ // this is not the preferred way.
+ let legacy_path = destination.join(&exe);
+ if legacy_path.exists() {
+ warn!(
+ "Renderer command `{}` uses a path relative to the \
+ renderer output directory `{}`. This was previously \
+ accepted, but has been deprecated. Relative executable \
+ paths should be relative to the book root.",
+ exe.display(),
+ destination.display()
+ );
+ legacy_path
+ } else {
+ // Let this bubble through to later be handled by
+ // handle_render_command_error.
+ abs_exe
+ }
+ }
+ };
+
+ let mut cmd = Command::new(exe);
+
+ for arg in words {
+ cmd.arg(arg);
+ }
+
+ Ok(cmd)
+ }
+}
+
+impl CmdRenderer {
+ fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> {
+ if let ErrorKind::NotFound = error.kind() {
+ // Look for "output.{self.name}.optional".
+ // If it exists and is true, treat this as a warning.
+ // Otherwise, fail the build.
+
+ let optional_key = format!("output.{}.optional", self.name);
+
+ let is_optional = match ctx.config.get(&optional_key) {
+ Some(Value::Boolean(value)) => *value,
+ _ => false,
+ };
+
+ if is_optional {
+ warn!(
+ "The command `{}` for backend `{}` was not found, \
+ but was marked as optional.",
+ self.cmd, self.name
+ );
+ return Ok(());
+ } else {
+ error!(
+ "The command `{0}` wasn't found, is the \"{1}\" backend installed? \
+ If you want to ignore this error when the \"{1}\" backend is not installed, \
+ set `optional = true` in the `[output.{1}]` section of the book.toml configuration file.",
+ self.cmd, self.name
+ );
+ }
+ }
+ Err(error).with_context(|| "Unable to start the backend")?
+ }
+}
+
+impl Renderer for CmdRenderer {
+ fn name(&self) -> &str {
+ &self.name
+ }
+
+ fn render(&self, ctx: &RenderContext) -> Result<()> {
+ info!("Invoking the \"{}\" renderer", self.name);
+
+ let _ = fs::create_dir_all(&ctx.destination);
+
+ let mut child = match self
+ .compose_command(&ctx.root, &ctx.destination)?
+ .stdin(Stdio::piped())
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit())
+ .current_dir(&ctx.destination)
+ .spawn()
+ {
+ Ok(c) => c,
+ Err(e) => return self.handle_render_command_error(ctx, e),
+ };
+
+ let mut stdin = child.stdin.take().expect("Child has stdin");
+ if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
+ // Looks like the backend hung up before we could finish
+ // sending it the render context. Log the error and keep going
+ warn!("Error writing the RenderContext to the backend, {}", e);
+ }
+
+ // explicitly close the `stdin` file handle
+ drop(stdin);
+
+ let status = child
+ .wait()
+ .with_context(|| "Error waiting for the backend to complete")?;
+
+ trace!("{} exited with output: {:?}", self.cmd, status);
+
+ if !status.success() {
+ error!("Renderer exited with non-zero return code.");
+ bail!("The \"{}\" renderer failed", self.name);
+ } else {
+ Ok(())
+ }
+ }
+}
diff --git a/vendor/mdbook/src/theme/ayu-highlight.css b/vendor/mdbook/src/theme/ayu-highlight.css
new file mode 100644
index 000000000..32c943222
--- /dev/null
+++ b/vendor/mdbook/src/theme/ayu-highlight.css
@@ -0,0 +1,78 @@
+/*
+Based off of the Ayu theme
+Original by Dempfi (https://github.com/dempfi/ayu)
+*/
+
+.hljs {
+ display: block;
+ overflow-x: auto;
+ background: #191f26;
+ color: #e6e1cf;
+}
+
+.hljs-comment,
+.hljs-quote {
+ color: #5c6773;
+ font-style: italic;
+}
+
+.hljs-variable,
+.hljs-template-variable,
+.hljs-attribute,
+.hljs-attr,
+.hljs-regexp,
+.hljs-link,
+.hljs-selector-id,
+.hljs-selector-class {
+ color: #ff7733;
+}
+
+.hljs-number,
+.hljs-meta,
+.hljs-builtin-name,
+.hljs-literal,
+.hljs-type,
+.hljs-params {
+ color: #ffee99;
+}
+
+.hljs-string,
+.hljs-bullet {
+ color: #b8cc52;
+}
+
+.hljs-title,
+.hljs-built_in,
+.hljs-section {
+ color: #ffb454;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-symbol {
+ color: #ff7733;
+}
+
+.hljs-name {
+ color: #36a3d9;
+}
+
+.hljs-tag {
+ color: #00568d;
+}
+
+.hljs-emphasis {
+ font-style: italic;
+}
+
+.hljs-strong {
+ font-weight: bold;
+}
+
+.hljs-addition {
+ color: #91b362;
+}
+
+.hljs-deletion {
+ color: #d96c75;
+}
diff --git a/vendor/mdbook/src/theme/book.js b/vendor/mdbook/src/theme/book.js
new file mode 100644
index 000000000..d40440c72
--- /dev/null
+++ b/vendor/mdbook/src/theme/book.js
@@ -0,0 +1,679 @@
+"use strict";
+
+// Fix back button cache problem
+window.onunload = function () { };
+
+// Global variable, shared between modules
+function playground_text(playground) {
+ let code_block = playground.querySelector("code");
+
+ if (window.ace && code_block.classList.contains("editable")) {
+ let editor = window.ace.edit(code_block);
+ return editor.getValue();
+ } else {
+ return code_block.textContent;
+ }
+}
+
+(function codeSnippets() {
+ function fetch_with_timeout(url, options, timeout = 6000) {
+ return Promise.race([
+ fetch(url, options),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
+ ]);
+ }
+
+ var playgrounds = Array.from(document.querySelectorAll(".playground"));
+ if (playgrounds.length > 0) {
+ fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
+ headers: {
+ 'Content-Type': "application/json",
+ },
+ method: 'POST',
+ mode: 'cors',
+ })
+ .then(response => response.json())
+ .then(response => {
+ // get list of crates available in the rust playground
+ let playground_crates = response.crates.map(item => item["id"]);
+ playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
+ });
+ }
+
+ function handle_crate_list_update(playground_block, playground_crates) {
+ // update the play buttons after receiving the response
+ update_play_button(playground_block, playground_crates);
+
+ // and install on change listener to dynamically update ACE editors
+ if (window.ace) {
+ let code_block = playground_block.querySelector("code");
+ if (code_block.classList.contains("editable")) {
+ let editor = window.ace.edit(code_block);
+ editor.addEventListener("change", function (e) {
+ update_play_button(playground_block, playground_crates);
+ });
+ // add Ctrl-Enter command to execute rust code
+ editor.commands.addCommand({
+ name: "run",
+ bindKey: {
+ win: "Ctrl-Enter",
+ mac: "Ctrl-Enter"
+ },
+ exec: _editor => run_rust_code(playground_block)
+ });
+ }
+ }
+ }
+
+ // updates the visibility of play button based on `no_run` class and
+ // used crates vs ones available on http://play.rust-lang.org
+ function update_play_button(pre_block, playground_crates) {
+ var play_button = pre_block.querySelector(".play-button");
+
+ // skip if code is `no_run`
+ if (pre_block.querySelector('code').classList.contains("no_run")) {
+ play_button.classList.add("hidden");
+ return;
+ }
+
+ // get list of `extern crate`'s from snippet
+ var txt = playground_text(pre_block);
+ var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
+ var snippet_crates = [];
+ var item;
+ while (item = re.exec(txt)) {
+ snippet_crates.push(item[1]);
+ }
+
+ // check if all used crates are available on play.rust-lang.org
+ var all_available = snippet_crates.every(function (elem) {
+ return playground_crates.indexOf(elem) > -1;
+ });
+
+ if (all_available) {
+ play_button.classList.remove("hidden");
+ } else {
+ play_button.classList.add("hidden");
+ }
+ }
+
+ function run_rust_code(code_block) {
+ var result_block = code_block.querySelector(".result");
+ if (!result_block) {
+ result_block = document.createElement('code');
+ result_block.className = 'result hljs language-bash';
+
+ code_block.append(result_block);
+ }
+
+ let text = playground_text(code_block);
+ let classes = code_block.querySelector('code').classList;
+ let edition = "2015";
+ if(classes.contains("edition2018")) {
+ edition = "2018";
+ } else if(classes.contains("edition2021")) {
+ edition = "2021";
+ }
+ var params = {
+ version: "stable",
+ optimize: "0",
+ code: text,
+ edition: edition
+ };
+
+ if (text.indexOf("#![feature") !== -1) {
+ params.version = "nightly";
+ }
+
+ result_block.innerText = "Running...";
+
+ fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
+ headers: {
+ 'Content-Type': "application/json",
+ },
+ method: 'POST',
+ mode: 'cors',
+ body: JSON.stringify(params)
+ })
+ .then(response => response.json())
+ .then(response => {
+ if (response.result.trim() === '') {
+ result_block.innerText = "No output";
+ result_block.classList.add("result-no-output");
+ } else {
+ result_block.innerText = response.result;
+ result_block.classList.remove("result-no-output");
+ }
+ })
+ .catch(error => result_block.innerText = "Playground Communication: " + error.message);
+ }
+
+ // Syntax highlighting Configuration
+ hljs.configure({
+ tabReplace: ' ', // 4 spaces
+ languages: [], // Languages used for auto-detection
+ });
+
+ let code_nodes = Array
+ .from(document.querySelectorAll('code'))
+ // Don't highlight `inline code` blocks in headers.
+ .filter(function (node) {return !node.parentElement.classList.contains("header"); });
+
+ if (window.ace) {
+ // language-rust class needs to be removed for editable
+ // blocks or highlightjs will capture events
+ code_nodes
+ .filter(function (node) {return node.classList.contains("editable"); })
+ .forEach(function (block) { block.classList.remove('language-rust'); });
+
+ Array
+ code_nodes
+ .filter(function (node) {return !node.classList.contains("editable"); })
+ .forEach(function (block) { hljs.highlightBlock(block); });
+ } else {
+ code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
+ }
+
+ // Adding the hljs class gives code blocks the color css
+ // even if highlighting doesn't apply
+ code_nodes.forEach(function (block) { block.classList.add('hljs'); });
+
+ Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
+
+ var lines = Array.from(block.querySelectorAll('.boring'));
+ // If no lines were hidden, return
+ if (!lines.length) { return; }
+ block.classList.add("hide-boring");
+
+ var buttons = document.createElement('div');
+ buttons.className = 'buttons';
+ buttons.innerHTML = "<button class=\"fa fa-eye\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
+
+ // add expand button
+ var pre_block = block.parentNode;
+ pre_block.insertBefore(buttons, pre_block.firstChild);
+
+ pre_block.querySelector('.buttons').addEventListener('click', function (e) {
+ if (e.target.classList.contains('fa-eye')) {
+ e.target.classList.remove('fa-eye');
+ e.target.classList.add('fa-eye-slash');
+ e.target.title = 'Hide lines';
+ e.target.setAttribute('aria-label', e.target.title);
+
+ block.classList.remove('hide-boring');
+ } else if (e.target.classList.contains('fa-eye-slash')) {
+ e.target.classList.remove('fa-eye-slash');
+ e.target.classList.add('fa-eye');
+ e.target.title = 'Show hidden lines';
+ e.target.setAttribute('aria-label', e.target.title);
+
+ block.classList.add('hide-boring');
+ }
+ });
+ });
+
+ if (window.playground_copyable) {
+ Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
+ var pre_block = block.parentNode;
+ if (!pre_block.classList.contains('playground')) {
+ var buttons = pre_block.querySelector(".buttons");
+ if (!buttons) {
+ buttons = document.createElement('div');
+ buttons.className = 'buttons';
+ pre_block.insertBefore(buttons, pre_block.firstChild);
+ }
+
+ var clipButton = document.createElement('button');
+ clipButton.className = 'fa fa-copy clip-button';
+ clipButton.title = 'Copy to clipboard';
+ clipButton.setAttribute('aria-label', clipButton.title);
+ clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
+
+ buttons.insertBefore(clipButton, buttons.firstChild);
+ }
+ });
+ }
+
+ // Process playground code blocks
+ Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
+ // Add play button
+ var buttons = pre_block.querySelector(".buttons");
+ if (!buttons) {
+ buttons = document.createElement('div');
+ buttons.className = 'buttons';
+ pre_block.insertBefore(buttons, pre_block.firstChild);
+ }
+
+ var runCodeButton = document.createElement('button');
+ runCodeButton.className = 'fa fa-play play-button';
+ runCodeButton.hidden = true;
+ runCodeButton.title = 'Run this code';
+ runCodeButton.setAttribute('aria-label', runCodeButton.title);
+
+ buttons.insertBefore(runCodeButton, buttons.firstChild);
+ runCodeButton.addEventListener('click', function (e) {
+ run_rust_code(pre_block);
+ });
+
+ if (window.playground_copyable) {
+ var copyCodeClipboardButton = document.createElement('button');
+ copyCodeClipboardButton.className = 'fa fa-copy clip-button';
+ copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
+ copyCodeClipboardButton.title = 'Copy to clipboard';
+ copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
+
+ buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
+ }
+
+ let code_block = pre_block.querySelector("code");
+ if (window.ace && code_block.classList.contains("editable")) {
+ var undoChangesButton = document.createElement('button');
+ undoChangesButton.className = 'fa fa-history reset-button';
+ undoChangesButton.title = 'Undo changes';
+ undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
+
+ buttons.insertBefore(undoChangesButton, buttons.firstChild);
+
+ undoChangesButton.addEventListener('click', function () {
+ let editor = window.ace.edit(code_block);
+ editor.setValue(editor.originalCode);
+ editor.clearSelection();
+ });
+ }
+ });
+})();
+
+(function themes() {
+ var html = document.querySelector('html');
+ var themeToggleButton = document.getElementById('theme-toggle');
+ var themePopup = document.getElementById('theme-list');
+ var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
+ var stylesheets = {
+ ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
+ tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
+ highlight: document.querySelector("[href$='highlight.css']"),
+ };
+
+ function showThemes() {
+ themePopup.style.display = 'block';
+ themeToggleButton.setAttribute('aria-expanded', true);
+ themePopup.querySelector("button#" + get_theme()).focus();
+ }
+
+ function hideThemes() {
+ themePopup.style.display = 'none';
+ themeToggleButton.setAttribute('aria-expanded', false);
+ themeToggleButton.focus();
+ }
+
+ function get_theme() {
+ var theme;
+ try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
+ if (theme === null || theme === undefined) {
+ return default_theme;
+ } else {
+ return theme;
+ }
+ }
+
+ function set_theme(theme, store = true) {
+ let ace_theme;
+
+ if (theme == 'coal' || theme == 'navy') {
+ stylesheets.ayuHighlight.disabled = true;
+ stylesheets.tomorrowNight.disabled = false;
+ stylesheets.highlight.disabled = true;
+
+ ace_theme = "ace/theme/tomorrow_night";
+ } else if (theme == 'ayu') {
+ stylesheets.ayuHighlight.disabled = false;
+ stylesheets.tomorrowNight.disabled = true;
+ stylesheets.highlight.disabled = true;
+ ace_theme = "ace/theme/tomorrow_night";
+ } else {
+ stylesheets.ayuHighlight.disabled = true;
+ stylesheets.tomorrowNight.disabled = true;
+ stylesheets.highlight.disabled = false;
+ ace_theme = "ace/theme/dawn";
+ }
+
+ setTimeout(function () {
+ themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor;
+ }, 1);
+
+ if (window.ace && window.editors) {
+ window.editors.forEach(function (editor) {
+ editor.setTheme(ace_theme);
+ });
+ }
+
+ var previousTheme = get_theme();
+
+ if (store) {
+ try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
+ }
+
+ html.classList.remove(previousTheme);
+ html.classList.add(theme);
+ }
+
+ // Set theme
+ var theme = get_theme();
+
+ set_theme(theme, false);
+
+ themeToggleButton.addEventListener('click', function () {
+ if (themePopup.style.display === 'block') {
+ hideThemes();
+ } else {
+ showThemes();
+ }
+ });
+
+ themePopup.addEventListener('click', function (e) {
+ var theme;
+ if (e.target.className === "theme") {
+ theme = e.target.id;
+ } else if (e.target.parentElement.className === "theme") {
+ theme = e.target.parentElement.id;
+ } else {
+ return;
+ }
+ set_theme(theme);
+ });
+
+ themePopup.addEventListener('focusout', function(e) {
+ // e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
+ if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
+ hideThemes();
+ }
+ });
+
+ // Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
+ document.addEventListener('click', function(e) {
+ if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
+ hideThemes();
+ }
+ });
+
+ document.addEventListener('keydown', function (e) {
+ if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
+ if (!themePopup.contains(e.target)) { return; }
+
+ switch (e.key) {
+ case 'Escape':
+ e.preventDefault();
+ hideThemes();
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ var li = document.activeElement.parentElement;
+ if (li && li.previousElementSibling) {
+ li.previousElementSibling.querySelector('button').focus();
+ }
+ break;
+ case 'ArrowDown':
+ e.preventDefault();
+ var li = document.activeElement.parentElement;
+ if (li && li.nextElementSibling) {
+ li.nextElementSibling.querySelector('button').focus();
+ }
+ break;
+ case 'Home':
+ e.preventDefault();
+ themePopup.querySelector('li:first-child button').focus();
+ break;
+ case 'End':
+ e.preventDefault();
+ themePopup.querySelector('li:last-child button').focus();
+ break;
+ }
+ });
+})();
+
+(function sidebar() {
+ var html = document.querySelector("html");
+ var sidebar = document.getElementById("sidebar");
+ var sidebarLinks = document.querySelectorAll('#sidebar a');
+ var sidebarToggleButton = document.getElementById("sidebar-toggle");
+ var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
+ var firstContact = null;
+
+ function showSidebar() {
+ html.classList.remove('sidebar-hidden')
+ html.classList.add('sidebar-visible');
+ Array.from(sidebarLinks).forEach(function (link) {
+ link.setAttribute('tabIndex', 0);
+ });
+ sidebarToggleButton.setAttribute('aria-expanded', true);
+ sidebar.setAttribute('aria-hidden', false);
+ try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
+ }
+
+
+ var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
+
+ function toggleSection(ev) {
+ ev.currentTarget.parentElement.classList.toggle('expanded');
+ }
+
+ Array.from(sidebarAnchorToggles).forEach(function (el) {
+ el.addEventListener('click', toggleSection);
+ });
+
+ function hideSidebar() {
+ html.classList.remove('sidebar-visible')
+ html.classList.add('sidebar-hidden');
+ Array.from(sidebarLinks).forEach(function (link) {
+ link.setAttribute('tabIndex', -1);
+ });
+ sidebarToggleButton.setAttribute('aria-expanded', false);
+ sidebar.setAttribute('aria-hidden', true);
+ try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { }
+ }
+
+ // Toggle sidebar
+ sidebarToggleButton.addEventListener('click', function sidebarToggle() {
+ if (html.classList.contains("sidebar-hidden")) {
+ var current_width = parseInt(
+ document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
+ if (current_width < 150) {
+ document.documentElement.style.setProperty('--sidebar-width', '150px');
+ }
+ showSidebar();
+ } else if (html.classList.contains("sidebar-visible")) {
+ hideSidebar();
+ } else {
+ if (getComputedStyle(sidebar)['transform'] === 'none') {
+ hideSidebar();
+ } else {
+ showSidebar();
+ }
+ }
+ });
+
+ sidebarResizeHandle.addEventListener('mousedown', initResize, false);
+
+ function initResize(e) {
+ window.addEventListener('mousemove', resize, false);
+ window.addEventListener('mouseup', stopResize, false);
+ html.classList.add('sidebar-resizing');
+ }
+ function resize(e) {
+ var pos = (e.clientX - sidebar.offsetLeft);
+ if (pos < 20) {
+ hideSidebar();
+ } else {
+ if (html.classList.contains("sidebar-hidden")) {
+ showSidebar();
+ }
+ pos = Math.min(pos, window.innerWidth - 100);
+ document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
+ }
+ }
+ //on mouseup remove windows functions mousemove & mouseup
+ function stopResize(e) {
+ html.classList.remove('sidebar-resizing');
+ window.removeEventListener('mousemove', resize, false);
+ window.removeEventListener('mouseup', stopResize, false);
+ }
+
+ document.addEventListener('touchstart', function (e) {
+ firstContact = {
+ x: e.touches[0].clientX,
+ time: Date.now()
+ };
+ }, { passive: true });
+
+ document.addEventListener('touchmove', function (e) {
+ if (!firstContact)
+ return;
+
+ var curX = e.touches[0].clientX;
+ var xDiff = curX - firstContact.x,
+ tDiff = Date.now() - firstContact.time;
+
+ if (tDiff < 250 && Math.abs(xDiff) >= 150) {
+ if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300))
+ showSidebar();
+ else if (xDiff < 0 && curX < 300)
+ hideSidebar();
+
+ firstContact = null;
+ }
+ }, { passive: true });
+
+ // Scroll sidebar to current active section
+ var activeSection = document.getElementById("sidebar").querySelector(".active");
+ if (activeSection) {
+ // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
+ activeSection.scrollIntoView({ block: 'center' });
+ }
+})();
+
+(function chapterNavigation() {
+ document.addEventListener('keydown', function (e) {
+ if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
+ if (window.search && window.search.hasFocus()) { return; }
+
+ switch (e.key) {
+ case 'ArrowRight':
+ e.preventDefault();
+ var nextButton = document.querySelector('.nav-chapters.next');
+ if (nextButton) {
+ window.location.href = nextButton.href;
+ }
+ break;
+ case 'ArrowLeft':
+ e.preventDefault();
+ var previousButton = document.querySelector('.nav-chapters.previous');
+ if (previousButton) {
+ window.location.href = previousButton.href;
+ }
+ break;
+ }
+ });
+})();
+
+(function clipboard() {
+ var clipButtons = document.querySelectorAll('.clip-button');
+
+ function hideTooltip(elem) {
+ elem.firstChild.innerText = "";
+ elem.className = 'fa fa-copy clip-button';
+ }
+
+ function showTooltip(elem, msg) {
+ elem.firstChild.innerText = msg;
+ elem.className = 'fa fa-copy tooltipped';
+ }
+
+ var clipboardSnippets = new ClipboardJS('.clip-button', {
+ text: function (trigger) {
+ hideTooltip(trigger);
+ let playground = trigger.closest("pre");
+ return playground_text(playground);
+ }
+ });
+
+ Array.from(clipButtons).forEach(function (clipButton) {
+ clipButton.addEventListener('mouseout', function (e) {
+ hideTooltip(e.currentTarget);
+ });
+ });
+
+ clipboardSnippets.on('success', function (e) {
+ e.clearSelection();
+ showTooltip(e.trigger, "Copied!");
+ });
+
+ clipboardSnippets.on('error', function (e) {
+ showTooltip(e.trigger, "Clipboard error!");
+ });
+})();
+
+(function scrollToTop () {
+ var menuTitle = document.querySelector('.menu-title');
+
+ menuTitle.addEventListener('click', function () {
+ document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
+ });
+})();
+
+(function controllMenu() {
+ var menu = document.getElementById('menu-bar');
+
+ (function controllPosition() {
+ var scrollTop = document.scrollingElement.scrollTop;
+ var prevScrollTop = scrollTop;
+ var minMenuY = -menu.clientHeight - 50;
+ // When the script loads, the page can be at any scroll (e.g. if you reforesh it).
+ menu.style.top = scrollTop + 'px';
+ // Same as parseInt(menu.style.top.slice(0, -2), but faster
+ var topCache = menu.style.top.slice(0, -2);
+ menu.classList.remove('sticky');
+ var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
+ document.addEventListener('scroll', function () {
+ scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
+ // `null` means that it doesn't need to be updated
+ var nextSticky = null;
+ var nextTop = null;
+ var scrollDown = scrollTop > prevScrollTop;
+ var menuPosAbsoluteY = topCache - scrollTop;
+ if (scrollDown) {
+ nextSticky = false;
+ if (menuPosAbsoluteY > 0) {
+ nextTop = prevScrollTop;
+ }
+ } else {
+ if (menuPosAbsoluteY > 0) {
+ nextSticky = true;
+ } else if (menuPosAbsoluteY < minMenuY) {
+ nextTop = prevScrollTop + minMenuY;
+ }
+ }
+ if (nextSticky === true && stickyCache === false) {
+ menu.classList.add('sticky');
+ stickyCache = true;
+ } else if (nextSticky === false && stickyCache === true) {
+ menu.classList.remove('sticky');
+ stickyCache = false;
+ }
+ if (nextTop !== null) {
+ menu.style.top = nextTop + 'px';
+ topCache = nextTop;
+ }
+ prevScrollTop = scrollTop;
+ }, { passive: true });
+ })();
+ (function controllBorder() {
+ menu.classList.remove('bordered');
+ document.addEventListener('scroll', function () {
+ if (menu.offsetTop === 0) {
+ menu.classList.remove('bordered');
+ } else {
+ menu.classList.add('bordered');
+ }
+ }, { passive: true });
+ })();
+})();
diff --git a/vendor/mdbook/src/theme/css/chrome.css b/vendor/mdbook/src/theme/css/chrome.css
new file mode 100644
index 000000000..10fa4b365
--- /dev/null
+++ b/vendor/mdbook/src/theme/css/chrome.css
@@ -0,0 +1,534 @@
+/* CSS for UI elements (a.k.a. chrome) */
+
+@import 'variables.css';
+
+::-webkit-scrollbar {
+ background: var(--bg);
+}
+::-webkit-scrollbar-thumb {
+ background: var(--scrollbar);
+}
+html {
+ scrollbar-color: var(--scrollbar) var(--bg);
+}
+#searchresults a,
+.content a:link,
+a:visited,
+a > .hljs {
+ color: var(--links);
+}
+
+/* Menu Bar */
+
+#menu-bar,
+#menu-bar-hover-placeholder {
+ z-index: 101;
+ margin: auto calc(0px - var(--page-padding));
+}
+#menu-bar {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ background-color: var(--bg);
+ border-bottom-color: var(--bg);
+ border-bottom-width: 1px;
+ border-bottom-style: solid;
+}
+#menu-bar.sticky,
+.js #menu-bar-hover-placeholder:hover + #menu-bar,
+.js #menu-bar:hover,
+.js.sidebar-visible #menu-bar {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0 !important;
+}
+#menu-bar-hover-placeholder {
+ position: sticky;
+ position: -webkit-sticky;
+ top: 0;
+ height: var(--menu-bar-height);
+}
+#menu-bar.bordered {
+ border-bottom-color: var(--table-border-color);
+}
+#menu-bar i, #menu-bar .icon-button {
+ position: relative;
+ padding: 0 8px;
+ z-index: 10;
+ line-height: var(--menu-bar-height);
+ cursor: pointer;
+ transition: color 0.5s;
+}
+@media only screen and (max-width: 420px) {
+ #menu-bar i, #menu-bar .icon-button {
+ padding: 0 5px;
+ }
+}
+
+.icon-button {
+ border: none;
+ background: none;
+ padding: 0;
+ color: inherit;
+}
+.icon-button i {
+ margin: 0;
+}
+
+.right-buttons {
+ margin: 0 15px;
+}
+.right-buttons a {
+ text-decoration: none;
+}
+
+.left-buttons {
+ display: flex;
+ margin: 0 5px;
+}
+.no-js .left-buttons {
+ display: none;
+}
+
+.menu-title {
+ display: inline-block;
+ font-weight: 200;
+ font-size: 2.4rem;
+ line-height: var(--menu-bar-height);
+ text-align: center;
+ margin: 0;
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.js .menu-title {
+ cursor: pointer;
+}
+
+.menu-bar,
+.menu-bar:visited,
+.nav-chapters,
+.nav-chapters:visited,
+.mobile-nav-chapters,
+.mobile-nav-chapters:visited,
+.menu-bar .icon-button,
+.menu-bar a i {
+ color: var(--icons);
+}
+
+.menu-bar i:hover,
+.menu-bar .icon-button:hover,
+.nav-chapters:hover,
+.mobile-nav-chapters i:hover {
+ color: var(--icons-hover);
+}
+
+/* Nav Icons */
+
+.nav-chapters {
+ font-size: 2.5em;
+ text-align: center;
+ text-decoration: none;
+
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ margin: 0;
+ max-width: 150px;
+ min-width: 90px;
+
+ display: flex;
+ justify-content: center;
+ align-content: center;
+ flex-direction: column;
+
+ transition: color 0.5s, background-color 0.5s;
+}
+
+.nav-chapters:hover {
+ text-decoration: none;
+ background-color: var(--theme-hover);
+ transition: background-color 0.15s, color 0.15s;
+}
+
+.nav-wrapper {
+ margin-top: 50px;
+ display: none;
+}
+
+.mobile-nav-chapters {
+ font-size: 2.5em;
+ text-align: center;
+ text-decoration: none;
+ width: 90px;
+ border-radius: 5px;
+ background-color: var(--sidebar-bg);
+}
+
+.previous {
+ float: left;
+}
+
+.next {
+ float: right;
+ right: var(--page-padding);
+}
+
+@media only screen and (max-width: 1080px) {
+ .nav-wide-wrapper { display: none; }
+ .nav-wrapper { display: block; }
+}
+
+@media only screen and (max-width: 1380px) {
+ .sidebar-visible .nav-wide-wrapper { display: none; }
+ .sidebar-visible .nav-wrapper { display: block; }
+}
+
+/* Inline code */
+
+:not(pre) > .hljs {
+ display: inline;
+ padding: 0.1em 0.3em;
+ border-radius: 3px;
+}
+
+:not(pre):not(a) > .hljs {
+ color: var(--inline-code-color);
+ overflow-x: initial;
+}
+
+a:hover > .hljs {
+ text-decoration: underline;
+}
+
+pre {
+ position: relative;
+}
+pre > .buttons {
+ position: absolute;
+ z-index: 100;
+ right: 0px;
+ top: 2px;
+ margin: 0px;
+ padding: 2px 0px;
+
+ color: var(--sidebar-fg);
+ cursor: pointer;
+ visibility: hidden;
+ opacity: 0;
+ transition: visibility 0.1s linear, opacity 0.1s linear;
+}
+pre:hover > .buttons {
+ visibility: visible;
+ opacity: 1
+}
+pre > .buttons :hover {
+ color: var(--sidebar-active);
+ border-color: var(--icons-hover);
+ background-color: var(--theme-hover);
+}
+pre > .buttons i {
+ margin-left: 8px;
+}
+pre > .buttons button {
+ cursor: inherit;
+ margin: 0px 5px;
+ padding: 3px 5px;
+ font-size: 14px;
+
+ border-style: solid;
+ border-width: 1px;
+ border-radius: 4px;
+ border-color: var(--icons);
+ background-color: var(--theme-popup-bg);
+ transition: 100ms;
+ transition-property: color,border-color,background-color;
+ color: var(--icons);
+}
+@media (pointer: coarse) {
+ pre > .buttons button {
+ /* On mobile, make it easier to tap buttons. */
+ padding: 0.3rem 1rem;
+ }
+}
+pre > code {
+ padding: 1rem;
+}
+
+/* FIXME: ACE editors overlap their buttons because ACE does absolute
+ positioning within the code block which breaks padding. The only solution I
+ can think of is to move the padding to the outer pre tag (or insert a div
+ wrapper), but that would require fixing a whole bunch of CSS rules.
+*/
+.hljs.ace_editor {
+ padding: 0rem 0rem;
+}
+
+pre > .result {
+ margin-top: 10px;
+}
+
+/* Search */
+
+#searchresults a {
+ text-decoration: none;
+}
+
+mark {
+ border-radius: 2px;
+ padding: 0 3px 1px 3px;
+ margin: 0 -3px -1px -3px;
+ background-color: var(--search-mark-bg);
+ transition: background-color 300ms linear;
+ cursor: pointer;
+}
+
+mark.fade-out {
+ background-color: rgba(0,0,0,0) !important;
+ cursor: auto;
+}
+
+.searchbar-outer {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: var(--content-max-width);
+}
+
+#searchbar {
+ width: 100%;
+ margin: 5px auto 0px auto;
+ padding: 10px 16px;
+ transition: box-shadow 300ms ease-in-out;
+ border: 1px solid var(--searchbar-border-color);
+ border-radius: 3px;
+ background-color: var(--searchbar-bg);
+ color: var(--searchbar-fg);
+}
+#searchbar:focus,
+#searchbar.active {
+ box-shadow: 0 0 3px var(--searchbar-shadow-color);
+}
+
+.searchresults-header {
+ font-weight: bold;
+ font-size: 1em;
+ padding: 18px 0 0 5px;
+ color: var(--searchresults-header-fg);
+}
+
+.searchresults-outer {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: var(--content-max-width);
+ border-bottom: 1px dashed var(--searchresults-border-color);
+}
+
+ul#searchresults {
+ list-style: none;
+ padding-left: 20px;
+}
+ul#searchresults li {
+ margin: 10px 0px;
+ padding: 2px;
+ border-radius: 2px;
+}
+ul#searchresults li.focus {
+ background-color: var(--searchresults-li-bg);
+}
+ul#searchresults span.teaser {
+ display: block;
+ clear: both;
+ margin: 5px 0 0 20px;
+ font-size: 0.8em;
+}
+ul#searchresults span.teaser em {
+ font-weight: bold;
+ font-style: normal;
+}
+
+/* Sidebar */
+
+.sidebar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: var(--sidebar-width);
+ font-size: 0.875em;
+ box-sizing: border-box;
+ -webkit-overflow-scrolling: touch;
+ overscroll-behavior-y: contain;
+ background-color: var(--sidebar-bg);
+ color: var(--sidebar-fg);
+}
+.sidebar-resizing {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.js:not(.sidebar-resizing) .sidebar {
+ transition: transform 0.3s; /* Animation: slide away */
+}
+.sidebar code {
+ line-height: 2em;
+}
+.sidebar .sidebar-scrollbox {
+ overflow-y: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 10px 10px;
+}
+.sidebar .sidebar-resize-handle {
+ position: absolute;
+ cursor: col-resize;
+ width: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+}
+.js .sidebar .sidebar-resize-handle {
+ cursor: col-resize;
+ width: 5px;
+}
+.sidebar-hidden .sidebar {
+ transform: translateX(calc(0px - var(--sidebar-width)));
+}
+.sidebar::-webkit-scrollbar {
+ background: var(--sidebar-bg);
+}
+.sidebar::-webkit-scrollbar-thumb {
+ background: var(--scrollbar);
+}
+
+.sidebar-visible .page-wrapper {
+ transform: translateX(var(--sidebar-width));
+}
+@media only screen and (min-width: 620px) {
+ .sidebar-visible .page-wrapper {
+ transform: none;
+ margin-left: var(--sidebar-width);
+ }
+}
+
+.chapter {
+ list-style: none outside none;
+ padding-left: 0;
+ line-height: 2.2em;
+}
+
+.chapter ol {
+ width: 100%;
+}
+
+.chapter li {
+ display: flex;
+ color: var(--sidebar-non-existant);
+}
+.chapter li a {
+ display: block;
+ padding: 0;
+ text-decoration: none;
+ color: var(--sidebar-fg);
+}
+
+.chapter li a:hover {
+ color: var(--sidebar-active);
+}
+
+.chapter li a.active {
+ color: var(--sidebar-active);
+}
+
+.chapter li > a.toggle {
+ cursor: pointer;
+ display: block;
+ margin-left: auto;
+ padding: 0 10px;
+ user-select: none;
+ opacity: 0.68;
+}
+
+.chapter li > a.toggle div {
+ transition: transform 0.5s;
+}
+
+/* collapse the section */
+.chapter li:not(.expanded) + li > ol {
+ display: none;
+}
+
+.chapter li.chapter-item {
+ line-height: 1.5em;
+ margin-top: 0.6em;
+}
+
+.chapter li.expanded > a.toggle div {
+ transform: rotate(90deg);
+}
+
+.spacer {
+ width: 100%;
+ height: 3px;
+ margin: 5px 0px;
+}
+.chapter .spacer {
+ background-color: var(--sidebar-spacer);
+}
+
+@media (-moz-touch-enabled: 1), (pointer: coarse) {
+ .chapter li a { padding: 5px 0; }
+ .spacer { margin: 10px 0; }
+}
+
+.section {
+ list-style: none outside none;
+ padding-left: 20px;
+ line-height: 1.9em;
+}
+
+/* Theme Menu Popup */
+
+.theme-popup {
+ position: absolute;
+ left: 10px;
+ top: var(--menu-bar-height);
+ z-index: 1000;
+ border-radius: 4px;
+ font-size: 0.7em;
+ color: var(--fg);
+ background: var(--theme-popup-bg);
+ border: 1px solid var(--theme-popup-border);
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ display: none;
+}
+.theme-popup .default {
+ color: var(--icons);
+}
+.theme-popup .theme {
+ width: 100%;
+ border: 0;
+ margin: 0;
+ padding: 2px 10px;
+ line-height: 25px;
+ white-space: nowrap;
+ text-align: left;
+ cursor: pointer;
+ color: inherit;
+ background: inherit;
+ font-size: inherit;
+}
+.theme-popup .theme:hover {
+ background-color: var(--theme-hover);
+}
+.theme-popup .theme:hover:first-child,
+.theme-popup .theme:hover:last-child {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
diff --git a/vendor/mdbook/src/theme/css/general.css b/vendor/mdbook/src/theme/css/general.css
new file mode 100644
index 000000000..0e4f07a50
--- /dev/null
+++ b/vendor/mdbook/src/theme/css/general.css
@@ -0,0 +1,191 @@
+/* Base styles and content styles */
+
+@import 'variables.css';
+
+:root {
+ /* Browser default font-size is 16px, this way 1 rem = 10px */
+ font-size: 62.5%;
+}
+
+html {
+ font-family: "Open Sans", sans-serif;
+ color: var(--fg);
+ background-color: var(--bg);
+ text-size-adjust: none;
+ -webkit-text-size-adjust: none;
+}
+
+body {
+ margin: 0;
+ font-size: 1.6rem;
+ overflow-x: hidden;
+}
+
+code {
+ font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important;
+ font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
+}
+
+/* make long words/inline code not x overflow */
+main {
+ overflow-wrap: break-word;
+}
+
+/* make wide tables scroll if they overflow */
+.table-wrapper {
+ overflow-x: auto;
+}
+
+/* Don't change font size in headers. */
+h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
+ font-size: unset;
+}
+
+.left { float: left; }
+.right { float: right; }
+.boring { opacity: 0.6; }
+.hide-boring .boring { display: none; }
+.hidden { display: none !important; }
+
+h2, h3 { margin-top: 2.5em; }
+h4, h5 { margin-top: 2em; }
+
+.header + .header h3,
+.header + .header h4,
+.header + .header h5 {
+ margin-top: 1em;
+}
+
+h1:target::before,
+h2:target::before,
+h3:target::before,
+h4:target::before,
+h5:target::before,
+h6:target::before {
+ display: inline-block;
+ content: "»";
+ margin-left: -30px;
+ width: 30px;
+}
+
+/* This is broken on Safari as of version 14, but is fixed
+ in Safari Technology Preview 117 which I think will be Safari 14.2.
+ https://bugs.webkit.org/show_bug.cgi?id=218076
+*/
+:target {
+ scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
+}
+
+.page {
+ outline: 0;
+ padding: 0 var(--page-padding);
+ margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
+}
+.page-wrapper {
+ box-sizing: border-box;
+}
+.js:not(.sidebar-resizing) .page-wrapper {
+ transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
+}
+
+.content {
+ overflow-y: auto;
+ padding: 0 5px 50px 5px;
+}
+.content main {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: var(--content-max-width);
+}
+.content p { line-height: 1.45em; }
+.content ol { line-height: 1.45em; }
+.content ul { line-height: 1.45em; }
+.content a { text-decoration: none; }
+.content a:hover { text-decoration: underline; }
+.content img, .content video { max-width: 100%; }
+.content .header:link,
+.content .header:visited {
+ color: var(--fg);
+}
+.content .header:link,
+.content .header:visited:hover {
+ text-decoration: none;
+}
+
+table {
+ margin: 0 auto;
+ border-collapse: collapse;
+}
+table td {
+ padding: 3px 20px;
+ border: 1px var(--table-border-color) solid;
+}
+table thead {
+ background: var(--table-header-bg);
+}
+table thead td {
+ font-weight: 700;
+ border: none;
+}
+table thead th {
+ padding: 3px 20px;
+}
+table thead tr {
+ border: 1px var(--table-header-bg) solid;
+}
+/* Alternate background colors for rows */
+table tbody tr:nth-child(2n) {
+ background: var(--table-alternate-bg);
+}
+
+
+blockquote {
+ margin: 20px 0;
+ padding: 0 20px;
+ color: var(--fg);
+ background-color: var(--quote-bg);
+ border-top: .1em solid var(--quote-border);
+ border-bottom: .1em solid var(--quote-border);
+}
+
+
+:not(.footnote-definition) + .footnote-definition,
+.footnote-definition + :not(.footnote-definition) {
+ margin-top: 2em;
+}
+.footnote-definition {
+ font-size: 0.9em;
+ margin: 0.5em 0;
+}
+.footnote-definition p {
+ display: inline;
+}
+
+.tooltiptext {
+ position: absolute;
+ visibility: hidden;
+ color: #fff;
+ background-color: #333;
+ transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
+ left: -8px; /* Half of the width of the icon */
+ top: -35px;
+ font-size: 0.8em;
+ text-align: center;
+ border-radius: 6px;
+ padding: 5px 8px;
+ margin: 5px;
+ z-index: 1000;
+}
+.tooltipped .tooltiptext {
+ visibility: visible;
+}
+
+.chapter li.part-title {
+ color: var(--sidebar-fg);
+ margin: 5px 0px;
+ font-weight: bold;
+}
+
+.result-no-output {
+ font-style: italic;
+}
diff --git a/vendor/mdbook/src/theme/css/print.css b/vendor/mdbook/src/theme/css/print.css
new file mode 100644
index 000000000..5e690f755
--- /dev/null
+++ b/vendor/mdbook/src/theme/css/print.css
@@ -0,0 +1,54 @@
+
+#sidebar,
+#menu-bar,
+.nav-chapters,
+.mobile-nav-chapters {
+ display: none;
+}
+
+#page-wrapper.page-wrapper {
+ transform: none;
+ margin-left: 0px;
+ overflow-y: initial;
+}
+
+#content {
+ max-width: none;
+ margin: 0;
+ padding: 0;
+}
+
+.page {
+ overflow-y: initial;
+}
+
+code {
+ background-color: #666666;
+ border-radius: 5px;
+
+ /* Force background to be printed in Chrome */
+ -webkit-print-color-adjust: exact;
+}
+
+pre > .buttons {
+ z-index: 2;
+}
+
+a, a:visited, a:active, a:hover {
+ color: #4183c4;
+ text-decoration: none;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ page-break-inside: avoid;
+ page-break-after: avoid;
+}
+
+pre, code {
+ page-break-inside: avoid;
+ white-space: pre-wrap;
+}
+
+.fa {
+ display: none !important;
+}
diff --git a/vendor/mdbook/src/theme/css/variables.css b/vendor/mdbook/src/theme/css/variables.css
new file mode 100644
index 000000000..56b634bc3
--- /dev/null
+++ b/vendor/mdbook/src/theme/css/variables.css
@@ -0,0 +1,253 @@
+
+/* Globals */
+
+:root {
+ --sidebar-width: 300px;
+ --page-padding: 15px;
+ --content-max-width: 750px;
+ --menu-bar-height: 50px;
+}
+
+/* Themes */
+
+.ayu {
+ --bg: hsl(210, 25%, 8%);
+ --fg: #c5c5c5;
+
+ --sidebar-bg: #14191f;
+ --sidebar-fg: #c8c9db;
+ --sidebar-non-existant: #5c6773;
+ --sidebar-active: #ffb454;
+ --sidebar-spacer: #2d334f;
+
+ --scrollbar: var(--sidebar-fg);
+
+ --icons: #737480;
+ --icons-hover: #b7b9cc;
+
+ --links: #0096cf;
+
+ --inline-code-color: #ffb454;
+
+ --theme-popup-bg: #14191f;
+ --theme-popup-border: #5c6773;
+ --theme-hover: #191f26;
+
+ --quote-bg: hsl(226, 15%, 17%);
+ --quote-border: hsl(226, 15%, 22%);
+
+ --table-border-color: hsl(210, 25%, 13%);
+ --table-header-bg: hsl(210, 25%, 28%);
+ --table-alternate-bg: hsl(210, 25%, 11%);
+
+ --searchbar-border-color: #848484;
+ --searchbar-bg: #424242;
+ --searchbar-fg: #fff;
+ --searchbar-shadow-color: #d4c89f;
+ --searchresults-header-fg: #666;
+ --searchresults-border-color: #888;
+ --searchresults-li-bg: #252932;
+ --search-mark-bg: #e3b171;
+}
+
+.coal {
+ --bg: hsl(200, 7%, 8%);
+ --fg: #98a3ad;
+
+ --sidebar-bg: #292c2f;
+ --sidebar-fg: #a1adb8;
+ --sidebar-non-existant: #505254;
+ --sidebar-active: #3473ad;
+ --sidebar-spacer: #393939;
+
+ --scrollbar: var(--sidebar-fg);
+
+ --icons: #43484d;
+ --icons-hover: #b3c0cc;
+
+ --links: #2b79a2;
+
+ --inline-code-color: #c5c8c6;
+
+ --theme-popup-bg: #141617;
+ --theme-popup-border: #43484d;
+ --theme-hover: #1f2124;
+
+ --quote-bg: hsl(234, 21%, 18%);
+ --quote-border: hsl(234, 21%, 23%);
+
+ --table-border-color: hsl(200, 7%, 13%);
+ --table-header-bg: hsl(200, 7%, 28%);
+ --table-alternate-bg: hsl(200, 7%, 11%);
+
+ --searchbar-border-color: #aaa;
+ --searchbar-bg: #b7b7b7;
+ --searchbar-fg: #000;
+ --searchbar-shadow-color: #aaa;
+ --searchresults-header-fg: #666;
+ --searchresults-border-color: #98a3ad;
+ --searchresults-li-bg: #2b2b2f;
+ --search-mark-bg: #355c7d;
+}
+
+.light {
+ --bg: hsl(0, 0%, 100%);
+ --fg: hsl(0, 0%, 0%);
+
+ --sidebar-bg: #fafafa;
+ --sidebar-fg: hsl(0, 0%, 0%);
+ --sidebar-non-existant: #aaaaaa;
+ --sidebar-active: #1f1fff;
+ --sidebar-spacer: #f4f4f4;
+
+ --scrollbar: #8F8F8F;
+
+ --icons: #747474;
+ --icons-hover: #000000;
+
+ --links: #20609f;
+
+ --inline-code-color: #301900;
+
+ --theme-popup-bg: #fafafa;
+ --theme-popup-border: #cccccc;
+ --theme-hover: #e6e6e6;
+
+ --quote-bg: hsl(197, 37%, 96%);
+ --quote-border: hsl(197, 37%, 91%);
+
+ --table-border-color: hsl(0, 0%, 95%);
+ --table-header-bg: hsl(0, 0%, 80%);
+ --table-alternate-bg: hsl(0, 0%, 97%);
+
+ --searchbar-border-color: #aaa;
+ --searchbar-bg: #fafafa;
+ --searchbar-fg: #000;
+ --searchbar-shadow-color: #aaa;
+ --searchresults-header-fg: #666;
+ --searchresults-border-color: #888;
+ --searchresults-li-bg: #e4f2fe;
+ --search-mark-bg: #a2cff5;
+}
+
+.navy {
+ --bg: hsl(226, 23%, 11%);
+ --fg: #bcbdd0;
+
+ --sidebar-bg: #282d3f;
+ --sidebar-fg: #c8c9db;
+ --sidebar-non-existant: #505274;
+ --sidebar-active: #2b79a2;
+ --sidebar-spacer: #2d334f;
+
+ --scrollbar: var(--sidebar-fg);
+
+ --icons: #737480;
+ --icons-hover: #b7b9cc;
+
+ --links: #2b79a2;
+
+ --inline-code-color: #c5c8c6;
+
+ --theme-popup-bg: #161923;
+ --theme-popup-border: #737480;
+ --theme-hover: #282e40;
+
+ --quote-bg: hsl(226, 15%, 17%);
+ --quote-border: hsl(226, 15%, 22%);
+
+ --table-border-color: hsl(226, 23%, 16%);
+ --table-header-bg: hsl(226, 23%, 31%);
+ --table-alternate-bg: hsl(226, 23%, 14%);
+
+ --searchbar-border-color: #aaa;
+ --searchbar-bg: #aeaec6;
+ --searchbar-fg: #000;
+ --searchbar-shadow-color: #aaa;
+ --searchresults-header-fg: #5f5f71;
+ --searchresults-border-color: #5c5c68;
+ --searchresults-li-bg: #242430;
+ --search-mark-bg: #a2cff5;
+}
+
+.rust {
+ --bg: hsl(60, 9%, 87%);
+ --fg: #262625;
+
+ --sidebar-bg: #3b2e2a;
+ --sidebar-fg: #c8c9db;
+ --sidebar-non-existant: #505254;
+ --sidebar-active: #e69f67;
+ --sidebar-spacer: #45373a;
+
+ --scrollbar: var(--sidebar-fg);
+
+ --icons: #737480;
+ --icons-hover: #262625;
+
+ --links: #2b79a2;
+
+ --inline-code-color: #6e6b5e;
+
+ --theme-popup-bg: #e1e1db;
+ --theme-popup-border: #b38f6b;
+ --theme-hover: #99908a;
+
+ --quote-bg: hsl(60, 5%, 75%);
+ --quote-border: hsl(60, 5%, 70%);
+
+ --table-border-color: hsl(60, 9%, 82%);
+ --table-header-bg: #b3a497;
+ --table-alternate-bg: hsl(60, 9%, 84%);
+
+ --searchbar-border-color: #aaa;
+ --searchbar-bg: #fafafa;
+ --searchbar-fg: #000;
+ --searchbar-shadow-color: #aaa;
+ --searchresults-header-fg: #666;
+ --searchresults-border-color: #888;
+ --searchresults-li-bg: #dec2a2;
+ --search-mark-bg: #e69f67;
+}
+
+@media (prefers-color-scheme: dark) {
+ .light.no-js {
+ --bg: hsl(200, 7%, 8%);
+ --fg: #98a3ad;
+
+ --sidebar-bg: #292c2f;
+ --sidebar-fg: #a1adb8;
+ --sidebar-non-existant: #505254;
+ --sidebar-active: #3473ad;
+ --sidebar-spacer: #393939;
+
+ --scrollbar: var(--sidebar-fg);
+
+ --icons: #43484d;
+ --icons-hover: #b3c0cc;
+
+ --links: #2b79a2;
+
+ --inline-code-color: #c5c8c6;
+
+ --theme-popup-bg: #141617;
+ --theme-popup-border: #43484d;
+ --theme-hover: #1f2124;
+
+ --quote-bg: hsl(234, 21%, 18%);
+ --quote-border: hsl(234, 21%, 23%);
+
+ --table-border-color: hsl(200, 7%, 13%);
+ --table-header-bg: hsl(200, 7%, 28%);
+ --table-alternate-bg: hsl(200, 7%, 11%);
+
+ --searchbar-border-color: #aaa;
+ --searchbar-bg: #b7b7b7;
+ --searchbar-fg: #000;
+ --searchbar-shadow-color: #aaa;
+ --searchresults-header-fg: #666;
+ --searchresults-border-color: #98a3ad;
+ --searchresults-li-bg: #2b2b2f;
+ --search-mark-bg: #355c7d;
+ }
+}
diff --git a/vendor/mdbook/src/theme/favicon.png b/vendor/mdbook/src/theme/favicon.png
new file mode 100644
index 000000000..a5b1aa16c
--- /dev/null
+++ b/vendor/mdbook/src/theme/favicon.png
Binary files differ
diff --git a/vendor/mdbook/src/theme/favicon.svg b/vendor/mdbook/src/theme/favicon.svg
new file mode 100755
index 000000000..90e0ea58b
--- /dev/null
+++ b/vendor/mdbook/src/theme/favicon.svg
@@ -0,0 +1,22 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 199.7 184.2">
+ <style>
+ @media (prefers-color-scheme: dark) {
+ svg { fill: white; }
+ }
+ </style>
+<path d="M189.5,36.8c0.2,2.8,0,5.1-0.6,6.8L153,162c-0.6,2.1-2,3.7-4.2,5c-2.2,1.2-4.4,1.9-6.7,1.9H31.4c-9.6,0-15.3-2.8-17.3-8.4
+ c-0.8-2.2-0.8-3.9,0.1-5.2c0.9-1.2,2.4-1.8,4.6-1.8H123c7.4,0,12.6-1.4,15.4-4.1s5.7-8.9,8.6-18.4l32.9-108.6
+ c1.8-5.9,1-11.1-2.2-15.6S169.9,0,164,0H72.7c-1,0-3.1,0.4-6.1,1.1l0.1-0.4C64.5,0.2,62.6,0,61,0.1s-3,0.5-4.3,1.4
+ c-1.3,0.9-2.4,1.8-3.2,2.8S52,6.5,51.2,8.1c-0.8,1.6-1.4,3-1.9,4.3s-1.1,2.7-1.8,4.2c-0.7,1.5-1.3,2.7-2,3.7c-0.5,0.6-1.2,1.5-2,2.5
+ s-1.6,2-2.2,2.8s-0.9,1.5-1.1,2.2c-0.2,0.7-0.1,1.8,0.2,3.2c0.3,1.4,0.4,2.4,0.4,3.1c-0.3,3-1.4,6.9-3.3,11.6
+ c-1.9,4.7-3.6,8.1-5.1,10.1c-0.3,0.4-1.2,1.3-2.6,2.7c-1.4,1.4-2.3,2.6-2.6,3.7c-0.3,0.4-0.3,1.5-0.1,3.4c0.3,1.8,0.4,3.1,0.3,3.8
+ c-0.3,2.7-1.3,6.3-3,10.8c-1.7,4.5-3.4,8.2-5,11c-0.2,0.5-0.9,1.4-2,2.8c-1.1,1.4-1.8,2.5-2,3.4c-0.2,0.6-0.1,1.8,0.1,3.4
+ c0.2,1.6,0.2,2.8-0.1,3.6c-0.6,3-1.8,6.7-3.6,11c-1.8,4.3-3.6,7.9-5.4,11c-0.5,0.8-1.1,1.7-2,2.8c-0.8,1.1-1.5,2-2,2.8
+ s-0.8,1.6-1,2.5c-0.1,0.5,0,1.3,0.4,2.3c0.3,1.1,0.4,1.9,0.4,2.6c-0.1,1.1-0.2,2.6-0.5,4.4c-0.2,1.8-0.4,2.9-0.4,3.2
+ c-1.8,4.8-1.7,9.9,0.2,15.2c2.2,6.2,6.2,11.5,11.9,15.8c5.7,4.3,11.7,6.4,17.8,6.4h110.7c5.2,0,10.1-1.7,14.7-5.2s7.7-7.8,9.2-12.9
+ l33-108.6c1.8-5.8,1-10.9-2.2-15.5C194.9,39.7,192.6,38,189.5,36.8z M59.6,122.8L73.8,80c0,0,7,0,10.8,0s28.8-1.7,25.4,17.5
+ c-3.4,19.2-18.8,25.2-36.8,25.4S59.6,122.8,59.6,122.8z M78.6,116.8c4.7-0.1,18.9-2.9,22.1-17.1S89.2,86.3,89.2,86.3l-8.9,0
+ l-10.2,30.5C70.2,116.9,74,116.9,78.6,116.8z M75.3,68.7L89,26.2h9.8l0.8,34l23.6-34h9.9l-13.6,42.5h-7.1l12.5-35.4l-24.5,35.4h-6.8
+ l-0.8-35L82,68.7H75.3z"/>
+</svg>
+<!-- Original image Copyright Dave Gandy — CC BY 4.0 License -->
diff --git a/vendor/mdbook/src/theme/head.hbs b/vendor/mdbook/src/theme/head.hbs
new file mode 100644
index 000000000..cb1be1876
--- /dev/null
+++ b/vendor/mdbook/src/theme/head.hbs
@@ -0,0 +1 @@
+{{!-- Put your head HTML text here --}}
diff --git a/vendor/mdbook/src/theme/header.hbs b/vendor/mdbook/src/theme/header.hbs
new file mode 100644
index 000000000..26fa2d2ef
--- /dev/null
+++ b/vendor/mdbook/src/theme/header.hbs
@@ -0,0 +1 @@
+{{!-- Put your header HTML text here --}} \ No newline at end of file
diff --git a/vendor/mdbook/src/theme/index.hbs b/vendor/mdbook/src/theme/index.hbs
new file mode 100644
index 000000000..18d984a2b
--- /dev/null
+++ b/vendor/mdbook/src/theme/index.hbs
@@ -0,0 +1,314 @@
+<!DOCTYPE HTML>
+<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}">
+ <head>
+ <!-- Book generated using mdBook -->
+ <meta charset="UTF-8">
+ <title>{{ title }}</title>
+ {{#if is_print }}
+ <meta name="robots" content="noindex" />
+ {{/if}}
+ {{#if base_url}}
+ <base href="{{ base_url }}">
+ {{/if}}
+
+
+ <!-- Custom HTML head -->
+ {{> head}}
+
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
+ <meta name="description" content="{{ description }}">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="theme-color" content="#ffffff" />
+
+ {{#if favicon_svg}}
+ <link rel="icon" href="{{ path_to_root }}favicon.svg">
+ {{/if}}
+ {{#if favicon_png}}
+ <link rel="shortcut icon" href="{{ path_to_root }}favicon.png">
+ {{/if}}
+ <link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
+ <link rel="stylesheet" href="{{ path_to_root }}css/general.css">
+ <link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
+ {{#if print_enable}}
+ <link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
+ {{/if}}
+
+ <!-- Fonts -->
+ <link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
+ {{#if copy_fonts}}
+ <link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
+ {{/if}}
+
+ <!-- Highlight.js Stylesheets -->
+ <link rel="stylesheet" href="{{ path_to_root }}highlight.css">
+ <link rel="stylesheet" href="{{ path_to_root }}tomorrow-night.css">
+ <link rel="stylesheet" href="{{ path_to_root }}ayu-highlight.css">
+
+ <!-- Custom theme stylesheets -->
+ {{#each additional_css}}
+ <link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
+ {{/each}}
+
+ {{#if mathjax_support}}
+ <!-- MathJax -->
+ <script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
+ {{/if}}
+ </head>
+ <body>
+ <!-- Provide site root to javascript -->
+ <script type="text/javascript">
+ var path_to_root = "{{ path_to_root }}";
+ var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
+ </script>
+
+ <!-- Work around some values being stored in localStorage wrapped in quotes -->
+ <script type="text/javascript">
+ 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 type="text/javascript">
+ 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('no-js')
+ html.classList.remove('{{ default_theme }}')
+ html.classList.add(theme);
+ html.classList.add('js');
+ </script>
+
+ <!-- Hide / unhide sidebar before it is displayed -->
+ <script type="text/javascript">
+ var html = document.querySelector('html');
+ var sidebar = 'hidden';
+ if (document.body.clientWidth >= 1080) {
+ try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
+ sidebar = sidebar || 'visible';
+ }
+ html.classList.remove('sidebar-visible');
+ html.classList.add("sidebar-" + sidebar);
+ </script>
+
+ <nav id="sidebar" class="sidebar" aria-label="Table of contents">
+ <div class="sidebar-scrollbox">
+ {{#toc}}{{/toc}}
+ </div>
+ <div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
+ </nav>
+
+ <div id="page-wrapper" class="page-wrapper">
+
+ <div class="page">
+ {{> header}}
+ <div id="menu-bar-hover-placeholder"></div>
+ <div id="menu-bar" class="menu-bar sticky bordered">
+ <div class="left-buttons">
+ <button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
+ <i class="fa fa-bars"></i>
+ </button>
+ <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">{{ theme_option "Light" }}</button></li>
+ <li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li>
+ <li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li>
+ <li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li>
+ <li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li>
+ </ul>
+ {{#if search_enabled}}
+ <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>
+ {{/if}}
+ </div>
+
+ <h1 class="menu-title">{{ book_title }}</h1>
+
+ <div class="right-buttons">
+ {{#if print_enable}}
+ <a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
+ <i id="print-button" class="fa fa-print"></i>
+ </a>
+ {{/if}}
+ {{#if git_repository_url}}
+ <a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
+ <i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
+ </a>
+ {{/if}}
+ {{#if git_repository_edit_url}}
+ <a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit">
+ <i id="git-edit-button" class="fa fa-edit"></i>
+ </a>
+ {{/if}}
+
+ </div>
+ </div>
+
+ {{#if search_enabled}}
+ <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>
+ {{/if}}
+
+ <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
+ <script type="text/javascript">
+ 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>
+ {{{ content }}}
+ </main>
+
+ <nav class="nav-wrapper" aria-label="Page navigation">
+ <!-- Mobile navigation buttons -->
+ {{#previous}}
+ <a rel="prev" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
+ <i class="fa fa-angle-left"></i>
+ </a>
+ {{/previous}}
+
+ {{#next}}
+ <a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
+ <i class="fa fa-angle-right"></i>
+ </a>
+ {{/next}}
+
+ <div style="clear: both"></div>
+ </nav>
+ </div>
+ </div>
+
+ <nav class="nav-wide-wrapper" aria-label="Page navigation">
+ {{#previous}}
+ <a rel="prev" href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
+ <i class="fa fa-angle-left"></i>
+ </a>
+ {{/previous}}
+
+ {{#next}}
+ <a rel="next" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
+ <i class="fa fa-angle-right"></i>
+ </a>
+ {{/next}}
+ </nav>
+
+ </div>
+
+ {{#if live_reload_endpoint}}
+ <!-- Livereload script (if served using the cli tool) -->
+ <script type="text/javascript">
+ const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
+ const socket = new WebSocket(wsAddress);
+ socket.onmessage = function (event) {
+ if (event.data === "reload") {
+ socket.close();
+ location.reload();
+ }
+ };
+
+ window.onbeforeunload = function() {
+ socket.close();
+ }
+ </script>
+ {{/if}}
+
+ {{#if google_analytics}}
+ <!-- Google Analytics Tag -->
+ <script type="text/javascript">
+ var localAddrs = ["localhost", "127.0.0.1", ""];
+
+ // make sure we don't activate google analytics if the developer is
+ // inspecting the book locally...
+ if (localAddrs.indexOf(document.location.hostname) === -1) {
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+ })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+
+ ga('create', '{{google_analytics}}', 'auto');
+ ga('send', 'pageview');
+ }
+ </script>
+ {{/if}}
+
+ {{#if playground_line_numbers}}
+ <script type="text/javascript">
+ window.playground_line_numbers = true;
+ </script>
+ {{/if}}
+
+ {{#if playground_copyable}}
+ <script type="text/javascript">
+ window.playground_copyable = true;
+ </script>
+ {{/if}}
+
+ {{#if playground_js}}
+ <script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
+ <script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
+ <script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
+ <script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script>
+ <script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
+ {{/if}}
+
+ {{#if search_js}}
+ <script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
+ <script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
+ <script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
+ {{/if}}
+
+ <script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script>
+ <script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script>
+ <script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script>
+
+ <!-- Custom JS scripts -->
+ {{#each additional_js}}
+ <script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
+ {{/each}}
+
+ {{#if is_print}}
+ {{#if mathjax_support}}
+ <script type="text/javascript">
+ window.addEventListener('load', function() {
+ MathJax.Hub.Register.StartupHook('End', function() {
+ window.setTimeout(window.print, 100);
+ });
+ });
+ </script>
+ {{else}}
+ <script type="text/javascript">
+ window.addEventListener('load', function() {
+ window.setTimeout(window.print, 100);
+ });
+ </script>
+ {{/if}}
+ {{/if}}
+
+ </body>
+</html>
diff --git a/vendor/mdbook/src/theme/mod.rs b/vendor/mdbook/src/theme/mod.rs
new file mode 100644
index 000000000..a1ee18aff
--- /dev/null
+++ b/vendor/mdbook/src/theme/mod.rs
@@ -0,0 +1,270 @@
+#![allow(missing_docs)]
+
+pub mod playground_editor;
+
+pub mod fonts;
+
+#[cfg(feature = "search")]
+pub mod searcher;
+
+use std::fs::File;
+use std::io::Read;
+use std::path::Path;
+
+use crate::errors::*;
+
+pub static INDEX: &[u8] = include_bytes!("index.hbs");
+pub static HEAD: &[u8] = include_bytes!("head.hbs");
+pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
+pub static HEADER: &[u8] = include_bytes!("header.hbs");
+pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
+pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
+pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
+pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css");
+pub static FAVICON_PNG: &[u8] = include_bytes!("favicon.png");
+pub static FAVICON_SVG: &[u8] = include_bytes!("favicon.svg");
+pub static JS: &[u8] = include_bytes!("book.js");
+pub static HIGHLIGHT_JS: &[u8] = include_bytes!("highlight.js");
+pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("tomorrow-night.css");
+pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("highlight.css");
+pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("ayu-highlight.css");
+pub static CLIPBOARD_JS: &[u8] = include_bytes!("clipboard.min.js");
+pub static FONT_AWESOME: &[u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
+pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
+pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
+pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
+pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
+pub static FONT_AWESOME_WOFF2: &[u8] =
+ include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2");
+pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
+
+/// The `Theme` struct should be used instead of the static variables because
+/// the `new()` method will look if the user has a theme directory in their
+/// source folder and use the users theme instead of the default.
+///
+/// You should only ever use the static variables directly if you want to
+/// override the user's theme with the defaults.
+#[derive(Debug, PartialEq)]
+pub struct Theme {
+ pub index: Vec<u8>,
+ pub head: Vec<u8>,
+ pub redirect: Vec<u8>,
+ pub header: Vec<u8>,
+ pub chrome_css: Vec<u8>,
+ pub general_css: Vec<u8>,
+ pub print_css: Vec<u8>,
+ pub variables_css: Vec<u8>,
+ pub favicon_png: Option<Vec<u8>>,
+ pub favicon_svg: Option<Vec<u8>>,
+ pub js: Vec<u8>,
+ pub highlight_css: Vec<u8>,
+ pub tomorrow_night_css: Vec<u8>,
+ pub ayu_highlight_css: Vec<u8>,
+ pub highlight_js: Vec<u8>,
+ pub clipboard_js: Vec<u8>,
+}
+
+impl Theme {
+ /// Creates a `Theme` from the given `theme_dir`.
+ /// If a file is found in the theme dir, it will override the default version.
+ pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
+ let theme_dir = theme_dir.as_ref();
+ let mut theme = Theme::default();
+
+ // If the theme directory doesn't exist there's no point continuing...
+ if !theme_dir.exists() || !theme_dir.is_dir() {
+ return theme;
+ }
+
+ // Check for individual files, if they exist copy them across
+ {
+ let files = vec![
+ (theme_dir.join("index.hbs"), &mut theme.index),
+ (theme_dir.join("head.hbs"), &mut theme.head),
+ (theme_dir.join("redirect.hbs"), &mut theme.redirect),
+ (theme_dir.join("header.hbs"), &mut theme.header),
+ (theme_dir.join("book.js"), &mut theme.js),
+ (theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
+ (theme_dir.join("css/general.css"), &mut theme.general_css),
+ (theme_dir.join("css/print.css"), &mut theme.print_css),
+ (
+ theme_dir.join("css/variables.css"),
+ &mut theme.variables_css,
+ ),
+ (theme_dir.join("highlight.js"), &mut theme.highlight_js),
+ (theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
+ (theme_dir.join("highlight.css"), &mut theme.highlight_css),
+ (
+ theme_dir.join("tomorrow-night.css"),
+ &mut theme.tomorrow_night_css,
+ ),
+ (
+ theme_dir.join("ayu-highlight.css"),
+ &mut theme.ayu_highlight_css,
+ ),
+ ];
+
+ let load_with_warn = |filename: &Path, dest| {
+ if !filename.exists() {
+ // Don't warn if the file doesn't exist.
+ return false;
+ }
+ if let Err(e) = load_file_contents(filename, dest) {
+ warn!("Couldn't load custom file, {}: {}", filename.display(), e);
+ false
+ } else {
+ true
+ }
+ };
+
+ for (filename, dest) in files {
+ load_with_warn(&filename, dest);
+ }
+
+ // If the user overrides one favicon, but not the other, do not
+ // copy the default for the other.
+ let favicon_png = &mut theme.favicon_png.as_mut().unwrap();
+ let png = load_with_warn(&theme_dir.join("favicon.png"), favicon_png);
+ let favicon_svg = &mut theme.favicon_svg.as_mut().unwrap();
+ let svg = load_with_warn(&theme_dir.join("favicon.svg"), favicon_svg);
+ match (png, svg) {
+ (true, true) | (false, false) => {}
+ (true, false) => {
+ theme.favicon_svg = None;
+ }
+ (false, true) => {
+ theme.favicon_png = None;
+ }
+ }
+ }
+
+ theme
+ }
+}
+
+impl Default for Theme {
+ fn default() -> Theme {
+ Theme {
+ index: INDEX.to_owned(),
+ head: HEAD.to_owned(),
+ redirect: REDIRECT.to_owned(),
+ header: HEADER.to_owned(),
+ chrome_css: CHROME_CSS.to_owned(),
+ general_css: GENERAL_CSS.to_owned(),
+ print_css: PRINT_CSS.to_owned(),
+ variables_css: VARIABLES_CSS.to_owned(),
+ favicon_png: Some(FAVICON_PNG.to_owned()),
+ favicon_svg: Some(FAVICON_SVG.to_owned()),
+ js: JS.to_owned(),
+ highlight_css: HIGHLIGHT_CSS.to_owned(),
+ tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
+ ayu_highlight_css: AYU_HIGHLIGHT_CSS.to_owned(),
+ highlight_js: HIGHLIGHT_JS.to_owned(),
+ clipboard_js: CLIPBOARD_JS.to_owned(),
+ }
+ }
+}
+
+/// Checks if a file exists, if so, the destination buffer will be filled with
+/// its contents.
+fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result<()> {
+ let filename = filename.as_ref();
+
+ let mut buffer = Vec::new();
+ File::open(filename)?.read_to_end(&mut buffer)?;
+
+ // We needed the buffer so we'd only overwrite the existing content if we
+ // could successfully load the file into memory.
+ dest.clear();
+ dest.append(&mut buffer);
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::fs;
+ use std::path::PathBuf;
+ use tempfile::Builder as TempFileBuilder;
+
+ #[test]
+ fn theme_uses_defaults_with_nonexistent_src_dir() {
+ let non_existent = PathBuf::from("/non/existent/directory/");
+ assert!(!non_existent.exists());
+
+ let should_be = Theme::default();
+ let got = Theme::new(&non_existent);
+
+ assert_eq!(got, should_be);
+ }
+
+ #[test]
+ fn theme_dir_overrides_defaults() {
+ let files = [
+ "index.hbs",
+ "head.hbs",
+ "redirect.hbs",
+ "header.hbs",
+ "favicon.png",
+ "favicon.svg",
+ "css/chrome.css",
+ "css/fonts.css",
+ "css/general.css",
+ "css/print.css",
+ "css/variables.css",
+ "book.js",
+ "highlight.js",
+ "tomorrow-night.css",
+ "highlight.css",
+ "ayu-highlight.css",
+ "clipboard.min.js",
+ ];
+
+ let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
+ fs::create_dir(temp.path().join("css")).unwrap();
+
+ // "touch" all of the special files so we have empty copies
+ for file in &files {
+ File::create(&temp.path().join(file)).unwrap();
+ }
+
+ let got = Theme::new(temp.path());
+
+ let empty = Theme {
+ index: Vec::new(),
+ head: Vec::new(),
+ redirect: Vec::new(),
+ header: Vec::new(),
+ chrome_css: Vec::new(),
+ general_css: Vec::new(),
+ print_css: Vec::new(),
+ variables_css: Vec::new(),
+ favicon_png: Some(Vec::new()),
+ favicon_svg: Some(Vec::new()),
+ js: Vec::new(),
+ highlight_css: Vec::new(),
+ tomorrow_night_css: Vec::new(),
+ ayu_highlight_css: Vec::new(),
+ highlight_js: Vec::new(),
+ clipboard_js: Vec::new(),
+ };
+
+ assert_eq!(got, empty);
+ }
+
+ #[test]
+ fn favicon_override() {
+ let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
+ fs::write(temp.path().join("favicon.png"), "1234").unwrap();
+ let got = Theme::new(temp.path());
+ assert_eq!(got.favicon_png.as_ref().unwrap(), b"1234");
+ assert_eq!(got.favicon_svg, None);
+
+ let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
+ fs::write(temp.path().join("favicon.svg"), "4567").unwrap();
+ let got = Theme::new(temp.path());
+ assert_eq!(got.favicon_png, None);
+ assert_eq!(got.favicon_svg.as_ref().unwrap(), b"4567");
+ }
+}
diff --git a/vendor/mdbook/src/theme/redirect.hbs b/vendor/mdbook/src/theme/redirect.hbs
new file mode 100644
index 000000000..9f49e6d09
--- /dev/null
+++ b/vendor/mdbook/src/theme/redirect.hbs
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Redirecting...</title>
+ <meta http-equiv="refresh" content="0;URL='{{url}}'">
+ <meta rel="canonical" href="{{url}}">
+ </head>
+ <body>
+ <p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
+ </body>
+</html>
diff --git a/vendor/mdbook/src/theme/searcher/mod.rs b/vendor/mdbook/src/theme/searcher/mod.rs
new file mode 100644
index 000000000..d5029db16
--- /dev/null
+++ b/vendor/mdbook/src/theme/searcher/mod.rs
@@ -0,0 +1,6 @@
+//! Theme dependencies for in-browser search. Not included in mdbook when
+//! the "search" cargo feature is disabled.
+
+pub static JS: &[u8] = include_bytes!("searcher.js");
+pub static MARK_JS: &[u8] = include_bytes!("mark.min.js");
+pub static ELASTICLUNR_JS: &[u8] = include_bytes!("elasticlunr.min.js");
diff --git a/vendor/mdbook/src/theme/searcher/searcher.js b/vendor/mdbook/src/theme/searcher/searcher.js
new file mode 100644
index 000000000..d2b0aeed3
--- /dev/null
+++ b/vendor/mdbook/src/theme/searcher/searcher.js
@@ -0,0 +1,483 @@
+"use strict";
+window.search = window.search || {};
+(function search(search) {
+ // Search functionality
+ //
+ // You can use !hasFocus() to prevent keyhandling in your key
+ // event handlers while the user is typing their search.
+
+ if (!Mark || !elasticlunr) {
+ return;
+ }
+
+ //IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
+ if (!String.prototype.startsWith) {
+ String.prototype.startsWith = function(search, pos) {
+ return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
+ };
+ }
+
+ var search_wrap = document.getElementById('search-wrapper'),
+ searchbar = document.getElementById('searchbar'),
+ searchbar_outer = document.getElementById('searchbar-outer'),
+ searchresults = document.getElementById('searchresults'),
+ searchresults_outer = document.getElementById('searchresults-outer'),
+ searchresults_header = document.getElementById('searchresults-header'),
+ searchicon = document.getElementById('search-toggle'),
+ content = document.getElementById('content'),
+
+ searchindex = null,
+ doc_urls = [],
+ results_options = {
+ teaser_word_count: 30,
+ limit_results: 30,
+ },
+ search_options = {
+ bool: "AND",
+ expand: true,
+ fields: {
+ title: {boost: 1},
+ body: {boost: 1},
+ breadcrumbs: {boost: 0}
+ }
+ },
+ mark_exclude = [],
+ marker = new Mark(content),
+ current_searchterm = "",
+ URL_SEARCH_PARAM = 'search',
+ URL_MARK_PARAM = 'highlight',
+ teaser_count = 0,
+
+ SEARCH_HOTKEY_KEYCODE = 83,
+ ESCAPE_KEYCODE = 27,
+ DOWN_KEYCODE = 40,
+ UP_KEYCODE = 38,
+ SELECT_KEYCODE = 13;
+
+ function hasFocus() {
+ return searchbar === document.activeElement;
+ }
+
+ function removeChildren(elem) {
+ while (elem.firstChild) {
+ elem.removeChild(elem.firstChild);
+ }
+ }
+
+ // Helper to parse a url into its building blocks.
+ function parseURL(url) {
+ var a = document.createElement('a');
+ a.href = url;
+ return {
+ source: url,
+ protocol: a.protocol.replace(':',''),
+ host: a.hostname,
+ port: a.port,
+ params: (function(){
+ var ret = {};
+ var seg = a.search.replace(/^\?/,'').split('&');
+ var len = seg.length, i = 0, s;
+ for (;i<len;i++) {
+ if (!seg[i]) { continue; }
+ s = seg[i].split('=');
+ ret[s[0]] = s[1];
+ }
+ return ret;
+ })(),
+ file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
+ hash: a.hash.replace('#',''),
+ path: a.pathname.replace(/^([^/])/,'/$1')
+ };
+ }
+
+ // Helper to recreate a url string from its building blocks.
+ function renderURL(urlobject) {
+ var url = urlobject.protocol + "://" + urlobject.host;
+ if (urlobject.port != "") {
+ url += ":" + urlobject.port;
+ }
+ url += urlobject.path;
+ var joiner = "?";
+ for(var prop in urlobject.params) {
+ if(urlobject.params.hasOwnProperty(prop)) {
+ url += joiner + prop + "=" + urlobject.params[prop];
+ joiner = "&";
+ }
+ }
+ if (urlobject.hash != "") {
+ url += "#" + urlobject.hash;
+ }
+ return url;
+ }
+
+ // Helper to escape html special chars for displaying the teasers
+ var escapeHTML = (function() {
+ var MAP = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&#34;',
+ "'": '&#39;'
+ };
+ var repl = function(c) { return MAP[c]; };
+ return function(s) {
+ return s.replace(/[&<>'"]/g, repl);
+ };
+ })();
+
+ function formatSearchMetric(count, searchterm) {
+ if (count == 1) {
+ return count + " search result for '" + searchterm + "':";
+ } else if (count == 0) {
+ return "No search results for '" + searchterm + "'.";
+ } else {
+ return count + " search results for '" + searchterm + "':";
+ }
+ }
+
+ function formatSearchResult(result, searchterms) {
+ var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
+ teaser_count++;
+
+ // The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
+ var url = doc_urls[result.ref].split("#");
+ if (url.length == 1) { // no anchor found
+ url.push("");
+ }
+
+ // encodeURIComponent escapes all chars that could allow an XSS except
+ // for '. Due to that we also manually replace ' with its url-encoded
+ // representation (%27).
+ var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27");
+
+ return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
+ + '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
+ + '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
+ + teaser + '</span>';
+ }
+
+ function makeTeaser(body, searchterms) {
+ // The strategy is as follows:
+ // First, assign a value to each word in the document:
+ // Words that correspond to search terms (stemmer aware): 40
+ // Normal words: 2
+ // First word in a sentence: 8
+ // Then use a sliding window with a constant number of words and count the
+ // sum of the values of the words within the window. Then use the window that got the
+ // maximum sum. If there are multiple maximas, then get the last one.
+ // Enclose the terms in <em>.
+ var stemmed_searchterms = searchterms.map(function(w) {
+ return elasticlunr.stemmer(w.toLowerCase());
+ });
+ var searchterm_weight = 40;
+ var weighted = []; // contains elements of ["word", weight, index_in_document]
+ // split in sentences, then words
+ var sentences = body.toLowerCase().split('. ');
+ var index = 0;
+ var value = 0;
+ var searchterm_found = false;
+ for (var sentenceindex in sentences) {
+ var words = sentences[sentenceindex].split(' ');
+ value = 8;
+ for (var wordindex in words) {
+ var word = words[wordindex];
+ if (word.length > 0) {
+ for (var searchtermindex in stemmed_searchterms) {
+ if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) {
+ value = searchterm_weight;
+ searchterm_found = true;
+ }
+ };
+ weighted.push([word, value, index]);
+ value = 2;
+ }
+ index += word.length;
+ index += 1; // ' ' or '.' if last word in sentence
+ };
+ index += 1; // because we split at a two-char boundary '. '
+ };
+
+ if (weighted.length == 0) {
+ return body;
+ }
+
+ var window_weight = [];
+ var window_size = Math.min(weighted.length, results_options.teaser_word_count);
+
+ var cur_sum = 0;
+ for (var wordindex = 0; wordindex < window_size; wordindex++) {
+ cur_sum += weighted[wordindex][1];
+ };
+ window_weight.push(cur_sum);
+ for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
+ cur_sum -= weighted[wordindex][1];
+ cur_sum += weighted[wordindex + window_size][1];
+ window_weight.push(cur_sum);
+ };
+
+ if (searchterm_found) {
+ var max_sum = 0;
+ var max_sum_window_index = 0;
+ // backwards
+ for (var i = window_weight.length - 1; i >= 0; i--) {
+ if (window_weight[i] > max_sum) {
+ max_sum = window_weight[i];
+ max_sum_window_index = i;
+ }
+ };
+ } else {
+ max_sum_window_index = 0;
+ }
+
+ // add <em/> around searchterms
+ var teaser_split = [];
+ var index = weighted[max_sum_window_index][2];
+ for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) {
+ var word = weighted[i];
+ if (index < word[2]) {
+ // missing text from index to start of `word`
+ teaser_split.push(body.substring(index, word[2]));
+ index = word[2];
+ }
+ if (word[1] == searchterm_weight) {
+ teaser_split.push("<em>")
+ }
+ index = word[2] + word[0].length;
+ teaser_split.push(body.substring(word[2], index));
+ if (word[1] == searchterm_weight) {
+ teaser_split.push("</em>")
+ }
+ };
+
+ return teaser_split.join('');
+ }
+
+ function init(config) {
+ results_options = config.results_options;
+ search_options = config.search_options;
+ searchbar_outer = config.searchbar_outer;
+ doc_urls = config.doc_urls;
+ searchindex = elasticlunr.Index.load(config.index);
+
+ // Set up events
+ searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
+ searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false);
+ document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false);
+ // If the user uses the browser buttons, do the same as if a reload happened
+ window.onpopstate = function(e) { doSearchOrMarkFromUrl(); };
+ // Suppress "submit" events so the page doesn't reload when the user presses Enter
+ document.addEventListener('submit', function(e) { e.preventDefault(); }, false);
+
+ // If reloaded, do the search or mark again, depending on the current url parameters
+ doSearchOrMarkFromUrl();
+ }
+
+ function unfocusSearchbar() {
+ // hacky, but just focusing a div only works once
+ var tmp = document.createElement('input');
+ tmp.setAttribute('style', 'position: absolute; opacity: 0;');
+ searchicon.appendChild(tmp);
+ tmp.focus();
+ tmp.remove();
+ }
+
+ // On reload or browser history backwards/forwards events, parse the url and do search or mark
+ function doSearchOrMarkFromUrl() {
+ // Check current URL for search request
+ var url = parseURL(window.location.href);
+ if (url.params.hasOwnProperty(URL_SEARCH_PARAM)
+ && url.params[URL_SEARCH_PARAM] != "") {
+ showSearch(true);
+ searchbar.value = decodeURIComponent(
+ (url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20'));
+ searchbarKeyUpHandler(); // -> doSearch()
+ } else {
+ showSearch(false);
+ }
+
+ if (url.params.hasOwnProperty(URL_MARK_PARAM)) {
+ var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
+ marker.mark(words, {
+ exclude: mark_exclude
+ });
+
+ var markers = document.querySelectorAll("mark");
+ function hide() {
+ for (var i = 0; i < markers.length; i++) {
+ markers[i].classList.add("fade-out");
+ window.setTimeout(function(e) { marker.unmark(); }, 300);
+ }
+ }
+ for (var i = 0; i < markers.length; i++) {
+ markers[i].addEventListener('click', hide);
+ }
+ }
+ }
+
+ // Eventhandler for keyevents on `document`
+ function globalKeyHandler(e) {
+ if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; }
+
+ if (e.keyCode === ESCAPE_KEYCODE) {
+ e.preventDefault();
+ searchbar.classList.remove("active");
+ setSearchUrlParameters("",
+ (searchbar.value.trim() !== "") ? "push" : "replace");
+ if (hasFocus()) {
+ unfocusSearchbar();
+ }
+ showSearch(false);
+ marker.unmark();
+ } else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) {
+ e.preventDefault();
+ showSearch(true);
+ window.scrollTo(0, 0);
+ searchbar.select();
+ } else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
+ e.preventDefault();
+ unfocusSearchbar();
+ searchresults.firstElementChild.classList.add("focus");
+ } else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
+ || e.keyCode === UP_KEYCODE
+ || e.keyCode === SELECT_KEYCODE)) {
+ // not `:focus` because browser does annoying scrolling
+ var focused = searchresults.querySelector("li.focus");
+ if (!focused) return;
+ e.preventDefault();
+ if (e.keyCode === DOWN_KEYCODE) {
+ var next = focused.nextElementSibling;
+ if (next) {
+ focused.classList.remove("focus");
+ next.classList.add("focus");
+ }
+ } else if (e.keyCode === UP_KEYCODE) {
+ focused.classList.remove("focus");
+ var prev = focused.previousElementSibling;
+ if (prev) {
+ prev.classList.add("focus");
+ } else {
+ searchbar.select();
+ }
+ } else { // SELECT_KEYCODE
+ window.location.assign(focused.querySelector('a'));
+ }
+ }
+ }
+
+ function showSearch(yes) {
+ if (yes) {
+ search_wrap.classList.remove('hidden');
+ searchicon.setAttribute('aria-expanded', 'true');
+ } else {
+ search_wrap.classList.add('hidden');
+ searchicon.setAttribute('aria-expanded', 'false');
+ var results = searchresults.children;
+ for (var i = 0; i < results.length; i++) {
+ results[i].classList.remove("focus");
+ }
+ }
+ }
+
+ function showResults(yes) {
+ if (yes) {
+ searchresults_outer.classList.remove('hidden');
+ } else {
+ searchresults_outer.classList.add('hidden');
+ }
+ }
+
+ // Eventhandler for search icon
+ function searchIconClickHandler() {
+ if (search_wrap.classList.contains('hidden')) {
+ showSearch(true);
+ window.scrollTo(0, 0);
+ searchbar.select();
+ } else {
+ showSearch(false);
+ }
+ }
+
+ // Eventhandler for keyevents while the searchbar is focused
+ function searchbarKeyUpHandler() {
+ var searchterm = searchbar.value.trim();
+ if (searchterm != "") {
+ searchbar.classList.add("active");
+ doSearch(searchterm);
+ } else {
+ searchbar.classList.remove("active");
+ showResults(false);
+ removeChildren(searchresults);
+ }
+
+ setSearchUrlParameters(searchterm, "push_if_new_search_else_replace");
+
+ // Remove marks
+ marker.unmark();
+ }
+
+ // Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor .
+ // `action` can be one of "push", "replace", "push_if_new_search_else_replace"
+ // and replaces or pushes a new browser history item.
+ // "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
+ function setSearchUrlParameters(searchterm, action) {
+ var url = parseURL(window.location.href);
+ var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM);
+ if (searchterm != "" || action == "push_if_new_search_else_replace") {
+ url.params[URL_SEARCH_PARAM] = searchterm;
+ delete url.params[URL_MARK_PARAM];
+ url.hash = "";
+ } else {
+ delete url.params[URL_MARK_PARAM];
+ delete url.params[URL_SEARCH_PARAM];
+ }
+ // A new search will also add a new history item, so the user can go back
+ // to the page prior to searching. A updated search term will only replace
+ // the url.
+ if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) {
+ history.pushState({}, document.title, renderURL(url));
+ } else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) {
+ history.replaceState({}, document.title, renderURL(url));
+ }
+ }
+
+ function doSearch(searchterm) {
+
+ // Don't search the same twice
+ if (current_searchterm == searchterm) { return; }
+ else { current_searchterm = searchterm; }
+
+ if (searchindex == null) { return; }
+
+ // Do the actual search
+ var results = searchindex.search(searchterm, search_options);
+ var resultcount = Math.min(results.length, results_options.limit_results);
+
+ // Display search metrics
+ searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
+
+ // Clear and insert results
+ var searchterms = searchterm.split(' ');
+ removeChildren(searchresults);
+ for(var i = 0; i < resultcount ; i++){
+ var resultElem = document.createElement('li');
+ resultElem.innerHTML = formatSearchResult(results[i], searchterms);
+ searchresults.appendChild(resultElem);
+ }
+
+ // Display results
+ showResults(true);
+ }
+
+ fetch(path_to_root + 'searchindex.json')
+ .then(response => response.json())
+ .then(json => init(json))
+ .catch(error => { // Try to load searchindex.js if fetch failed
+ var script = document.createElement('script');
+ script.src = path_to_root + 'searchindex.js';
+ script.onload = () => init(window.search);
+ document.head.appendChild(script);
+ });
+
+ // Exported functions
+ search.hasFocus = hasFocus;
+})(window.search);
diff --git a/vendor/mdbook/src/theme/tomorrow-night.css b/vendor/mdbook/src/theme/tomorrow-night.css
new file mode 100644
index 000000000..5b4aca77c
--- /dev/null
+++ b/vendor/mdbook/src/theme/tomorrow-night.css
@@ -0,0 +1,102 @@
+/* Tomorrow Night Theme */
+/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
+/* Original theme - https://github.com/chriskempson/tomorrow-theme */
+/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
+
+/* Tomorrow Comment */
+.hljs-comment {
+ color: #969896;
+}
+
+/* Tomorrow Red */
+.hljs-variable,
+.hljs-attribute,
+.hljs-tag,
+.hljs-regexp,
+.ruby .hljs-constant,
+.xml .hljs-tag .hljs-title,
+.xml .hljs-pi,
+.xml .hljs-doctype,
+.html .hljs-doctype,
+.css .hljs-id,
+.css .hljs-class,
+.css .hljs-pseudo {
+ color: #cc6666;
+}
+
+/* Tomorrow Orange */
+.hljs-number,
+.hljs-preprocessor,
+.hljs-pragma,
+.hljs-built_in,
+.hljs-literal,
+.hljs-params,
+.hljs-constant {
+ color: #de935f;
+}
+
+/* Tomorrow Yellow */
+.ruby .hljs-class .hljs-title,
+.css .hljs-rule .hljs-attribute {
+ color: #f0c674;
+}
+
+/* Tomorrow Green */
+.hljs-string,
+.hljs-value,
+.hljs-inheritance,
+.hljs-header,
+.hljs-name,
+.ruby .hljs-symbol,
+.xml .hljs-cdata {
+ color: #b5bd68;
+}
+
+/* Tomorrow Aqua */
+.hljs-title,
+.css .hljs-hexcolor {
+ color: #8abeb7;
+}
+
+/* Tomorrow Blue */
+.hljs-function,
+.python .hljs-decorator,
+.python .hljs-title,
+.ruby .hljs-function .hljs-title,
+.ruby .hljs-title .hljs-keyword,
+.perl .hljs-sub,
+.javascript .hljs-title,
+.coffeescript .hljs-title {
+ color: #81a2be;
+}
+
+/* Tomorrow Purple */
+.hljs-keyword,
+.javascript .hljs-function {
+ color: #b294bb;
+}
+
+.hljs {
+ display: block;
+ overflow-x: auto;
+ background: #1d1f21;
+ color: #c5c8c6;
+}
+
+.coffeescript .javascript,
+.javascript .xml,
+.tex .hljs-formula,
+.xml .javascript,
+.xml .vbscript,
+.xml .css,
+.xml .hljs-cdata {
+ opacity: 0.5;
+}
+
+.hljs-addition {
+ color: #718c00;
+}
+
+.hljs-deletion {
+ color: #c82829;
+}
diff --git a/vendor/mdbook/src/utils/fs.rs b/vendor/mdbook/src/utils/fs.rs
new file mode 100644
index 000000000..a933d548a
--- /dev/null
+++ b/vendor/mdbook/src/utils/fs.rs
@@ -0,0 +1,275 @@
+use crate::errors::*;
+use std::convert::Into;
+use std::fs::{self, File};
+use std::io::Write;
+use std::path::{Component, Path, PathBuf};
+
+/// Naively replaces any path separator with a forward-slash '/'
+pub fn normalize_path(path: &str) -> String {
+ use std::path::is_separator;
+ path.chars()
+ .map(|ch| if is_separator(ch) { '/' } else { ch })
+ .collect::<String>()
+}
+
+/// Write the given data to a file, creating it first if necessary
+pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8]) -> Result<()> {
+ let path = build_dir.join(filename);
+
+ create_file(&path)?.write_all(content).map_err(Into::into)
+}
+
+/// Takes a path and returns a path containing just enough `../` to point to
+/// the root of the given path.
+///
+/// This is mostly interesting for a relative path to point back to the
+/// directory from where the path starts.
+///
+/// ```rust
+/// # use std::path::Path;
+/// # use mdbook::utils::fs::path_to_root;
+/// let path = Path::new("some/relative/path");
+/// assert_eq!(path_to_root(path), "../../");
+/// ```
+///
+/// **note:** it's not very fool-proof, if you find a situation where
+/// it doesn't return the correct path.
+/// Consider [submitting a new issue](https://github.com/rust-lang/mdBook/issues)
+/// or a [pull-request](https://github.com/rust-lang/mdBook/pulls) to improve it.
+pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
+ debug!("path_to_root");
+ // Remove filename and add "../" for every directory
+
+ path.into()
+ .parent()
+ .expect("")
+ .components()
+ .fold(String::new(), |mut s, c| {
+ match c {
+ Component::Normal(_) => s.push_str("../"),
+ _ => {
+ debug!("Other path component... {:?}", c);
+ }
+ }
+ s
+ })
+}
+
+/// This function creates a file and returns it. But before creating the file
+/// it checks every directory in the path to see if it exists,
+/// and if it does not it will be created.
+pub fn create_file(path: &Path) -> Result<File> {
+ debug!("Creating {}", path.display());
+
+ // Construct path
+ if let Some(p) = path.parent() {
+ trace!("Parent directory is: {:?}", p);
+
+ fs::create_dir_all(p)?;
+ }
+
+ File::create(path).map_err(Into::into)
+}
+
+/// Removes all the content of a directory but not the directory itself
+pub fn remove_dir_content(dir: &Path) -> Result<()> {
+ for item in fs::read_dir(dir)? {
+ if let Ok(item) = item {
+ let item = item.path();
+ if item.is_dir() {
+ fs::remove_dir_all(item)?;
+ } else {
+ fs::remove_file(item)?;
+ }
+ }
+ }
+ Ok(())
+}
+
+/// Copies all files of a directory to another one except the files
+/// with the extensions given in the `ext_blacklist` array
+pub fn copy_files_except_ext(
+ from: &Path,
+ to: &Path,
+ recursive: bool,
+ avoid_dir: Option<&PathBuf>,
+ ext_blacklist: &[&str],
+) -> Result<()> {
+ debug!(
+ "Copying all files from {} to {} (blacklist: {:?}), avoiding {:?}",
+ from.display(),
+ to.display(),
+ ext_blacklist,
+ avoid_dir
+ );
+
+ // Check that from and to are different
+ if from == to {
+ return Ok(());
+ }
+
+ for entry in fs::read_dir(from)? {
+ let entry = entry?;
+ let metadata = entry
+ .path()
+ .metadata()
+ .with_context(|| format!("Failed to read {:?}", entry.path()))?;
+
+ // If the entry is a dir and the recursive option is enabled, call itself
+ if metadata.is_dir() && recursive {
+ if entry.path() == to.to_path_buf() {
+ continue;
+ }
+
+ if let Some(avoid) = avoid_dir {
+ if entry.path() == *avoid {
+ continue;
+ }
+ }
+
+ // check if output dir already exists
+ if !to.join(entry.file_name()).exists() {
+ fs::create_dir(&to.join(entry.file_name()))?;
+ }
+
+ copy_files_except_ext(
+ &from.join(entry.file_name()),
+ &to.join(entry.file_name()),
+ true,
+ avoid_dir,
+ ext_blacklist,
+ )?;
+ } else if metadata.is_file() {
+ // Check if it is in the blacklist
+ if let Some(ext) = entry.path().extension() {
+ if ext_blacklist.contains(&ext.to_str().unwrap()) {
+ continue;
+ }
+ }
+ debug!(
+ "creating path for file: {:?}",
+ &to.join(
+ entry
+ .path()
+ .file_name()
+ .expect("a file should have a file name...")
+ )
+ );
+
+ debug!(
+ "Copying {:?} to {:?}",
+ entry.path(),
+ &to.join(
+ entry
+ .path()
+ .file_name()
+ .expect("a file should have a file name...")
+ )
+ );
+ fs::copy(
+ entry.path(),
+ &to.join(
+ entry
+ .path()
+ .file_name()
+ .expect("a file should have a file name..."),
+ ),
+ )?;
+ }
+ }
+ Ok(())
+}
+
+pub fn get_404_output_file(input_404: &Option<String>) -> String {
+ input_404
+ .as_ref()
+ .unwrap_or(&"404.md".to_string())
+ .replace(".md", ".html")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::copy_files_except_ext;
+ use std::{fs, io::Result, path::Path};
+
+ #[cfg(target_os = "windows")]
+ fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
+ std::os::windows::fs::symlink_file(src, dst)
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
+ std::os::unix::fs::symlink(src, dst)
+ }
+
+ #[test]
+ fn copy_files_except_ext_test() {
+ let tmp = match tempfile::TempDir::new() {
+ Ok(t) => t,
+ Err(e) => panic!("Could not create a temp dir: {}", e),
+ };
+
+ // Create a couple of files
+ if let Err(err) = fs::File::create(&tmp.path().join("file.txt")) {
+ panic!("Could not create file.txt: {}", err);
+ }
+ if let Err(err) = fs::File::create(&tmp.path().join("file.md")) {
+ panic!("Could not create file.md: {}", err);
+ }
+ if let Err(err) = fs::File::create(&tmp.path().join("file.png")) {
+ panic!("Could not create file.png: {}", err);
+ }
+ if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir")) {
+ panic!("Could not create sub_dir: {}", err);
+ }
+ if let Err(err) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
+ panic!("Could not create sub_dir/file.png: {}", err);
+ }
+ if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
+ panic!("Could not create sub_dir_exists: {}", err);
+ }
+ if let Err(err) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
+ panic!("Could not create sub_dir_exists/file.txt: {}", err);
+ }
+ if let Err(err) = symlink(
+ &tmp.path().join("file.png"),
+ &tmp.path().join("symlink.png"),
+ ) {
+ panic!("Could not symlink file.png: {}", err);
+ }
+
+ // Create output dir
+ if let Err(err) = fs::create_dir(&tmp.path().join("output")) {
+ panic!("Could not create output: {}", err);
+ }
+ if let Err(err) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
+ panic!("Could not create output/sub_dir_exists: {}", err);
+ }
+
+ if let Err(e) =
+ copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])
+ {
+ panic!("Error while executing the function:\n{:?}", e);
+ }
+
+ // Check if the correct files where created
+ if !(&tmp.path().join("output/file.txt")).exists() {
+ panic!("output/file.txt should exist")
+ }
+ if (&tmp.path().join("output/file.md")).exists() {
+ panic!("output/file.md should not exist")
+ }
+ if !(&tmp.path().join("output/file.png")).exists() {
+ panic!("output/file.png should exist")
+ }
+ if !(&tmp.path().join("output/sub_dir/file.png")).exists() {
+ panic!("output/sub_dir/file.png should exist")
+ }
+ if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
+ panic!("output/sub_dir/file.png should exist")
+ }
+ if !(&tmp.path().join("output/symlink.png")).exists() {
+ panic!("output/symlink.png should exist")
+ }
+ }
+}
diff --git a/vendor/mdbook/src/utils/mod.rs b/vendor/mdbook/src/utils/mod.rs
new file mode 100644
index 000000000..a205633f9
--- /dev/null
+++ b/vendor/mdbook/src/utils/mod.rs
@@ -0,0 +1,494 @@
+#![allow(missing_docs)] // FIXME: Document this
+
+pub mod fs;
+mod string;
+pub(crate) mod toml_ext;
+use crate::errors::Error;
+use regex::Regex;
+
+use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
+
+use std::borrow::Cow;
+use std::collections::HashMap;
+use std::fmt::Write;
+use std::path::Path;
+
+pub use self::string::{
+ take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
+ take_rustdoc_include_lines,
+};
+
+/// Replaces multiple consecutive whitespace characters with a single space character.
+pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
+ lazy_static! {
+ static ref RE: Regex = Regex::new(r"\s\s+").unwrap();
+ }
+ RE.replace_all(text, " ")
+}
+
+/// Convert the given string to a valid HTML element ID.
+/// The only restriction is that the ID must not contain any ASCII whitespace.
+pub fn normalize_id(content: &str) -> String {
+ content
+ .chars()
+ .filter_map(|ch| {
+ if ch.is_alphanumeric() || ch == '_' || ch == '-' {
+ Some(ch.to_ascii_lowercase())
+ } else if ch.is_whitespace() {
+ Some('-')
+ } else {
+ None
+ }
+ })
+ .collect::<String>()
+}
+
+/// Generate an ID for use with anchors which is derived from a "normalised"
+/// string.
+// This function should be made private when the deprecation expires.
+#[deprecated(since = "0.4.16", note = "use unique_id_from_content instead")]
+pub fn id_from_content(content: &str) -> String {
+ let mut content = content.to_string();
+
+ // Skip any tags or html-encoded stuff
+ lazy_static! {
+ static ref HTML: Regex = Regex::new(r"(<.*?>)").unwrap();
+ }
+ content = HTML.replace_all(&content, "").into();
+ const REPL_SUB: &[&str] = &["&lt;", "&gt;", "&amp;", "&#39;", "&quot;"];
+ for sub in REPL_SUB {
+ content = content.replace(sub, "");
+ }
+
+ // Remove spaces and hashes indicating a header
+ let trimmed = content.trim().trim_start_matches('#').trim();
+ normalize_id(trimmed)
+}
+
+/// Generate an ID for use with anchors which is derived from a "normalised"
+/// string.
+///
+/// Each ID returned will be unique, if the same `id_counter` is provided on
+/// each call.
+pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, usize>) -> String {
+ let id = {
+ #[allow(deprecated)]
+ id_from_content(content)
+ };
+
+ // If we have headers with the same normalized id, append an incrementing counter
+ let id_count = id_counter.entry(id.clone()).or_insert(0);
+ let unique_id = match *id_count {
+ 0 => id,
+ id_count => format!("{}-{}", id, id_count),
+ };
+ *id_count += 1;
+ unique_id
+}
+
+/// Fix links to the correct location.
+///
+/// This adjusts links, such as turning `.md` extensions to `.html`.
+///
+/// `path` is the path to the page being rendered relative to the root of the
+/// book. This is used for the `print.html` page so that links on the print
+/// page go to the original location. Normal page rendering sets `path` to
+/// None. Ideally, print page links would link to anchors on the print page,
+/// but that is very difficult.
+fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
+ lazy_static! {
+ static ref SCHEME_LINK: Regex = Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap();
+ static ref MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap();
+ }
+
+ fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
+ if dest.starts_with('#') {
+ // Fragment-only link.
+ if let Some(path) = path {
+ let mut base = path.display().to_string();
+ if base.ends_with(".md") {
+ base.replace_range(base.len() - 3.., ".html");
+ }
+ return format!("{}{}", base, dest).into();
+ } else {
+ return dest;
+ }
+ }
+ // Don't modify links with schemes like `https`.
+ if !SCHEME_LINK.is_match(&dest) {
+ // This is a relative link, adjust it as necessary.
+ let mut fixed_link = String::new();
+ if let Some(path) = path {
+ let base = path
+ .parent()
+ .expect("path can't be empty")
+ .to_str()
+ .expect("utf-8 paths only");
+ if !base.is_empty() {
+ write!(fixed_link, "{}/", base).unwrap();
+ }
+ }
+
+ if let Some(caps) = MD_LINK.captures(&dest) {
+ fixed_link.push_str(&caps["link"]);
+ fixed_link.push_str(".html");
+ if let Some(anchor) = caps.name("anchor") {
+ fixed_link.push_str(anchor.as_str());
+ }
+ } else {
+ fixed_link.push_str(&dest);
+ };
+ return CowStr::from(fixed_link);
+ }
+ dest
+ }
+
+ fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
+ // This is a terrible hack, but should be reasonably reliable. Nobody
+ // should ever parse a tag with a regex. However, there isn't anything
+ // in Rust that I know of that is suitable for handling partial html
+ // fragments like those generated by pulldown_cmark.
+ //
+ // There are dozens of HTML tags/attributes that contain paths, so
+ // feel free to add more tags if desired; these are the only ones I
+ // care about right now.
+ lazy_static! {
+ static ref HTML_LINK: Regex =
+ Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap();
+ }
+
+ HTML_LINK
+ .replace_all(&html, |caps: &regex::Captures<'_>| {
+ let fixed = fix(caps[2].into(), path);
+ format!("{}{}\"", &caps[1], fixed)
+ })
+ .into_owned()
+ .into()
+ }
+
+ match event {
+ Event::Start(Tag::Link(link_type, dest, title)) => {
+ Event::Start(Tag::Link(link_type, fix(dest, path), title))
+ }
+ Event::Start(Tag::Image(link_type, dest, title)) => {
+ Event::Start(Tag::Image(link_type, fix(dest, path), title))
+ }
+ Event::Html(html) => Event::Html(fix_html(html, path)),
+ _ => event,
+ }
+}
+
+/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
+pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
+ render_markdown_with_path(text, curly_quotes, None)
+}
+
+pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> {
+ let mut opts = Options::empty();
+ opts.insert(Options::ENABLE_TABLES);
+ opts.insert(Options::ENABLE_FOOTNOTES);
+ opts.insert(Options::ENABLE_STRIKETHROUGH);
+ opts.insert(Options::ENABLE_TASKLISTS);
+ if curly_quotes {
+ opts.insert(Options::ENABLE_SMART_PUNCTUATION);
+ }
+ Parser::new_ext(text, opts)
+}
+
+pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String {
+ let mut s = String::with_capacity(text.len() * 3 / 2);
+ let p = new_cmark_parser(text, curly_quotes);
+ let events = p
+ .map(clean_codeblock_headers)
+ .map(|event| adjust_links(event, path))
+ .flat_map(|event| {
+ let (a, b) = wrap_tables(event);
+ a.into_iter().chain(b)
+ });
+
+ html::push_html(&mut s, events);
+ s
+}
+
+/// Wraps tables in a `.table-wrapper` class to apply overflow-x rules to.
+fn wrap_tables(event: Event<'_>) -> (Option<Event<'_>>, Option<Event<'_>>) {
+ match event {
+ Event::Start(Tag::Table(_)) => (
+ Some(Event::Html(r#"<div class="table-wrapper">"#.into())),
+ Some(event),
+ ),
+ Event::End(Tag::Table(_)) => (Some(event), Some(Event::Html(r#"</div>"#.into()))),
+ _ => (Some(event), None),
+ }
+}
+
+fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
+ match event {
+ Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info))) => {
+ let info: String = info
+ .chars()
+ .map(|x| match x {
+ ' ' | '\t' => ',',
+ _ => x,
+ })
+ .filter(|ch| !ch.is_whitespace())
+ .collect();
+
+ Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::from(info))))
+ }
+ _ => event,
+ }
+}
+
+/// Prints a "backtrace" of some `Error`.
+pub fn log_backtrace(e: &Error) {
+ error!("Error: {}", e);
+
+ for cause in e.chain().skip(1) {
+ error!("\tCaused By: {}", cause);
+ }
+}
+
+pub(crate) fn bracket_escape(mut s: &str) -> String {
+ let mut escaped = String::with_capacity(s.len());
+ let needs_escape: &[char] = &['<', '>'];
+ while let Some(next) = s.find(needs_escape) {
+ escaped.push_str(&s[..next]);
+ match s.as_bytes()[next] {
+ b'<' => escaped.push_str("&lt;"),
+ b'>' => escaped.push_str("&gt;"),
+ _ => unreachable!(),
+ }
+ s = &s[next + 1..];
+ }
+ escaped.push_str(s);
+ escaped
+}
+
+#[cfg(test)]
+mod tests {
+ use super::bracket_escape;
+
+ mod render_markdown {
+ use super::super::render_markdown;
+
+ #[test]
+ fn preserves_external_links() {
+ assert_eq!(
+ render_markdown("[example](https://www.rust-lang.org/)", false),
+ "<p><a href=\"https://www.rust-lang.org/\">example</a></p>\n"
+ );
+ }
+
+ #[test]
+ fn it_can_adjust_markdown_links() {
+ assert_eq!(
+ render_markdown("[example](example.md)", false),
+ "<p><a href=\"example.html\">example</a></p>\n"
+ );
+ assert_eq!(
+ render_markdown("[example_anchor](example.md#anchor)", false),
+ "<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
+ );
+
+ // this anchor contains 'md' inside of it
+ assert_eq!(
+ render_markdown("[phantom data](foo.html#phantomdata)", false),
+ "<p><a href=\"foo.html#phantomdata\">phantom data</a></p>\n"
+ );
+ }
+
+ #[test]
+ fn it_can_wrap_tables() {
+ let src = r#"
+| Original | Punycode | Punycode + Encoding |
+|-----------------|-----------------|---------------------|
+| føø | f-5gaa | f_5gaa |
+"#;
+ let out = r#"
+<div class="table-wrapper"><table><thead><tr><th>Original</th><th>Punycode</th><th>Punycode + Encoding</th></tr></thead><tbody>
+<tr><td>føø</td><td>f-5gaa</td><td>f_5gaa</td></tr>
+</tbody></table>
+</div>
+"#.trim();
+ assert_eq!(render_markdown(src, false), out);
+ }
+
+ #[test]
+ fn it_can_keep_quotes_straight() {
+ assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
+ }
+
+ #[test]
+ fn it_can_make_quotes_curly_except_when_they_are_in_code() {
+ let input = r#"
+'one'
+```
+'two'
+```
+`'three'` 'four'"#;
+ let expected = r#"<p>‘one’</p>
+<pre><code>'two'
+</code></pre>
+<p><code>'three'</code> ‘four’</p>
+"#;
+ assert_eq!(render_markdown(input, true), expected);
+ }
+
+ #[test]
+ fn whitespace_outside_of_codeblock_header_is_preserved() {
+ let input = r#"
+some text with spaces
+```rust
+fn main() {
+// code inside is unchanged
+}
+```
+more text with spaces
+"#;
+
+ let expected = r#"<p>some text with spaces</p>
+<pre><code class="language-rust">fn main() {
+// code inside is unchanged
+}
+</code></pre>
+<p>more text with spaces</p>
+"#;
+ assert_eq!(render_markdown(input, false), expected);
+ assert_eq!(render_markdown(input, true), expected);
+ }
+
+ #[test]
+ fn rust_code_block_properties_are_passed_as_space_delimited_class() {
+ let input = r#"
+```rust,no_run,should_panic,property_3
+```
+"#;
+
+ let expected = r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
+"#;
+ assert_eq!(render_markdown(input, false), expected);
+ assert_eq!(render_markdown(input, true), expected);
+ }
+
+ #[test]
+ fn rust_code_block_properties_with_whitespace_are_passed_as_space_delimited_class() {
+ let input = r#"
+```rust, no_run,,,should_panic , ,property_3
+```
+"#;
+
+ let expected = r#"<pre><code class="language-rust,,,,,no_run,,,should_panic,,,,property_3"></code></pre>
+"#;
+ assert_eq!(render_markdown(input, false), expected);
+ assert_eq!(render_markdown(input, true), expected);
+ }
+
+ #[test]
+ fn rust_code_block_without_properties_has_proper_html_class() {
+ let input = r#"
+```rust
+```
+"#;
+
+ let expected = r#"<pre><code class="language-rust"></code></pre>
+"#;
+ assert_eq!(render_markdown(input, false), expected);
+ assert_eq!(render_markdown(input, true), expected);
+
+ let input = r#"
+```rust
+```
+"#;
+ assert_eq!(render_markdown(input, false), expected);
+ assert_eq!(render_markdown(input, true), expected);
+ }
+ }
+
+ #[allow(deprecated)]
+ mod id_from_content {
+ use super::super::id_from_content;
+
+ #[test]
+ fn it_generates_anchors() {
+ assert_eq!(
+ id_from_content("## Method-call expressions"),
+ "method-call-expressions"
+ );
+ assert_eq!(id_from_content("## **Bold** title"), "bold-title");
+ assert_eq!(id_from_content("## `Code` title"), "code-title");
+ assert_eq!(
+ id_from_content("## title <span dir=rtl>foo</span>"),
+ "title-foo"
+ );
+ }
+
+ #[test]
+ fn it_generates_anchors_from_non_ascii_initial() {
+ assert_eq!(
+ id_from_content("## `--passes`: add more rustdoc passes"),
+ "--passes-add-more-rustdoc-passes"
+ );
+ assert_eq!(
+ id_from_content("## 中文標題 CJK title"),
+ "中文標題-cjk-title"
+ );
+ assert_eq!(id_from_content("## Über"), "Über");
+ }
+ }
+
+ mod html_munging {
+ use super::super::{normalize_id, unique_id_from_content};
+
+ #[test]
+ fn it_normalizes_ids() {
+ assert_eq!(
+ normalize_id("`--passes`: add more rustdoc passes"),
+ "--passes-add-more-rustdoc-passes"
+ );
+ assert_eq!(
+ normalize_id("Method-call 🐙 expressions \u{1f47c}"),
+ "method-call--expressions-"
+ );
+ assert_eq!(normalize_id("_-_12345"), "_-_12345");
+ assert_eq!(normalize_id("12345"), "12345");
+ assert_eq!(normalize_id("中文"), "中文");
+ assert_eq!(normalize_id("にほんご"), "にほんご");
+ assert_eq!(normalize_id("한국어"), "한국어");
+ assert_eq!(normalize_id(""), "");
+ }
+
+ #[test]
+ fn it_generates_unique_ids_from_content() {
+ // Same id if not given shared state
+ assert_eq!(
+ unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
+ "中文標題-cjk-title"
+ );
+ assert_eq!(
+ unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
+ "中文標題-cjk-title"
+ );
+
+ // Different id if given shared state
+ let mut id_counter = Default::default();
+ assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über");
+ assert_eq!(
+ unique_id_from_content("## 中文標題 CJK title", &mut id_counter),
+ "中文標題-cjk-title"
+ );
+ assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-1");
+ assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-2");
+ }
+ }
+
+ #[test]
+ fn escaped_brackets() {
+ assert_eq!(bracket_escape(""), "");
+ assert_eq!(bracket_escape("<"), "&lt;");
+ assert_eq!(bracket_escape(">"), "&gt;");
+ assert_eq!(bracket_escape("<>"), "&lt;&gt;");
+ assert_eq!(bracket_escape("<test>"), "&lt;test&gt;");
+ assert_eq!(bracket_escape("a<test>b"), "a&lt;test&gt;b");
+ }
+}
diff --git a/vendor/mdbook/src/utils/string.rs b/vendor/mdbook/src/utils/string.rs
new file mode 100644
index 000000000..97485d7b6
--- /dev/null
+++ b/vendor/mdbook/src/utils/string.rs
@@ -0,0 +1,255 @@
+use regex::Regex;
+use std::ops::Bound::{Excluded, Included, Unbounded};
+use std::ops::RangeBounds;
+
+/// Take a range of lines from a string.
+pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
+ let start = match range.start_bound() {
+ Excluded(&n) => n + 1,
+ Included(&n) => n,
+ Unbounded => 0,
+ };
+ let lines = s.lines().skip(start);
+ match range.end_bound() {
+ Excluded(end) => lines
+ .take(end.saturating_sub(start))
+ .collect::<Vec<_>>()
+ .join("\n"),
+ Included(end) => lines
+ .take((end + 1).saturating_sub(start))
+ .collect::<Vec<_>>()
+ .join("\n"),
+ Unbounded => lines.collect::<Vec<_>>().join("\n"),
+ }
+}
+
+lazy_static! {
+ static ref ANCHOR_START: Regex = Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap();
+ static ref ANCHOR_END: Regex = Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap();
+}
+
+/// Take anchored lines from a string.
+/// Lines containing anchor are ignored.
+pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
+ let mut retained = Vec::<&str>::new();
+ let mut anchor_found = false;
+
+ for l in s.lines() {
+ if anchor_found {
+ match ANCHOR_END.captures(l) {
+ Some(cap) => {
+ if &cap["anchor_name"] == anchor {
+ break;
+ }
+ }
+ None => {
+ if !ANCHOR_START.is_match(l) {
+ retained.push(l);
+ }
+ }
+ }
+ } else if let Some(cap) = ANCHOR_START.captures(l) {
+ if &cap["anchor_name"] == anchor {
+ anchor_found = true;
+ }
+ }
+ }
+
+ retained.join("\n")
+}
+
+/// Keep lines contained within the range specified as-is.
+/// For any lines not in the range, include them but use `#` at the beginning. This will hide the
+/// lines from initial display but include them when expanding the code snippet or testing with
+/// rustdoc.
+pub fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
+ let mut output = String::with_capacity(s.len());
+
+ for (index, line) in s.lines().enumerate() {
+ if !range.contains(&index) {
+ output.push_str("# ");
+ }
+ output.push_str(line);
+ output.push('\n');
+ }
+ output.pop();
+ output
+}
+
+/// Keep lines between the anchor comments specified as-is.
+/// For any lines not between the anchors, include them but use `#` at the beginning. This will
+/// hide the lines from initial display but include them when expanding the code snippet or testing
+/// with rustdoc.
+pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
+ let mut output = String::with_capacity(s.len());
+ let mut within_anchored_section = false;
+
+ for l in s.lines() {
+ if within_anchored_section {
+ match ANCHOR_END.captures(l) {
+ Some(cap) => {
+ if &cap["anchor_name"] == anchor {
+ within_anchored_section = false;
+ }
+ }
+ None => {
+ if !ANCHOR_START.is_match(l) {
+ output.push_str(l);
+ output.push('\n');
+ }
+ }
+ }
+ } else if let Some(cap) = ANCHOR_START.captures(l) {
+ if &cap["anchor_name"] == anchor {
+ within_anchored_section = true;
+ }
+ } else if !ANCHOR_END.is_match(l) {
+ output.push_str("# ");
+ output.push_str(l);
+ output.push('\n');
+ }
+ }
+
+ output.pop();
+ output
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
+ take_rustdoc_include_lines,
+ };
+
+ #[test]
+ #[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled
+ fn take_lines_test() {
+ let s = "Lorem\nipsum\ndolor\nsit\namet";
+ assert_eq!(take_lines(s, 1..3), "ipsum\ndolor");
+ assert_eq!(take_lines(s, 3..), "sit\namet");
+ assert_eq!(take_lines(s, ..3), "Lorem\nipsum\ndolor");
+ assert_eq!(take_lines(s, ..), s);
+ // corner cases
+ assert_eq!(take_lines(s, 4..3), "");
+ assert_eq!(take_lines(s, ..100), s);
+ }
+
+ #[test]
+ fn take_anchored_lines_test() {
+ let s = "Lorem\nipsum\ndolor\nsit\namet";
+ assert_eq!(take_anchored_lines(s, "test"), "");
+
+ let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet";
+ assert_eq!(take_anchored_lines(s, "test"), "");
+
+ let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet";
+ assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
+ assert_eq!(take_anchored_lines(s, "something"), "");
+
+ let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
+ assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
+ assert_eq!(take_anchored_lines(s, "something"), "");
+
+ let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
+ assert_eq!(take_anchored_lines(s, "test"), "ipsum\ndolor\nsit\namet");
+ assert_eq!(take_anchored_lines(s, "something"), "");
+
+ let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum";
+ assert_eq!(
+ take_anchored_lines(s, "test2"),
+ "ipsum\ndolor\nsit\namet\nlorem"
+ );
+ assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
+ assert_eq!(take_anchored_lines(s, "something"), "");
+ }
+
+ #[test]
+ #[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled
+ fn take_rustdoc_include_lines_test() {
+ let s = "Lorem\nipsum\ndolor\nsit\namet";
+ assert_eq!(
+ take_rustdoc_include_lines(s, 1..3),
+ "# Lorem\nipsum\ndolor\n# sit\n# amet"
+ );
+ assert_eq!(
+ take_rustdoc_include_lines(s, 3..),
+ "# Lorem\n# ipsum\n# dolor\nsit\namet"
+ );
+ assert_eq!(
+ take_rustdoc_include_lines(s, ..3),
+ "Lorem\nipsum\ndolor\n# sit\n# amet"
+ );
+ assert_eq!(take_rustdoc_include_lines(s, ..), s);
+ // corner cases
+ assert_eq!(
+ take_rustdoc_include_lines(s, 4..3),
+ "# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
+ );
+ assert_eq!(take_rustdoc_include_lines(s, ..100), s);
+ }
+
+ #[test]
+ fn take_rustdoc_include_anchored_lines_test() {
+ let s = "Lorem\nipsum\ndolor\nsit\namet";
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "test"),
+ "# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
+ );
+
+ let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet";
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "test"),
+ "# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
+ );
+
+ let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet";
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "test"),
+ "# Lorem\n# ipsum\ndolor\nsit\namet"
+ );
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "something"),
+ "# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
+ );
+
+ let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "test"),
+ "# Lorem\n# ipsum\ndolor\nsit\namet\n# lorem\n# ipsum"
+ );
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "something"),
+ "# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum"
+ );
+
+ let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "test"),
+ "# Lorem\nipsum\ndolor\nsit\namet\n# lorem\n# ipsum"
+ );
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "something"),
+ "# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum"
+ );
+
+ let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum";
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "test2"),
+ "# Lorem\nipsum\ndolor\nsit\namet\nlorem\n# ipsum"
+ );
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "test"),
+ "# Lorem\n# ipsum\ndolor\nsit\namet\n# lorem\n# ipsum"
+ );
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "something"),
+ "# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum"
+ );
+
+ let s = "Lorem\nANCHOR: test\nipsum\nANCHOR_END: test\ndolor\nANCHOR: test\nsit\nANCHOR_END: test\namet";
+ assert_eq!(
+ take_rustdoc_include_anchored_lines(s, "test"),
+ "# Lorem\nipsum\n# dolor\nsit\n# amet"
+ );
+ }
+}
diff --git a/vendor/mdbook/src/utils/toml_ext.rs b/vendor/mdbook/src/utils/toml_ext.rs
new file mode 100644
index 000000000..bf25ad11b
--- /dev/null
+++ b/vendor/mdbook/src/utils/toml_ext.rs
@@ -0,0 +1,130 @@
+use toml::value::{Table, Value};
+
+pub(crate) trait TomlExt {
+ fn read(&self, key: &str) -> Option<&Value>;
+ fn read_mut(&mut self, key: &str) -> Option<&mut Value>;
+ fn insert(&mut self, key: &str, value: Value);
+ fn delete(&mut self, key: &str) -> Option<Value>;
+}
+
+impl TomlExt for Value {
+ fn read(&self, key: &str) -> Option<&Value> {
+ if let Some((head, tail)) = split(key) {
+ self.get(head)?.read(tail)
+ } else {
+ self.get(key)
+ }
+ }
+
+ fn read_mut(&mut self, key: &str) -> Option<&mut Value> {
+ if let Some((head, tail)) = split(key) {
+ self.get_mut(head)?.read_mut(tail)
+ } else {
+ self.get_mut(key)
+ }
+ }
+
+ fn insert(&mut self, key: &str, value: Value) {
+ if !self.is_table() {
+ *self = Value::Table(Table::new());
+ }
+
+ let table = self.as_table_mut().expect("unreachable");
+
+ if let Some((head, tail)) = split(key) {
+ table
+ .entry(head)
+ .or_insert_with(|| Value::Table(Table::new()))
+ .insert(tail, value);
+ } else {
+ table.insert(key.to_string(), value);
+ }
+ }
+
+ fn delete(&mut self, key: &str) -> Option<Value> {
+ if let Some((head, tail)) = split(key) {
+ self.get_mut(head)?.delete(tail)
+ } else if let Some(table) = self.as_table_mut() {
+ table.remove(key)
+ } else {
+ None
+ }
+ }
+}
+
+fn split(key: &str) -> Option<(&str, &str)> {
+ let ix = key.find('.')?;
+
+ let (head, tail) = key.split_at(ix);
+ // splitting will leave the "."
+ let tail = &tail[1..];
+
+ Some((head, tail))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::str::FromStr;
+
+ #[test]
+ fn read_simple_table() {
+ let src = "[table]";
+ let value = Value::from_str(src).unwrap();
+
+ let got = value.read("table").unwrap();
+
+ assert!(got.is_table());
+ }
+
+ #[test]
+ fn read_nested_item() {
+ let src = "[table]\nnested=true";
+ let value = Value::from_str(src).unwrap();
+
+ let got = value.read("table.nested").unwrap();
+
+ assert_eq!(got, &Value::Boolean(true));
+ }
+
+ #[test]
+ fn insert_item_at_top_level() {
+ let mut value = Value::Table(Table::default());
+ let item = Value::Boolean(true);
+
+ value.insert("first", item.clone());
+
+ assert_eq!(value.get("first").unwrap(), &item);
+ }
+
+ #[test]
+ fn insert_nested_item() {
+ let mut value = Value::Table(Table::default());
+ let item = Value::Boolean(true);
+
+ value.insert("first.second", item.clone());
+
+ let inserted = value.read("first.second").unwrap();
+ assert_eq!(inserted, &item);
+ }
+
+ #[test]
+ fn delete_a_top_level_item() {
+ let src = "top = true";
+ let mut value = Value::from_str(src).unwrap();
+
+ let got = value.delete("top").unwrap();
+
+ assert_eq!(got, Value::Boolean(true));
+ }
+
+ #[test]
+ fn delete_a_nested_item() {
+ let src = "[table]\n nested = true";
+ let mut value = Value::from_str(src).unwrap();
+
+ let got = value.delete("table.nested").unwrap();
+
+ assert_eq!(got, Value::Boolean(true));
+ }
+}