Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,17 @@ rust-version.workspace = true

[dependencies]
anyhow.workspace = true
clap.workspace = true
clap_complete.workspace = true
clap.workspace = true
ignore.workspace = true
mdbook-core.workspace = true
mdbook-driver.workspace = true
mdbook-html.workspace = true
opener.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true

# Watch feature
ignore = { workspace = true, optional = true }
notify = { workspace = true, optional = true }
notify-debouncer-mini = { workspace = true, optional = true }
pathdiff = { workspace = true, optional = true }
Expand All @@ -125,7 +125,7 @@ walkdir.workspace = true

[features]
default = ["watch", "serve", "search"]
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"]
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:pathdiff", "dep:walkdir"]
serve = ["dep:futures-util", "dep:listenfd", "dep:tokio", "dep:axum", "dep:tower-http"]
search = ["mdbook-html/search"]

Expand Down
1 change: 1 addition & 0 deletions crates/mdbook-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions crates/mdbook-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,8 @@ pub struct BuildConfig {
pub use_default_preprocessors: bool,
/// Extra directories to trigger rebuild when watching/serving
pub extra_watch_dirs: Vec<PathBuf>,
/// Ignore globs: all matching files under src/ will not be copied to the output
pub ignore: Vec<String>,
}

impl Default for BuildConfig {
Expand All @@ -411,6 +413,7 @@ impl Default for BuildConfig {
create_missing: true,
use_default_preprocessors: true,
extra_watch_dirs: Vec::new(),
ignore: Vec::new(),
}
}
}
Expand Down Expand Up @@ -825,6 +828,7 @@ mod tests {
create_missing: false,
use_default_preprocessors: true,
extra_watch_dirs: Vec::new(),
ignore: Vec::new(),
};
let rust_should_be = RustConfig { edition: None };
let playground_should_be = Playground {
Expand Down
44 changes: 36 additions & 8 deletions crates/mdbook-core/src/utils/fs.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Filesystem utilities and helpers.

use anyhow::{Context, Result};
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use std::fs;
use std::path::{Component, Path, PathBuf};
use tracing::debug;
Expand Down Expand Up @@ -96,12 +97,30 @@ pub fn copy_files_except_ext(
recursive: bool,
avoid_dir: Option<&PathBuf>,
ext_blacklist: &[&str],
) -> Result<()> {
let mut builder = 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 {:?}",
from.display(),
to.display(),
ext_blacklist,
ignore,
avoid_dir
);

Expand All @@ -119,6 +138,14 @@ pub fn copy_files_except_ext(
let entry_file_name = entry.file_name().unwrap();
let target_file_path = to.join(entry_file_name);

// Check if it is in the ignored list
if let Some(ignore) = ignore {
let path = entry.as_path();
if ignore.matched(path, path.is_dir()).is_ignore() {
continue;
}
}

// If the entry is a dir and the recursive option is enabled, call itself
if metadata.is_dir() && recursive {
if entry == to.as_os_str() {
Expand All @@ -131,19 +158,20 @@ pub fn copy_files_except_ext(
}
}

if let Some(ignore) = ignore {
let path = entry.as_path();
if ignore.matched(path, path.is_dir()).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()) {
continue;
}
}
debug!("Copying {entry:?} to {target_file_path:?}");
copy(&entry, &target_file_path)?;
}
Expand Down
1 change: 1 addition & 0 deletions crates/mdbook-html/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion crates/mdbook-html/src/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, BuiltinTheme, Config, HtmlConfig};
use mdbook_core::utils::fs;
Expand Down Expand Up @@ -464,7 +465,22 @@ 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);
builder.add_line(None, "*.md")?;

@GuillaumeGomez GuillaumeGomez Oct 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ignoring all *.md files?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that is what the current code does I assume? My understanding is markdown files are converted to html files so there is no need to include them in the output

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're not copied currently, so that seems strange that we need to have this line to make it work.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a change, this was there before the pull request, and this is the line responsible for not copying the .md files

See https://github.com/rust-lang/mdBook/blob/main/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs#L467, current implementation does it as well

This is what building the guide without this line looks like:
image

for rule in &ctx.config.build.ignore {
builder
.add_line(None, rule)
.expect(&format!("Rule `{rule}` is not a valid glob"));
}
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());

Expand Down
5 changes: 5 additions & 0 deletions guide/src/format/configuration/general.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ build-dir = "book" # the directory where the output is placed
create-missing = true # whether or not to create missing pages
use-default-preprocessors = true # use the default preprocessors
extra-watch-dirs = [] # directories to watch for triggering builds
ignore = [] # globs to ignore when building
```

- **build-dir:** The directory to put the rendered book in. By default this is
Expand All @@ -116,3 +117,7 @@ extra-watch-dirs = [] # directories to watch for triggering builds
- **extra-watch-dirs**: A list of paths to directories that will be watched in
the `watch` and `serve` commands. Changes to files under these directories will
trigger rebuilds. Useful if your book depends on files outside its `src` directory.
- **ignore**: A list of globs that will be ignored when copying files from the source
directory to the book's output directory. All globs will be interpreted after preppending
the source directory specified in the `[book]` config (`src/` by default).
`mdbook` will panic if a glob is invalid.
15 changes: 15 additions & 0 deletions tests/testsuite/ignore.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use crate::prelude::BookTest;

// Simple smoke test that ignore works.
#[test]
fn ignore_file_is_respected() {
let mut test = BookTest::from_dir("ignore/simple");
test.run("build", |_| ());

assert!(test.dir.join("book/index.html").exists());
assert!(test.dir.join("book/normal_file").exists());
assert!(!test.dir.join("book/ignored_file").exists());
// make sure the *.md filtering is still applied properly
assert!(!test.dir.join("book/chapter_1.md").exists());
assert!(!test.dir.join("book/SUMMARY.md").exists());
}
3 changes: 3 additions & 0 deletions tests/testsuite/ignore/simple/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Basic book

This GUI test book is the default book with a single chapter.
5 changes: 5 additions & 0 deletions tests/testsuite/ignore/simple/book.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[book]
title = "ignore"

[build]
ignore = ["ignored_file"]
3 changes: 3 additions & 0 deletions tests/testsuite/ignore/simple/src/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Summary

- [Chapter 1](./chapter_1.md)
1 change: 1 addition & 0 deletions tests/testsuite/ignore/simple/src/chapter_1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Chapter 1
1 change: 1 addition & 0 deletions tests/testsuite/ignore/simple/src/ignored_file
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file should not be copied!
1 change: 1 addition & 0 deletions tests/testsuite/ignore/simple/src/normal_file
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file should be copied!
1 change: 1 addition & 0 deletions tests/testsuite/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ build-dir = "out"
create-missing = true
use-default-preprocessors = true
extra-watch-dirs = []
ignore = []

"#]],
)
Expand Down
1 change: 1 addition & 0 deletions tests/testsuite/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod book_test;
mod build;
mod cli;
mod config;
mod ignore;
mod includes;
mod index;
mod init;
Expand Down
Loading