From 502bb4d1b41ee79b926943179331fb073fc74043 Mon Sep 17 00:00:00 2001 From: Ben Falk Date: Thu, 18 Dec 2025 15:18:38 -0500 Subject: [PATCH 1/2] Support for HTML Renderer to honor `.gitignore` The Watcher and Poller already respected `.gitignore` files when monitoring for changes; however, the HTML Renderer did not. This update ensures that the HTML Renderer also honors `.gitignore`. This prevents ignored files from being processed and included in the generated HTML output. I noticed this when working on a project where the mdbook source directory was the root of the project. mdBook was taking a long time to copy the entire contents of the `target` directory into the book which was gigabytes in size. After this change, the HTML renderer correctly ignored the `target` directory as specified in the `.gitignore` file, significantly speeding up the build process. --- Cargo.lock | 2 + crates/mdbook-core/src/utils/fs.rs | 41 +++++++++++---- crates/mdbook-html/Cargo.toml | 2 + .../src/html_handlebars/hbs_renderer.rs | 52 ++++++++++++++++++- 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b35d30c61..44c6b2a450 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1027,10 +1027,12 @@ dependencies = [ "handlebars", "hex", "html5ever 0.36.1", + "ignore", "indexmap", "mdbook-core", "mdbook-markdown", "mdbook-renderer", + "pathdiff", "pulldown-cmark", "regex", "serde", diff --git a/crates/mdbook-core/src/utils/fs.rs b/crates/mdbook-core/src/utils/fs.rs index 2e61d87d93..1ab41c83fe 100644 --- a/crates/mdbook-core/src/utils/fs.rs +++ b/crates/mdbook-core/src/utils/fs.rs @@ -105,6 +105,31 @@ pub fn copy_files_except_ext( avoid_dir ); + copy_files_when(from, to, recursive, &|path| { + if let Some(avoid) = avoid_dir { + if path == avoid { + return false; + } + } + + if let Some(ext) = path.extension() { + if ext_blacklist.contains(&ext.to_str().unwrap()) { + return false; + } + } + + true + }) +} + +/// Copies all files of a directory to another one +/// when the copy logic returns true. It should be +/// noted that the predicate is required by reference +/// should it be called recursively. +pub fn copy_files_when(from: &Path, to: &Path, recursive: bool, predicate: &T) -> Result<()> +where + T: Fn(&PathBuf) -> bool, +{ // Check that from and to are different if from == to { return Ok(()); @@ -125,10 +150,9 @@ pub fn copy_files_except_ext( continue; } - if let Some(avoid) = avoid_dir { - if entry == *avoid { - continue; - } + if !predicate(&entry) { + debug!("Skipping copy {entry:?}"); + continue; } // check if output dir already exists @@ -136,13 +160,12 @@ pub fn copy_files_except_ext( fs::create_dir(&target_file_path)?; } - copy_files_except_ext(&entry, &target_file_path, true, avoid_dir, ext_blacklist)?; + copy_files_when(&entry, &target_file_path, true, predicate)?; } 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; - } + if !predicate(&entry) { + debug!("Skipping copy {entry:?}"); + continue; } debug!("Copying {entry:?} to {target_file_path:?}"); copy(&entry, &target_file_path)?; diff --git a/crates/mdbook-html/Cargo.toml b/crates/mdbook-html/Cargo.toml index 7ca8b08090..00b0d275f6 100644 --- a/crates/mdbook-html/Cargo.toml +++ b/crates/mdbook-html/Cargo.toml @@ -14,6 +14,8 @@ elasticlunr-rs = { workspace = true, optional = true } font-awesome-as-a-crate.workspace = true handlebars.workspace = true hex.workspace = true +ignore.workspace = true +pathdiff.workspace = true html5ever.workspace = true indexmap.workspace = true mdbook-core.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..6bb8c39780 100644 --- a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs +++ b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs @@ -6,10 +6,12 @@ use crate::theme::Theme; use crate::utils::ToUrlPath; use anyhow::{Context, Result, bail}; use handlebars::Handlebars; +use ignore::gitignore::Gitignore; use mdbook_core::book::{Book, BookItem, Chapter}; use mdbook_core::config::{BookConfig, Config, HtmlConfig}; use mdbook_core::utils::fs; use mdbook_renderer::{RenderContext, Renderer}; +use pathdiff::diff_paths; use serde_json::json; use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; @@ -443,8 +445,56 @@ impl Renderer for HtmlHandlebars { self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect) .context("Unable to emit redirects")?; + let ignore = src_dir + .ancestors() + .map(|p| p.join(".gitignore")) + .find(|p| p.exists()) + .map(|gitignore_path| { + let (ignore, err) = Gitignore::new(&gitignore_path); + if let Some(err) = err { + warn!( + "error reading gitignore `{}`: {err}", + gitignore_path.display() + ); + } + // Note: The usage of `canonicalize` may encounter occasional + // failures on the Windows platform, presenting a potential risk. + // For more details, refer to [Pull Request + // #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981). + let ignore_path = ignore + .path() + .canonicalize() + .expect("ignore root canonicalize error"); + (ignore_path, ignore) + }); + // 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"])?; + // and also honor the .gitignore file found in the project if any + fs::copy_files_when(&src_dir, destination, true, &|path| { + if *path == build_dir { + return false; + } + + if let Some(ext) = path.extension() { + if ext.to_str().unwrap() == "md" { + return false; + } + } + + if let Some((ignore_path, ignore)) = &ignore { + let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); + let relative_path = diff_paths(&path, &ignore_path) + .expect("One of the paths should be an absolute"); + if ignore + .matched_path_or_any_parents(&relative_path, relative_path.is_dir()) + .is_ignore() + { + return false; + } + } + + true + })?; info!("HTML book written to `{}`", destination.display()); From 0df5d80c29de062876dcd46ec3d17c8c53ef903d Mon Sep 17 00:00:00 2001 From: Ben Falk Date: Fri, 19 Dec 2025 07:52:39 -0500 Subject: [PATCH 2/2] fix for windows canonicalize error --- .../src/html_handlebars/hbs_renderer.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs index 6bb8c39780..5ae8cc4165 100644 --- a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs +++ b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs @@ -449,7 +449,7 @@ impl Renderer for HtmlHandlebars { .ancestors() .map(|p| p.join(".gitignore")) .find(|p| p.exists()) - .map(|gitignore_path| { + .and_then(|gitignore_path| { let (ignore, err) = Gitignore::new(&gitignore_path); if let Some(err) = err { warn!( @@ -457,15 +457,14 @@ impl Renderer for HtmlHandlebars { gitignore_path.display() ); } - // Note: The usage of `canonicalize` may encounter occasional - // failures on the Windows platform, presenting a potential risk. - // For more details, refer to [Pull Request - // #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981). - let ignore_path = ignore - .path() - .canonicalize() - .expect("ignore root canonicalize error"); - (ignore_path, ignore) + + match ignore.path().canonicalize() { + Err(error) => { + warn!("unable to canonicalize path for ignore: {error:?}"); + None + } + Ok(path) => Some((path, ignore)), + } }); // Copy all remaining files, avoid a recursive copy from/to the book build dir