From e41190cd86e8824db1e1d4d114cbe78b55a1c3e4 Mon Sep 17 00:00:00 2001 From: Jonathan Hult Date: Sat, 21 Feb 2026 20:18:47 -0600 Subject: [PATCH 1/4] feat(fs): add copy_files_except_ignored with ignore support Add a new function that uses the crate to filter files during copy operations, supporting gitignore-style patterns. build: update Cargo.lock for ignore dependency --- Cargo.lock | 2 ++ crates/mdbook-core/Cargo.toml | 1 + crates/mdbook-core/src/utils/fs.rs | 38 +++++++++++++++++++++++++----- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 058a5d5517..685672b75d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,6 +1029,7 @@ name = "mdbook-core" version = "0.5.2" dependencies = [ "anyhow", + "ignore", "regex", "serde", "serde_json", @@ -1070,6 +1071,7 @@ dependencies = [ "handlebars", "hex", "html5ever 0.38.0", + "ignore", "indexmap", "mdbook-core", "mdbook-markdown", diff --git a/crates/mdbook-core/Cargo.toml b/crates/mdbook-core/Cargo.toml index f1aada8282..42040111f1 100644 --- a/crates/mdbook-core/Cargo.toml +++ b/crates/mdbook-core/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true +ignore.workspace = true regex.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/mdbook-core/src/utils/fs.rs b/crates/mdbook-core/src/utils/fs.rs index 2e61d87d93..103e90e5ec 100644 --- a/crates/mdbook-core/src/utils/fs.rs +++ b/crates/mdbook-core/src/utils/fs.rs @@ -1,6 +1,7 @@ //! Filesystem utilities and helpers. use anyhow::{Context, Result}; +use ignore::gitignore::Gitignore; use std::fs; use std::path::{Component, Path, PathBuf}; use tracing::debug; @@ -96,12 +97,30 @@ pub fn copy_files_except_ext( recursive: bool, avoid_dir: Option<&PathBuf>, ext_blacklist: &[&str], +) -> Result<()> { + let mut builder = ignore::gitignore::GitignoreBuilder::new(from); + for ext in ext_blacklist { + builder.add_line(None, &format!("*.{ext}"))?; + } + let ignore = builder.build()?; + + copy_files_except_ignored(from, to, recursive, avoid_dir, Some(&ignore)) +} + +/// Copies all files of a directory to another one except the files that are +/// ignored by the passed [`Gitignore`] +pub fn copy_files_except_ignored( + from: &Path, + to: &Path, + recursive: bool, + avoid_dir: Option<&PathBuf>, + ignore: Option<&Gitignore>, ) -> Result<()> { debug!( - "Copying all files from {} to {} (blacklist: {:?}), avoiding {:?}", + "Copying all files from {} to {} (ignoring: {:?}), avoiding {:?}", from.display(), to.display(), - ext_blacklist, + ignore, avoid_dir ); @@ -131,16 +150,23 @@ pub fn copy_files_except_ext( } } + if let Some(ignore) = ignore { + let path = entry.as_path(); + if ignore.matched(path, true).is_ignore() { + continue; + } + } + // check if output dir already exists if !target_file_path.exists() { fs::create_dir(&target_file_path)?; } - copy_files_except_ext(&entry, &target_file_path, true, avoid_dir, ext_blacklist)?; + copy_files_except_ignored(&entry, &target_file_path, true, avoid_dir, ignore)?; } else if metadata.is_file() { - // Check if it is in the blacklist - if let Some(ext) = entry.extension() { - if ext_blacklist.contains(&ext.to_str().unwrap()) { + if let Some(ignore) = ignore { + let path = entry.as_path(); + if ignore.matched(path, false).is_ignore() { continue; } } From 2ebf1c4d2c5716db2c296b431a719d14233607a2 Mon Sep 17 00:00:00 2001 From: Jonathan Hult Date: Sat, 21 Feb 2026 20:18:57 -0600 Subject: [PATCH 2/4] feat(html): support .mdbookignore file for excluding files Load .mdbookignore file from src directory and use it to filter files copied during the HTML build process. --- crates/mdbook-html/Cargo.toml | 1 + .../src/html_handlebars/hbs_renderer.rs | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/mdbook-html/Cargo.toml b/crates/mdbook-html/Cargo.toml index 7ca8b08090..662a2e9f1a 100644 --- a/crates/mdbook-html/Cargo.toml +++ b/crates/mdbook-html/Cargo.toml @@ -15,6 +15,7 @@ font-awesome-as-a-crate.workspace = true handlebars.workspace = true hex.workspace = true html5ever.workspace = true +ignore.workspace = true indexmap.workspace = true mdbook-core.workspace = true mdbook-markdown.workspace = true diff --git a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs index 8edac3cace..3c7313905b 100644 --- a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs +++ b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs @@ -6,6 +6,7 @@ use crate::theme::Theme; use crate::utils::ToUrlPath; use anyhow::{Context, Result, bail}; use handlebars::Handlebars; +use ignore::gitignore::GitignoreBuilder; use mdbook_core::book::{Book, BookItem, Chapter}; use mdbook_core::config::{BookConfig, Config, HtmlConfig}; use mdbook_core::utils::fs; @@ -444,7 +445,23 @@ impl Renderer for HtmlHandlebars { .context("Unable to emit redirects")?; // Copy all remaining files, avoid a recursive copy from/to the book build dir - fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?; + let mut builder = GitignoreBuilder::new(&src_dir); + let mdbook_ignore = src_dir.join(".mdbookignore"); + if mdbook_ignore.exists() { + if let Some(err) = builder.add(mdbook_ignore) { + warn!("Unable to load '.mdbookignore' file: {}", err); + } + } + builder.add_line(None, "*.md")?; + let ignore = builder.build()?; + + fs::copy_files_except_ignored( + &src_dir, + destination, + true, + Some(&build_dir), + Some(&ignore), + )?; info!("HTML book written to `{}`", destination.display()); From 20fd24c6f13f82178352a05b597e6cc9ef4952d5 Mon Sep 17 00:00:00 2001 From: Jonathan Hult Date: Sat, 21 Feb 2026 20:19:33 -0600 Subject: [PATCH 3/4] docs: document .mdbookignore file feature --- guide/src/format/configuration/renderers.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index 22dfd425fb..2a705c86a9 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -322,6 +322,18 @@ This will generate an HTML page which will automatically redirect to the given l When fragment redirects are specified, the page must use JavaScript to redirect to the correct location. This is useful if you rename or move a section header. Fragment redirects work with existing pages and deleted pages. +### `.mdbookignore` + +You can use a `.mdbookignore` file to exclude files from the build process. +The file is placed in the `src` directory of your book and has the format known +from [`.gitignore`](https://git-scm.com/docs/gitignore) files. + +For example: +``` +*.rs +/target/ +``` + ## Markdown renderer The Markdown renderer will run preprocessors and then output the resulting From a42c81df2c424c933664365793ab67485a7979e3 Mon Sep 17 00:00:00 2001 From: Jonathan Hult Date: Sat, 21 Feb 2026 20:19:46 -0600 Subject: [PATCH 4/4] test(build): add tests for .mdbookignore feature --- crates/mdbook-core/src/utils/fs.rs | 53 +++++++++++++++++++ tests/testsuite/build.rs | 15 ++++++ tests/testsuite/build/mdbookignore/book.toml | 2 + .../build/mdbookignore/src/.mdbookignore | 3 ++ .../build/mdbookignore/src/SUMMARY.md | 3 ++ .../build/mdbookignore/src/chapter_1.md | 3 ++ .../build/mdbookignore/src/ignored.txt | 1 + .../mdbookignore/src/ignored_dir/file.json | 1 + .../build/mdbookignore/src/ignored_file | 1 + .../build/mdbookignore/src/included.json | 1 + 10 files changed, 83 insertions(+) create mode 100644 tests/testsuite/build/mdbookignore/book.toml create mode 100644 tests/testsuite/build/mdbookignore/src/.mdbookignore create mode 100644 tests/testsuite/build/mdbookignore/src/SUMMARY.md create mode 100644 tests/testsuite/build/mdbookignore/src/chapter_1.md create mode 100644 tests/testsuite/build/mdbookignore/src/ignored.txt create mode 100644 tests/testsuite/build/mdbookignore/src/ignored_dir/file.json create mode 100644 tests/testsuite/build/mdbookignore/src/ignored_file create mode 100644 tests/testsuite/build/mdbookignore/src/included.json diff --git a/crates/mdbook-core/src/utils/fs.rs b/crates/mdbook-core/src/utils/fs.rs index 103e90e5ec..e054fe721c 100644 --- a/crates/mdbook-core/src/utils/fs.rs +++ b/crates/mdbook-core/src/utils/fs.rs @@ -236,6 +236,7 @@ fn copy, Q: AsRef>(from: P, to: Q) -> Result<()> { #[cfg(test)] mod tests { use super::*; + use ignore::gitignore::GitignoreBuilder; use std::io::Result; use std::path::Path; @@ -296,4 +297,56 @@ mod tests { panic!("output/symlink.png should exist") } } + + #[test] + fn copy_files_except_ignored_test() { + let tmp = match tempfile::TempDir::new() { + Ok(t) => t, + Err(e) => panic!("Could not create a temp dir: {e}"), + }; + + // Create files and directories + write(tmp.path().join("file.txt"), "").unwrap(); + write(tmp.path().join("file.json"), "").unwrap(); + write(tmp.path().join("ignored_dir/nested.txt"), "").unwrap(); + write(tmp.path().join("included_dir/file.json"), "").unwrap(); + write(tmp.path().join("included_dir/file.txt"), "").unwrap(); + + // Create a gitignore that ignores *.txt and ignored_dir/ + let mut builder = GitignoreBuilder::new(tmp.path()); + builder.add_line(None, "*.txt").unwrap(); + builder.add_line(None, "ignored_dir/").unwrap(); + let ignore = builder.build().unwrap(); + + // Create output dir + create_dir_all(tmp.path().join("output")).unwrap(); + + copy_files_except_ignored( + tmp.path(), + &tmp.path().join("output"), + true, + None, + Some(&ignore), + ) + .unwrap(); + + // Check that .txt files are ignored + if tmp.path().join("output/file.txt").exists() { + panic!("output/file.txt should not exist") + } + if tmp.path().join("output/included_dir/file.txt").exists() { + panic!("output/included_dir/file.txt should not exist") + } + // Check that ignored_dir is not copied + if tmp.path().join("output/ignored_dir").exists() { + panic!("output/ignored_dir should not exist") + } + // Check that non-ignored files are copied + if !tmp.path().join("output/file.json").exists() { + panic!("output/file.json should exist") + } + if !tmp.path().join("output/included_dir/file.json").exists() { + panic!("output/included_dir/file.json should exist") + } + } } diff --git a/tests/testsuite/build.rs b/tests/testsuite/build.rs index 889a9e65ea..e8b2cec0ce 100644 --- a/tests/testsuite/build.rs +++ b/tests/testsuite/build.rs @@ -85,3 +85,18 @@ fn dest_dir_relative_path() { }); assert!(current_dir.join("foo/index.html").exists()); } + +// Test that .mdbookignore files are respected (single files, glob patterns, and directories). +#[test] +fn mdbookignore() { + let mut test = BookTest::from_dir("build/mdbookignore"); + test.build(); + // Single file listed by name should not be copied. + assert!(!test.dir.join("book/ignored_file").exists()); + // Files matching *.txt glob pattern should not be copied. + assert!(!test.dir.join("book/ignored.txt").exists()); + // Directories listed in .mdbookignore should not be copied. + assert!(!test.dir.join("book/ignored_dir").exists()); + // Non-ignored files should be copied. + assert!(test.dir.join("book/included.json").exists()); +} diff --git a/tests/testsuite/build/mdbookignore/book.toml b/tests/testsuite/build/mdbookignore/book.toml new file mode 100644 index 0000000000..3757bf7825 --- /dev/null +++ b/tests/testsuite/build/mdbookignore/book.toml @@ -0,0 +1,2 @@ +[book] +title = "mdbookignore test" diff --git a/tests/testsuite/build/mdbookignore/src/.mdbookignore b/tests/testsuite/build/mdbookignore/src/.mdbookignore new file mode 100644 index 0000000000..02013c445d --- /dev/null +++ b/tests/testsuite/build/mdbookignore/src/.mdbookignore @@ -0,0 +1,3 @@ +ignored_file +*.txt +ignored_dir/ diff --git a/tests/testsuite/build/mdbookignore/src/SUMMARY.md b/tests/testsuite/build/mdbookignore/src/SUMMARY.md new file mode 100644 index 0000000000..7390c82896 --- /dev/null +++ b/tests/testsuite/build/mdbookignore/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/tests/testsuite/build/mdbookignore/src/chapter_1.md b/tests/testsuite/build/mdbookignore/src/chapter_1.md new file mode 100644 index 0000000000..bb56326c68 --- /dev/null +++ b/tests/testsuite/build/mdbookignore/src/chapter_1.md @@ -0,0 +1,3 @@ +# Chapter 1 + +This is chapter 1. diff --git a/tests/testsuite/build/mdbookignore/src/ignored.txt b/tests/testsuite/build/mdbookignore/src/ignored.txt new file mode 100644 index 0000000000..033cb38984 --- /dev/null +++ b/tests/testsuite/build/mdbookignore/src/ignored.txt @@ -0,0 +1 @@ +This txt file should not be copied. diff --git a/tests/testsuite/build/mdbookignore/src/ignored_dir/file.json b/tests/testsuite/build/mdbookignore/src/ignored_dir/file.json new file mode 100644 index 0000000000..ffd978e17a --- /dev/null +++ b/tests/testsuite/build/mdbookignore/src/ignored_dir/file.json @@ -0,0 +1 @@ +This file in ignored dir should not be copied. diff --git a/tests/testsuite/build/mdbookignore/src/ignored_file b/tests/testsuite/build/mdbookignore/src/ignored_file new file mode 100644 index 0000000000..de6ea7cd72 --- /dev/null +++ b/tests/testsuite/build/mdbookignore/src/ignored_file @@ -0,0 +1 @@ +This file should not be copied. diff --git a/tests/testsuite/build/mdbookignore/src/included.json b/tests/testsuite/build/mdbookignore/src/included.json new file mode 100644 index 0000000000..e97bd2e962 --- /dev/null +++ b/tests/testsuite/build/mdbookignore/src/included.json @@ -0,0 +1 @@ +This file should be copied.