diff --git a/crates/mdbook-core/src/book.rs b/crates/mdbook-core/src/book.rs index e38f66599a..6a992756f7 100644 --- a/crates/mdbook-core/src/book.rs +++ b/crates/mdbook-core/src/book.rs @@ -89,6 +89,14 @@ impl Book { ); } + /// Recursively collect all chapters in the book as mutable thin references, + /// allowing you to mutate them in parallel. + pub fn chapters_mut_thin(&mut self) -> Vec> { + let mut chapters_thin = Vec::new(); + chapters_mut_thin(&mut self.items, &mut chapters_thin); + chapters_thin + } + /// Append a `BookItem` to the `Book`. pub fn push_item>(&mut self, item: I) -> &mut Self { self.items.push(item.into()); @@ -110,6 +118,32 @@ where } } +/// Collect all chapters in the book. +pub fn chapters_mut_thin<'a>(items: &'a mut [BookItem], accumulator: &mut Vec>) { + items.iter_mut().for_each(move |item| { + if let BookItem::Chapter(Chapter { + name, + content, + number, + sub_items, + path, + source_path, + parent_names, + }) = item + { + accumulator.push(ChapterMutThin { + name, + content, + number, + path, + source_path, + parent_names, + }); + chapters_mut_thin(sub_items, accumulator); + } + }) +} + /// Enum representing any type of item which can be added to a book. #[allow( clippy::exhaustive_enums, @@ -206,6 +240,30 @@ impl Chapter { } } +/// A thin mutable reference to a chapter for parallel book patching. +#[non_exhaustive] +pub struct ChapterMutThin<'a> { + /// The chapter's name. + pub name: &'a mut String, + /// The chapter's contents. + pub content: &'a mut String, + /// The chapter's section number, if it has one. + pub number: &'a mut Option, + /// The chapter's location, relative to the `SUMMARY.md` file. + pub path: &'a mut Option, + /// The chapter's source file, relative to the `SUMMARY.md` file. + pub source_path: &'a mut Option, + /// An ordered list of the names of each chapter above this one in the hierarchy. + pub parent_names: &'a mut Vec, +} + +impl ChapterMutThin<'_> { + /// 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 { diff --git a/crates/mdbook-core/src/book/tests.rs b/crates/mdbook-core/src/book/tests.rs index 54cb005bbd..4f966dbe9c 100644 --- a/crates/mdbook-core/src/book/tests.rs +++ b/crates/mdbook-core/src/book/tests.rs @@ -121,3 +121,56 @@ fn iterate_over_nested_book_items() { assert_eq!(chapter_names, should_be); } + +#[test] +fn chapters_mut_thin_visits_nested_chapters() { + let items = vec![BookItem::Chapter(Chapter { + name: String::from("Chapter 1"), + content: String::from("# Chapter 1"), + 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::from("hello"), + "Chapter_1/hello.md", + Vec::new(), + )), + BookItem::Chapter(Chapter::new( + "Goodbye World", + String::from("goodbye"), + "Chapter_1/goodbye.md", + Vec::new(), + )), + ], + })]; + let mut book = Book::new_with_items(items); + + let mut chapters = book.chapters_mut_thin(); + assert_eq!(chapters.len(), 3); + + chapters[0].name.push_str(" updated"); + chapters[1].content.push_str(" world"); + + drop(chapters); + + let chapter_names: Vec<_> = book + .iter() + .filter_map(|item| match item { + BookItem::Chapter(chapter) => Some(chapter.name.clone()), + _ => None, + }) + .collect(); + assert_eq!(chapter_names[0], "Chapter 1 updated"); + + let chapter_contents: Vec<_> = book + .iter() + .filter_map(|item| match item { + BookItem::Chapter(chapter) => Some(chapter.content.clone()), + _ => None, + }) + .collect(); + assert_eq!(chapter_contents[1], "hello world"); +}