diff --git a/NEWS.md b/NEWS.md index e5087ea93..77642d3b5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # pkgdown (development version) +* `build_home()` now supports a `.pkgdownignore` file to exclude markdown files + from being rendered to HTML. The file can be placed in the package root, + `pkgdown/`, or `_pkgdown/` directories (@kyleGrealis, #2959). + # pkgdown 2.2.0 * Make `build_llm_docs()` more robust to the use of old Pandoc (@nanxstats, @galachad, #2952, #2954) diff --git a/R/build-home-md.R b/R/build-home-md.R index ae156b838..507039589 100644 --- a/R/build-home-md.R +++ b/R/build-home-md.R @@ -16,6 +16,14 @@ package_mds <- function(path, in_dev = FALSE) { # Remove files handled elsewhere handled <- c("README.md", "LICENSE.md", "LICENCE.md", "NEWS.md") + + # Append user-defined ignores from .pkgdownignore + user_ignores <- read_pkgdownignore(path) + if (length(user_ignores) > 0) { + cli::cli_inform("Ignoring: {user_ignores}") + } + handled <- c(handled, user_ignores) + # handled <- c(handled) mds <- mds[!path_file(mds) %in% handled] # Do not build 404 page if in-dev @@ -59,3 +67,35 @@ render_md <- function(pkg, filename) { invisible() } + +#' Read .pkgdownignore file +#' +#' Searches for .pkgdownignore in standard pkgdown config locations. +#' Returns filenames to exclude (one per line, with comments ignored). +#' +#' @param path Path to the package root +#' @return Character vector of filenames to exclude +#' @noRd +read_pkgdownignore <- function(path) { + # Check standard pkgdown config locations: + candidates <- c( + path(path, ".pkgdownignore"), + path(path, "_pkgdown", ".pkgdownignore"), + path(path, "pkgdown", ".pkgdownignore") + ) + + # Read output from all ignore files + ignore_paths <- candidates[file_exists(candidates)] + if (length(ignore_paths) == 0) { + return(character()) + } + + # Combine lines from all ignore files + lines <- unlist(lapply(ignore_paths, readLines)) + + # Remove comments & empty lines and trim whitespace + lines <- lines[!grepl("^\\s*#", lines)] + lines <- lines[nzchar(trimws(lines))] + + unique(trimws(lines)) +} diff --git a/tests/testthat/test-pkgdownignore.R b/tests/testthat/test-pkgdownignore.R new file mode 100644 index 000000000..3a7b14a4b --- /dev/null +++ b/tests/testthat/test-pkgdownignore.R @@ -0,0 +1,162 @@ +test_that("read_pkgdownignore returns empty when no file exists", { + pkg <- local_pkgdown_site() + expect_equal(read_pkgdownignore(pkg$src_path), character()) +}) + +test_that("read_pkgdownignore reads from package root", { + pkg <- local_pkgdown_site() + write_lines(c("CLAUDE.md", "AGENTS.md"), path(pkg$src_path, ".pkgdownignore")) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, c("CLAUDE.md", "AGENTS.md")) +}) + +test_that("read_pkgdownignore reads from pkgdown/ directory", { + pkg <- local_pkgdown_site() + dir_create(path(pkg$src_path, "pkgdown")) + write_lines("INTERNAL.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, "INTERNAL.md") +}) + +test_that("read_pkgdownignore reads from _pkgdown/ directory", { + pkg <- local_pkgdown_site() + dir_create(path(pkg$src_path, "_pkgdown")) + write_lines("NOTES.md", path(pkg$src_path, "_pkgdown", ".pkgdownignore")) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, "NOTES.md") +}) + +test_that("read_pkgdownignore combines files from multiple locations", { + pkg <- local_pkgdown_site() + + # Root + + write_lines("CLAUDE.md", path(pkg$src_path, ".pkgdownignore")) + + # pkgdown/ + dir_create(path(pkg$src_path, "pkgdown")) + write_lines("AGENTS.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) + + result <- read_pkgdownignore(pkg$src_path) + expect_setequal(result, c("CLAUDE.md", "AGENTS.md")) +}) + +test_that("read_pkgdownignore ignores comments and empty lines", { + pkg <- local_pkgdown_site() + write_lines( + c( + "# This is a comment", + "CLAUDE.md", + "", + " # Indented comment", + "AGENTS.md", + " ", + "INTERNAL.md" + ), + path(pkg$src_path, ".pkgdownignore") + ) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, c("CLAUDE.md", "AGENTS.md", "INTERNAL.md")) +}) + +test_that("read_pkgdownignore trims whitespace", { + pkg <- local_pkgdown_site() + write_lines( + c(" CLAUDE.md ", "\tAGENTS.md\t"), + path(pkg$src_path, ".pkgdownignore") + ) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, c("CLAUDE.md", "AGENTS.md")) +}) + +test_that("read_pkgdownignore deduplicates entries", { + pkg <- local_pkgdown_site() + + # Same file in root and pkgdown/ + + write_lines("CLAUDE.md", path(pkg$src_path, ".pkgdownignore")) + dir_create(path(pkg$src_path, "pkgdown")) + write_lines("CLAUDE.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, "CLAUDE.md") +}) + +test_that("package_mds excludes files listed in .pkgdownignore", { + pkg <- local_pkgdown_site() + + # Create test markdown files + write_lines("# CLAUDE", path(pkg$src_path, "CLAUDE.md")) + write_lines("# AGENTS", path(pkg$src_path, "AGENTS.md")) + write_lines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) + + # Create .pkgdownignore + + write_lines(c("CLAUDE.md", "AGENTS.md"), path(pkg$src_path, ".pkgdownignore")) + + result <- package_mds(pkg$src_path) + result_files <- path_file(result) + + expect_false("CLAUDE.md" %in% result_files) + expect_false("AGENTS.md" %in% result_files) + expect_true("ROADMAP.md" %in% result_files) +}) + +test_that("package_mds excludes .github files listed in .pkgdownignore", { + pkg <- local_pkgdown_site() + + # Create .github directory with markdown files + dir_create(path(pkg$src_path, ".github")) + write_lines("# Internal", path(pkg$src_path, ".github", "INTERNAL.md")) + write_lines( + "# Contributing", + path(pkg$src_path, ".github", "CONTRIBUTING.md") + ) + + # Ignore the internal one + write_lines("INTERNAL.md", path(pkg$src_path, ".pkgdownignore")) + + result <- package_mds(pkg$src_path) + result_files <- path_file(result) + + expect_false("INTERNAL.md" %in% result_files) + expect_true("CONTRIBUTING.md" %in% result_files) +}) + +test_that("package_mds works when .pkgdownignore is empty", { + pkg <- local_pkgdown_site() + + # Create test file + write_lines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) + + # Empty ignore file + file_create(path(pkg$src_path, ".pkgdownignore")) + + result <- package_mds(pkg$src_path) + result_files <- path_file(result) + + expect_true("ROADMAP.md" %in% result_files) +}) + +test_that("package_mds works when .pkgdownignore has only comments", { + pkg <- local_pkgdown_site() + + # Create test file + write_lines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) + + # Ignore file with only comments + write_lines( + c("# comment 1", "# comment 2"), + path(pkg$src_path, ".pkgdownignore") + ) + + result <- package_mds(pkg$src_path) + result_files <- path_file(result) + + expect_true("ROADMAP.md" %in% result_files) +})