diff --git a/Cargo.toml b/Cargo.toml index cec168f558..0fd2f26c03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ once_cell = "1.17.1" env_logger = "0.11.1" handlebars = "6.0" hex = "0.4.3" +ignore = "0.4.23" log = "0.4.17" memchr = "2.5.0" opener = "0.7.0" @@ -44,7 +45,6 @@ topological-sort = "0.2.2" # Watch feature notify = { version = "8.0.0", optional = true } notify-debouncer-mini = { version = "0.6.0", optional = true } -ignore = { version = "0.4.20", optional = true } pathdiff = { version = "0.2.1", optional = true } walkdir = { version = "2.3.3", optional = true } @@ -67,7 +67,7 @@ walkdir = "2.3.3" [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:warp"] search = ["dep:elasticlunr-rs", "dep:ammonia"] diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index a827d2936f..5d4390eeee 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -318,6 +318,18 @@ The value can be any valid URI the browser should navigate to (e.g. `https://rus This will generate an HTML page which will automatically redirect to the given location. Note that the source location does not support `#` anchor redirects. +### `.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 diff --git a/src/book/mod.rs b/src/book/mod.rs index c5d20130ee..89e7cad15b 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -626,7 +626,8 @@ fn preprocessor_should_run( mod tests { use super::*; use std::str::FromStr; - use toml::value::Table; + use tempfile::Builder as TempFileBuilder; + use toml::value::{Table, Value}; #[test] fn config_defaults_to_html_renderer_if_empty() { @@ -895,4 +896,21 @@ mod tests { let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg); assert_eq!(got, should_be); } + + #[test] + fn mdbookignore_ignores_file() { + let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap(); + let test_book_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_book"); + + utils::fs::copy_files_except_ignored(&test_book_dir, temp_dir.path(), true, None, None) + .expect("Error while copying test book to temp dir"); + + let book = MDBook::load(temp_dir.path()).expect("Unable to load book"); + book.build().expect("Error while building book"); + + let book_dir = temp_dir.path().join("book"); + assert!(book_dir.join("index.html").exists()); + assert!(book_dir.join(".mdbookignore").exists()); + assert!(!book_dir.join("ignored_file").exists()); + } } diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 02951c2559..11ec385603 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -15,6 +15,7 @@ use std::path::{Path, PathBuf}; use crate::utils::fs::get_404_output_file; use handlebars::Handlebars; +use ignore::gitignore::GitignoreBuilder; use log::{debug, trace, warn}; use once_cell::sync::Lazy; use regex::{Captures, Regex}; @@ -479,7 +480,23 @@ impl Renderer for HtmlHandlebars { .context("Unable to emit redirects")?; // Copy all remaining files, avoid a recursive copy from/to the book build dir - utils::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()?; + + utils::fs::copy_files_except_ignored( + &src_dir, + destination, + true, + Some(&build_dir), + Some(&ignore), + )?; Ok(()) } diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 220bcd8b33..9dfe49c3ad 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -1,4 +1,5 @@ use crate::errors::*; +use ignore::gitignore::{Gitignore, GitignoreBuilder}; use log::{debug, trace}; use std::fs::{self, File}; use std::io::Write; @@ -91,12 +92,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 ); @@ -114,6 +133,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() { @@ -126,19 +153,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)?; } @@ -268,7 +296,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/test_book/src/.mdbookignore b/test_book/src/.mdbookignore new file mode 100644 index 0000000000..6a79f808a9 --- /dev/null +++ b/test_book/src/.mdbookignore @@ -0,0 +1 @@ +ignored_file diff --git a/test_book/src/ignored_file b/test_book/src/ignored_file new file mode 100644 index 0000000000..1ce1f71700 --- /dev/null +++ b/test_book/src/ignored_file @@ -0,0 +1 @@ +This will not be copied to the book directory.