From 3629e2c0519ed907e2ad3c12ac54a5bbfedde1dc Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Mon, 15 Sep 2025 07:11:19 -0700 Subject: [PATCH] Add an iterator over chapters This adds the `Book::chapters` iterator (and `for_each_chapter_mut`) to iterate over non-draft chapters. This is a common pattern I keep encountering, and I figure it might simplify things. It runs a little risk that callers may not be properly handling every item type, but I think it should be ok. --- crates/mdbook-core/src/book.rs | 28 +++++++++++++++++++ .../builtin_renderers/markdown_renderer.rs | 17 ++++------- .../src/html_handlebars/hbs_renderer.rs | 8 +----- .../mdbook-html/src/html_handlebars/search.rs | 15 +++------- .../mdbook-remove-emphasis/src/main.rs | 16 +++-------- guide/guide-helper/src/lib.rs | 10 ++----- 6 files changed, 45 insertions(+), 49 deletions(-) diff --git a/crates/mdbook-core/src/book.rs b/crates/mdbook-core/src/book.rs index e7fa136895..e38f66599a 100644 --- a/crates/mdbook-core/src/book.rs +++ b/crates/mdbook-core/src/book.rs @@ -46,6 +46,14 @@ impl Book { } } + /// A depth-first iterator over each [`Chapter`], skipping draft chapters. + pub fn chapters(&self) -> impl Iterator { + self.iter().filter_map(|item| match item { + BookItem::Chapter(ch) if !ch.is_draft_chapter() => Some(ch), + _ => None, + }) + } + /// Recursively apply a closure to each item in the book, allowing you to /// mutate them. /// @@ -61,6 +69,26 @@ impl Book { for_each_mut(&mut func, &mut self.items); } + /// Recursively apply a closure to each non-draft chapter in the book, + /// allowing you to mutate them. + pub fn for_each_chapter_mut(&mut self, mut func: F) + where + F: FnMut(&mut Chapter), + { + for_each_mut( + &mut |item| { + let BookItem::Chapter(ch) = item else { + return; + }; + if ch.is_draft_chapter() { + return; + } + func(ch) + }, + &mut self.items, + ); + } + /// Append a `BookItem` to the `Book`. pub fn push_item>(&mut self, item: I) -> &mut Self { self.items.push(item.into()); diff --git a/crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs b/crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs index d0f0a744e8..4c9931ab53 100644 --- a/crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs +++ b/crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs @@ -1,5 +1,4 @@ use anyhow::{Context, Result}; -use mdbook_core::book::BookItem; use mdbook_core::utils; use mdbook_renderer::{RenderContext, Renderer}; use std::fs; @@ -33,16 +32,12 @@ impl Renderer for MarkdownRenderer { } trace!("markdown render"); - for item in book.iter() { - if let BookItem::Chapter(ref ch) = *item { - if !ch.is_draft_chapter() { - utils::fs::write_file( - &ctx.destination, - ch.path.as_ref().expect("Checked path exists before"), - ch.content.as_bytes(), - )?; - } - } + for ch in book.chapters() { + utils::fs::write_file( + &ctx.destination, + ch.path.as_ref().expect("Checked path exists before"), + ch.content.as_bytes(), + )?; } fs::create_dir_all(destination) diff --git a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs index a0f01c7a98..7a5663433e 100644 --- a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs +++ b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs @@ -459,13 +459,7 @@ impl Renderer for HtmlHandlebars { utils::fs::write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?; } - let chapters: Vec<_> = book - .iter() - .filter_map(|item| match item { - BookItem::Chapter(ch) if !ch.is_draft_chapter() => Some(ch), - _ => None, - }) - .collect(); + let chapters: Vec<_> = book.chapters().collect(); for (i, ch) in chapters.iter().enumerate() { let previous = (i != 0).then(|| chapters[i - 1]); let next = (i != chapters.len() - 1).then(|| chapters[i + 1]); diff --git a/crates/mdbook-html/src/html_handlebars/search.rs b/crates/mdbook-html/src/html_handlebars/search.rs index 9575ea3168..fededd4b5e 100644 --- a/crates/mdbook-html/src/html_handlebars/search.rs +++ b/crates/mdbook-html/src/html_handlebars/search.rs @@ -2,7 +2,7 @@ use super::static_files::StaticFiles; use crate::theme::searcher; use anyhow::{Context, Result, bail}; use elasticlunr::{Index, IndexBuilder}; -use mdbook_core::book::{Book, BookItem, Chapter}; +use mdbook_core::book::{Book, Chapter}; use mdbook_core::config::{Search, SearchChapterSettings}; use mdbook_core::utils; use mdbook_markdown::HtmlRenderOptions; @@ -43,11 +43,7 @@ pub(super) fn create_files( let chapter_configs = sort_search_config(&search_config.chapter); validate_chapter_config(&chapter_configs, book)?; - for item in book.iter() { - let chapter = match item { - BookItem::Chapter(ch) if !ch.is_draft_chapter() => ch, - _ => continue, - }; + for chapter in book.chapters() { if let Some(path) = settings_path(chapter) { let chapter_settings = get_chapter_settings(&chapter_configs, path); if !chapter_settings.enable.unwrap_or(true) { @@ -349,11 +345,8 @@ fn validate_chapter_config( ) -> Result<()> { for (path, _) in chapter_configs { let found = book - .iter() - .filter_map(|item| match item { - BookItem::Chapter(ch) if !ch.is_draft_chapter() => settings_path(ch), - _ => None, - }) + .chapters() + .filter_map(|ch| settings_path(ch)) .any(|source_path| source_path.starts_with(path)); if !found { bail!( diff --git a/examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs b/examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs index 95974d8fd8..699b0f67c3 100644 --- a/examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs +++ b/examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs @@ -1,7 +1,7 @@ //! This is a demonstration of an mdBook preprocessor which parses markdown //! and removes any instances of emphasis. -use mdbook_preprocessor::book::{Book, BookItem, Chapter}; +use mdbook_preprocessor::book::{Book, Chapter}; use mdbook_preprocessor::errors::Result; use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use pulldown_cmark::{Event, Parser, Tag, TagEnd}; @@ -36,17 +36,9 @@ impl Preprocessor for RemoveEmphasis { fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result { let mut total = 0; - book.for_each_mut(|item| { - let BookItem::Chapter(ch) = item else { - return; - }; - if ch.is_draft_chapter() { - return; - } - match remove_emphasis(&mut total, ch) { - Ok(s) => ch.content = s, - Err(e) => eprintln!("failed to process chapter: {e:?}"), - } + book.for_each_chapter_mut(|ch| match remove_emphasis(&mut total, ch) { + Ok(s) => ch.content = s, + Err(e) => eprintln!("failed to process chapter: {e:?}"), }); eprintln!("removed {total} emphasis"); Ok(book) diff --git a/guide/guide-helper/src/lib.rs b/guide/guide-helper/src/lib.rs index 89cc935b72..28969ccb73 100644 --- a/guide/guide-helper/src/lib.rs +++ b/guide/guide-helper/src/lib.rs @@ -1,6 +1,6 @@ //! Preprocessor for the mdBook guide. -use mdbook_preprocessor::book::{Book, BookItem}; +use mdbook_preprocessor::book::Book; use mdbook_preprocessor::errors::Result; use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use semver::{Version, VersionReq}; @@ -53,13 +53,7 @@ fn insert_version(book: &mut Book) { let manifest: toml::Value = toml::from_str(&manifest_contents).unwrap(); let version = manifest["package"]["version"].as_str().unwrap(); const MARKER: &str = "{{ mdbook-version }}"; - book.for_each_mut(|item| { - let BookItem::Chapter(ch) = item else { - return; - }; - if ch.is_draft_chapter() { - return; - } + book.for_each_chapter_mut(|ch| { if ch.content.contains(MARKER) { ch.content = ch.content.replace(MARKER, version); }