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::*; /// Load a book into memory from its `src/` directory. pub fn load_book>(src_dir: P, cfg: &BuildConfig) -> Result { 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) .chain_err(|| "Couldn't open SUMMARY.md")? .read_to_string(&mut summary_content)?; let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?; if cfg.create_missing { create_missing(&src_dir, &summary).chain_err(|| "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)?; writeln!(f, "# {}", 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, __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(&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>(&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, { 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, } impl From 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, /// Nested items. pub sub_items: Vec, /// The chapter's location, relative to the `SUMMARY.md` file. pub path: Option, /// An ordered list of the names of each chapter above this one, in the hierarchy. pub parent_names: Vec, } impl Chapter { /// Create a new chapter with the provided content. pub fn new>( name: &str, content: String, path: P, parent_names: Vec, ) -> Chapter { Chapter { name: name.to_string(), content, path: Some(path.into()), parent_names, ..Default::default() } } /// Create a new draft chapter that is not attached to a source markdown file and has /// thus no content. pub fn new_draft(name: &str, parent_names: Vec) -> Self { Chapter { name: name.to_string(), content: String::new(), 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 { match self.path { Some(_) => false, None => true, } } } /// 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>(summary: &Summary, src_dir: P) -> Result { 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 + Clone>( item: &SummaryItem, src_dir: P, parent_names: Vec, ) -> Result { match *item { SummaryItem::Separator => Ok(BookItem::Separator), SummaryItem::Link(ref link) => { load_chapter(link, src_dir, parent_names).map(BookItem::Chapter) } } } fn load_chapter>( link: &Link, src_dir: P, parent_names: Vec, ) -> Result { 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) .chain_err(|| format!("Chapter file not found, {}", link_location.display()))?; let mut content = String::new(); f.read_to_string(&mut content) .chain_err(|| format!("Unable to read \"{}\" ({})", link.name, location.display()))?; 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.clone(); 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::>>()?; 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. /// /// [`Book::iter()`]: struct.Book.html#method.iter pub struct BookItems<'a> { items: VecDeque<&'a BookItem>, } impl<'a> Iterator for BookItems<'a> { type Item = &'a BookItem; fn next(&mut self) -> Option { 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.clone().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 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")), 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")), parent_names: Vec::new(), sub_items: vec![ BookItem::Chapter(nested.clone()), BookItem::Separator, BookItem::Chapter(nested.clone()), ], }); 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")), ..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")), 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 = 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")), 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()); } }