//! A tree structure representing a book. use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::fmt::{self, Display, Formatter}; use std::ops::{Deref, DerefMut}; use std::path::PathBuf; #[cfg(test)] mod tests; /// A tree structure representing a book. /// /// 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 item 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)] #[non_exhaustive] pub struct Book { /// The items in this book. pub items: Vec, } impl Book { /// Create an empty book. pub fn new() -> Self { Default::default() } /// Creates a new book with the given items. pub fn new_with_items(items: Vec) -> Book { Book { items } } /// Get a depth-first iterator over the items in the book. pub fn iter(&self) -> BookItems<'_> { BookItems { items: self.items.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.items); } /// Append a `BookItem` to the `Book`. pub fn push_item>(&mut self, item: I) -> &mut Self { self.items.push(item.into()); self } } 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)] #[non_exhaustive] pub enum BookItem { /// A nested chapter. Chapter(Chapter), /// A section separator. Separator, /// A part title. PartTitle(String), } 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)] #[non_exhaustive] 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. /// /// **Note**: After the index preprocessor runs, any README files will be /// modified to be `index.md`. If you need access to the actual filename /// on disk, use [`Chapter::source_path`] instead. /// /// This is `None` for a draft chapter. pub path: Option, /// The chapter's source file, relative to the `SUMMARY.md` file. /// /// **Note**: Beware that README files will internally be treated as /// `index.md` via the [`Chapter::path`] field. The `source_path` field /// exists if you need access to the true file path. /// /// This is `None` for a draft chapter, or a synthetically generated /// chapter that has no file on disk. pub source_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, p: P, parent_names: Vec, ) -> 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) -> 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() } } 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) } } /// A section number like "1.2.3", basically just a newtype'd `Vec` with /// a pretty `Display` impl. #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] pub struct SectionNumber(Vec); impl SectionNumber { /// Creates a new [`SectionNumber`]. pub fn new(numbers: impl Into>) -> SectionNumber { SectionNumber(numbers.into()) } } 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; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for SectionNumber { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl FromIterator for SectionNumber { fn from_iter>(it: I) -> Self { SectionNumber(it.into_iter().collect()) } } /// 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 { let item = self.items.pop_front(); if let Some(BookItem::Chapter(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 } }