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.
This commit is contained in:
Eric Huss
2025-09-15 07:11:19 -07:00
parent 166a972e9a
commit 3629e2c051
6 changed files with 45 additions and 49 deletions

View File

@@ -46,6 +46,14 @@ impl Book {
}
}
/// A depth-first iterator over each [`Chapter`], skipping draft chapters.
pub fn chapters(&self) -> impl Iterator<Item = &Chapter> {
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<F>(&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<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
self.items.push(item.into());

View File

@@ -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,17 +32,13 @@ impl Renderer for MarkdownRenderer {
}
trace!("markdown render");
for item in book.iter() {
if let BookItem::Chapter(ref ch) = *item {
if !ch.is_draft_chapter() {
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)
.with_context(|| "Unexpected error when constructing destination path")?;

View File

@@ -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]);

View File

@@ -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!(

View File

@@ -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<Book> {
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) {
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)

View File

@@ -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);
}