summaryrefslogtreecommitdiffstats
path: root/vendor/mdbook/src/book
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/book
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/book')
-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
4 files changed, 2780 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);
+ }
+}