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.

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
91 changes: 85 additions & 6 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;
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 = 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
);

Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -210,6 +236,7 @@ fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use ignore::gitignore::GitignoreBuilder;
use std::io::Result;
use std::path::Path;

Expand Down Expand Up @@ -270,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")
}
}
}
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
19 changes: 18 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, Config, HtmlConfig};
use mdbook_core::utils::fs;
Expand Down Expand Up @@ -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());

Expand Down
12 changes: 12 additions & 0 deletions guide/src/format/configuration/renderers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions tests/testsuite/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
2 changes: 2 additions & 0 deletions tests/testsuite/build/mdbookignore/book.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[book]
title = "mdbookignore test"
3 changes: 3 additions & 0 deletions tests/testsuite/build/mdbookignore/src/.mdbookignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ignored_file
*.txt
ignored_dir/
3 changes: 3 additions & 0 deletions tests/testsuite/build/mdbookignore/src/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Summary

- [Chapter 1](./chapter_1.md)
3 changes: 3 additions & 0 deletions tests/testsuite/build/mdbookignore/src/chapter_1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Chapter 1

This is chapter 1.
1 change: 1 addition & 0 deletions tests/testsuite/build/mdbookignore/src/ignored.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This txt file should not be copied.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file in ignored dir should not be copied.
1 change: 1 addition & 0 deletions tests/testsuite/build/mdbookignore/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/build/mdbookignore/src/included.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file should be copied.