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/Cargo.toml b/Cargo.toml index 4b40754bb6..18225811aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,17 +89,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 } @@ -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:tokio", "dep:axum", "dep:tower-http"] search = ["mdbook-html/search"] 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..29a4069755 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, GitignoreBuilder}; 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 = 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 ); @@ -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 blacklist + 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() { @@ -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)?; } @@ -247,7 +275,7 @@ mod tests { if let Err(e) = copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"]) { - panic!("Error while executing the function:\n{e:?}"); + panic!("Error while executing the function:\n{:?}", e); } // Check if the correct files where created 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()); diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index 22dfd425fb..a562146324 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -322,7 +322,19 @@ 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. -## Markdown renderer +### `.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 same format as +[`.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 Markdown. This is mostly useful for debugging preprocessors, especially in diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 9ef11dd76e..b024b831fe 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -12,6 +12,7 @@ mod includes; mod index; mod init; mod markdown; +mod mdbookignore; mod playground; mod preprocessor; mod print; diff --git a/tests/testsuite/mdbookignore.rs b/tests/testsuite/mdbookignore.rs new file mode 100644 index 0000000000..dde90c3cd6 --- /dev/null +++ b/tests/testsuite/mdbookignore.rs @@ -0,0 +1,13 @@ +use crate::prelude::BookTest; + +// Simple smoke test that mdbookignore works. +#[test] +fn ignore_file_is_respected() { + let mut test = BookTest::from_dir("mdbookignore/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()); + assert!(!test.dir.join("book/.mdbookignore").exists()); +} diff --git a/tests/testsuite/mdbookignore/simple/README.md b/tests/testsuite/mdbookignore/simple/README.md new file mode 100644 index 0000000000..2c574c3a50 --- /dev/null +++ b/tests/testsuite/mdbookignore/simple/README.md @@ -0,0 +1,3 @@ +# Basic book + +This GUI test book is the default book with a single chapter. diff --git a/tests/testsuite/mdbookignore/simple/book.toml b/tests/testsuite/mdbookignore/simple/book.toml new file mode 100644 index 0000000000..1888cab3e1 --- /dev/null +++ b/tests/testsuite/mdbookignore/simple/book.toml @@ -0,0 +1,2 @@ +[book] +title = "mdbookignore" diff --git a/tests/testsuite/mdbookignore/simple/src/.mdbookignore b/tests/testsuite/mdbookignore/simple/src/.mdbookignore new file mode 100644 index 0000000000..24ea36fcdc --- /dev/null +++ b/tests/testsuite/mdbookignore/simple/src/.mdbookignore @@ -0,0 +1,2 @@ +.mdbookignore +ignored_file diff --git a/tests/testsuite/mdbookignore/simple/src/SUMMARY.md b/tests/testsuite/mdbookignore/simple/src/SUMMARY.md new file mode 100644 index 0000000000..7390c82896 --- /dev/null +++ b/tests/testsuite/mdbookignore/simple/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/tests/testsuite/mdbookignore/simple/src/chapter_1.md b/tests/testsuite/mdbookignore/simple/src/chapter_1.md new file mode 100644 index 0000000000..b743fda354 --- /dev/null +++ b/tests/testsuite/mdbookignore/simple/src/chapter_1.md @@ -0,0 +1 @@ +# Chapter 1 diff --git a/tests/testsuite/mdbookignore/simple/src/ignored_file b/tests/testsuite/mdbookignore/simple/src/ignored_file new file mode 100644 index 0000000000..5ca2d0ebcf --- /dev/null +++ b/tests/testsuite/mdbookignore/simple/src/ignored_file @@ -0,0 +1 @@ +This file should not be copied! diff --git a/tests/testsuite/mdbookignore/simple/src/normal_file b/tests/testsuite/mdbookignore/simple/src/normal_file new file mode 100644 index 0000000000..7462d90d01 --- /dev/null +++ b/tests/testsuite/mdbookignore/simple/src/normal_file @@ -0,0 +1 @@ +This file should be copied!