From e220fc44fb3c96ba3e7c0bfa8098cf890492d8c1 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 24 May 2024 10:18:47 -0500 Subject: [PATCH] Create `build-article.R` (#2581) * Rename rmarkdown.R to build-article.R * Move build_article() to new file * Move build_article specific tests * Simplify redirect test since it's mostly covered elsewhere --- DESCRIPTION | 2 +- R/build-article.R | 311 ++++++++++++++++++ R/build-articles.R | 156 +-------- R/package.R | 4 +- R/rmarkdown.R | 159 --------- man/build_articles.Rd | 2 +- .../_snaps/{rmarkdown.md => build-article.md} | 56 ++++ tests/testthat/_snaps/build-articles.md | 56 ---- tests/testthat/test-build-article.R | 285 ++++++++++++++++ tests/testthat/test-build-articles.R | 246 +------------- tests/testthat/test-rmarkdown.R | 61 ---- 11 files changed, 663 insertions(+), 675 deletions(-) create mode 100644 R/build-article.R delete mode 100644 R/rmarkdown.R rename tests/testthat/_snaps/{rmarkdown.md => build-article.md} (51%) create mode 100644 tests/testthat/test-build-article.R delete mode 100644 tests/testthat/test-rmarkdown.R diff --git a/DESCRIPTION b/DESCRIPTION index 72aa52cf9..5f1fff73e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -67,7 +67,7 @@ Config/Needs/website: usethis, servr Config/potools/style: explicit Config/testthat/edition: 3 Config/testthat/parallel: true -Config/testthat/start-first: build-articles, build-reference +Config/testthat/start-first: build-article, build-reference Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.1 diff --git a/R/build-article.R b/R/build-article.R new file mode 100644 index 000000000..246052727 --- /dev/null +++ b/R/build-article.R @@ -0,0 +1,311 @@ +#' @order 2 +#' @export +#' @rdname build_articles +#' @param name Name of article to render. This should be either a path +#' relative to `vignettes/` without extension, or `index` or `README`. +#' @param data Additional data to pass on to template. +#' @param new_process Build the article in a clean R process? The default, +#' `TRUE`, ensures that every article is build in a fresh environment, but +#' you may want to set it to `FALSE` to make debugging easier. +build_article <- function(name, + pkg = ".", + data = list(), + lazy = FALSE, + seed = 1014L, + new_process = TRUE, + quiet = TRUE) { + + pkg <- as_pkgdown(pkg) + + # Look up in pkg vignette data - this allows convenient automatic + # specification of depth, output destination, and other parameters that + # allow code sharing with building of the index. + vig <- match(name, pkg$vignettes$name) + if (is.na(vig)) { + cli::cli_abort( + "Can't find article {.file {name}}" + ) + } + + input <- pkg$vignettes$file_in[vig] + output_file <- pkg$vignettes$file_out[vig] + depth <- pkg$vignettes$depth[vig] + + input_path <- path_abs(input, pkg$src_path) + output_path <- path_abs(output_file, pkg$dst_path) + + if (lazy && !out_of_date(input_path, output_path)) { + return(invisible()) + } + + local_envvar_pkgdown(pkg) + local_options_link(pkg, depth = depth) + + front <- rmarkdown::yaml_front_matter(input_path) + # Take opengraph from article's yaml front matter + front_opengraph <- check_open_graph(front$opengraph, input) + data$opengraph <- modify_list(data$opengraph, front_opengraph) + + # Allow users to opt-in to their own template + ext <- purrr::pluck(front, "pkgdown", "extension", .default = "html") + as_is <- isTRUE(purrr::pluck(front, "pkgdown", "as_is")) + + default_data <- list( + pagetitle = escape_html(front$title), + toc = toc <- front$toc %||% TRUE, + opengraph = list(description = front$description %||% pkg$package), + source = repo_source(pkg, input), + filename = path_file(input), + output_file = output_file, + as_is = as_is + ) + data <- modify_list(default_data, data) + + if (as_is) { + format <- NULL + + if (identical(ext, "html")) { + data$as_is <- TRUE + template <- rmarkdown_template(pkg, "article", depth = depth, data = data) + output <- rmarkdown::default_output_format(input_path) + + # Override defaults & values supplied in metadata + options <- list( + template = template$path, + self_contained = FALSE + ) + if (output$name != "rmarkdown::html_vignette") { + # Force to NULL unless overridden by user + options$theme <- output$options$theme + } + } else { + options <- list() + } + } else { + format <- build_rmarkdown_format( + pkg = pkg, + name = "article", + depth = depth, + data = data, + toc = TRUE + ) + options <- NULL + } + + render_rmarkdown( + pkg, + input = input, + output = output_file, + output_format = format, + output_options = options, + seed = seed, + new_process = new_process, + quiet = quiet + ) +} + +build_rmarkdown_format <- function(pkg, + name, + depth = 1L, + data = list(), + toc = TRUE) { + + template <- rmarkdown_template(pkg, name, depth = depth, data = data) + + out <- rmarkdown::html_document( + toc = toc, + toc_depth = 2, + self_contained = FALSE, + theme = NULL, + template = template$path, + anchor_sections = FALSE, + extra_dependencies = bs_theme_deps_suppress() + ) + out$knitr$opts_chunk <- fig_opts_chunk(pkg$figures, out$knitr$opts_chunk) + + old_pre <- out$pre_knit + width <- config_pluck_number_whole(pkg, "code.width", default = 80) + + out$pre_knit <- function(...) { + options(width = width) + if (is.function(old_pre)) { + old_pre(...) + } + } + + attr(out, "__cleanup") <- template$cleanup + + out +} + +# Generates pandoc template format by rendering +# inst/template/article-vignette.html +# Output is a path + environment; when the environment is garbage collected +# the path will be deleted +rmarkdown_template <- function(pkg, name, data, depth) { + path <- tempfile(fileext = ".html") + render_page(pkg, name, data, path, depth = depth, quiet = TRUE) + + # Remove template file when format object is GC'd + e <- env() + reg.finalizer(e, function(e) file_delete(path)) + + list(path = path, cleanup = e) +} + +render_rmarkdown <- function(pkg, + input, + output, + ..., + seed = NULL, + copy_images = TRUE, + new_process = TRUE, + quiet = TRUE, + call = caller_env()) { + + input_path <- path_abs(input, pkg$src_path) + output_path <- path_abs(output, pkg$dst_path) + + if (!file_exists(input_path)) { + cli::cli_abort("Can't find {src_path(input)}.", call = call) + } + + cli::cli_inform("Reading {src_path(input)}") + digest <- file_digest(output_path) + + args <- list( + input = input_path, + output_file = path_file(output_path), + output_dir = path_dir(output_path), + intermediates_dir = tempdir(), + encoding = "UTF-8", + seed = seed, + ..., + quiet = quiet + ) + + withr::local_envvar( + callr::rcmd_safe_env(), + BSTINPUTS = bst_paths(input_path), + TEXINPUTS = tex_paths(input_path), + BIBINPUTS = bib_paths(input_path), + R_CLI_NUM_COLORS = 256 + ) + + if (new_process) { + path <- withCallingHandlers( + callr::r_safe(rmarkdown_render_with_seed, args = args, show = !quiet), + error = function(cnd) { + lines <- strsplit(gsub("^\r?\n", "", cnd$stderr), "\r?\n")[[1]] + lines <- escape_cli(lines) + cli::cli_abort( + c( + "!" = "Failed to render {.path {input}}.", + set_names(lines, "x") + ), + parent = cnd$parent %||% cnd, + trace = cnd$parent$trace, + call = call + ) + } + ) + } else { + path <- inject(rmarkdown_render_with_seed(!!!args)) + } + + is_html <- identical(path_ext(path)[[1]], "html") + if (is_html) { + update_html( + path, + tweak_rmarkdown_html, + input_path = path_dir(input_path), + pkg = pkg + ) + } + if (digest != file_digest(output_path)) { + writing_file(path_rel(output_path, pkg$dst_path), output) + } + + # Copy over images needed by the document + if (copy_images && is_html) { + ext_src <- rmarkdown::find_external_resources(input_path) + + # temporarily copy the rendered html into the input path directory and scan + # again for additional external resources that may be been included by R code + tempfile <- path(path_dir(input_path), "--find-assets.html") + withr::defer(try(file_delete(tempfile))) + file_copy(path, tempfile) + ext_post <- rmarkdown::find_external_resources(tempfile) + + ext <- rbind(ext_src, ext_post) + ext <- ext[!duplicated(ext$path), ] + + # copy web + explicit files beneath vignettes/ + is_child <- path_has_parent(ext$path, ".") + ext_path <- ext$path[(ext$web | ext$explicit) & is_child] + + src <- path(path_dir(input_path), ext_path) + dst <- path(path_dir(output_path), ext_path) + # Make sure destination paths exist before copying files there + dir_create(unique(path_dir(dst))) + file_copy(src, dst, overwrite = TRUE) + } + + if (is_html) { + check_missing_images(pkg, input_path, output) + } + + invisible(path) +} + +#' Escapes a cli msg +#' +#' Removes empty lines and escapes braces +#' @param msg A character vector with messages to be escaped +#' @noRd +escape_cli <- function(msg) { + msg <- msg[nchar(msg) >0] + msg <- gsub("{", "{{", msg, fixed = TRUE) + msg <- gsub("}", "}}", msg, fixed = TRUE) + msg +} + +rmarkdown_render_with_seed <- function(..., seed = NULL) { + if (!is.null(seed)) { + set.seed(seed) + if (requireNamespace("htmlwidgets", quietly = TRUE)) { + htmlwidgets::setWidgetIdSeed(seed) + } + } + + # Ensure paths from output are not made relative to input + # https://github.com/yihui/knitr/issues/2171 + options(knitr.graphics.rel_path = FALSE) + + rmarkdown::render(envir = globalenv(), ...) +} + +# adapted from tools::texi2dvi +bst_paths <- function(path) { + paths <- c( + Sys.getenv("BSTINPUTS"), + path_dir(path), + path(R.home("share"), "texmf", "bibtex", "bst") + ) + paste(paths, collapse = .Platform$path.sep) +} +tex_paths <- function(path) { + paths <- c( + Sys.getenv("TEXINPUTS"), + path_dir(path), + path(R.home("share"), "texmf", "tex", "latex") + ) + paste(paths, collapse = .Platform$path.sep) +} +bib_paths <- function(path) { + paths <- c( + Sys.getenv("BIBINPUTS"), + tex_paths(path) + ) + paste(paths, collapse = .Platform$path.sep) +} diff --git a/R/build-articles.R b/R/build-articles.R index cc9f4570b..ce0d1f590 100644 --- a/R/build-articles.R +++ b/R/build-articles.R @@ -191,6 +191,7 @@ #' @param preview If `TRUE`, or `is.na(preview) && interactive()`, will preview #' freshly generated section in browser. #' @export +#' @order 1 build_articles <- function(pkg = ".", quiet = TRUE, lazy = TRUE, @@ -221,164 +222,11 @@ build_articles <- function(pkg = ".", preview_site(pkg, "articles", preview = preview) } -#' @export -#' @rdname build_articles -#' @param name Name of article to render. This should be either a path -#' relative to `vignettes/` without extension, or `index` or `README`. -#' @param data Additional data to pass on to template. -#' @param new_process Build the article in a clean R process? The default, -#' `TRUE`, ensures that every article is build in a fresh environment, but -#' you may want to set it to `FALSE` to make debugging easier. -build_article <- function(name, - pkg = ".", - data = list(), - lazy = FALSE, - seed = 1014L, - new_process = TRUE, - quiet = TRUE) { - - pkg <- as_pkgdown(pkg) - - # Look up in pkg vignette data - this allows convenient automatic - # specification of depth, output destination, and other parameters that - # allow code sharing with building of the index. - vig <- match(name, pkg$vignettes$name) - if (is.na(vig)) { - cli::cli_abort( - "Can't find article {.file {name}}" - ) - } - - input <- pkg$vignettes$file_in[vig] - output_file <- pkg$vignettes$file_out[vig] - depth <- pkg$vignettes$depth[vig] - - input_path <- path_abs(input, pkg$src_path) - output_path <- path_abs(output_file, pkg$dst_path) - - if (lazy && !out_of_date(input_path, output_path)) { - return(invisible()) - } - - local_envvar_pkgdown(pkg) - local_options_link(pkg, depth = depth) - - front <- rmarkdown::yaml_front_matter(input_path) - # Take opengraph from article's yaml front matter - front_opengraph <- check_open_graph(front$opengraph, input) - data$opengraph <- modify_list(data$opengraph, front_opengraph) - - # Allow users to opt-in to their own template - ext <- purrr::pluck(front, "pkgdown", "extension", .default = "html") - as_is <- isTRUE(purrr::pluck(front, "pkgdown", "as_is")) - - default_data <- list( - pagetitle = escape_html(front$title), - toc = toc <- front$toc %||% TRUE, - opengraph = list(description = front$description %||% pkg$package), - source = repo_source(pkg, input), - filename = path_file(input), - output_file = output_file, - as_is = as_is - ) - data <- modify_list(default_data, data) - - if (as_is) { - format <- NULL - - if (identical(ext, "html")) { - data$as_is <- TRUE - template <- rmarkdown_template(pkg, "article", depth = depth, data = data) - output <- rmarkdown::default_output_format(input_path) - - # Override defaults & values supplied in metadata - options <- list( - template = template$path, - self_contained = FALSE - ) - if (output$name != "rmarkdown::html_vignette") { - # Force to NULL unless overridden by user - options$theme <- output$options$theme - } - } else { - options <- list() - } - } else { - format <- build_rmarkdown_format( - pkg = pkg, - name = "article", - depth = depth, - data = data, - toc = TRUE - ) - options <- NULL - } - - render_rmarkdown( - pkg, - input = input, - output = output_file, - output_format = format, - output_options = options, - seed = seed, - new_process = new_process, - quiet = quiet - ) -} - -build_rmarkdown_format <- function(pkg, - name, - depth = 1L, - data = list(), - toc = TRUE) { - - template <- rmarkdown_template(pkg, name, depth = depth, data = data) - - out <- rmarkdown::html_document( - toc = toc, - toc_depth = 2, - self_contained = FALSE, - theme = NULL, - template = template$path, - anchor_sections = FALSE, - extra_dependencies = bs_theme_deps_suppress() - ) - out$knitr$opts_chunk <- fig_opts_chunk(pkg$figures, out$knitr$opts_chunk) - - old_pre <- out$pre_knit - width <- config_pluck_number_whole(pkg, "code.width", default = 80) - - out$pre_knit <- function(...) { - options(width = width) - if (is.function(old_pre)) { - old_pre(...) - } - } - - attr(out, "__cleanup") <- template$cleanup - - out -} - -# Generates pandoc template format by rendering -# inst/template/article-vignette.html -# Output is a path + environment; when the environment is garbage collected -# the path will be deleted -rmarkdown_template <- function(pkg, name, data, depth) { - path <- tempfile(fileext = ".html") - render_page(pkg, name, data, path, depth = depth, quiet = TRUE) - - # Remove template file when format object is GC'd - e <- env() - reg.finalizer(e, function(e) file_delete(path)) - - list(path = path, cleanup = e) -} - # Articles index ---------------------------------------------------------- #' @export #' @rdname build_articles +#' @order 3 build_articles_index <- function(pkg = ".") { pkg <- as_pkgdown(pkg) diff --git a/R/package.R b/R/package.R index a097aff57..aba4d02a4 100644 --- a/R/package.R +++ b/R/package.R @@ -317,8 +317,8 @@ package_vignettes <- function(path = ".") { out <- tibble::tibble( name = as.character(path_ext_remove(vig_path)), - file_in = file_in, - file_out = file_out, + file_in = as.character(file_in), + file_out = as.character(file_out), title = title, description = desc, depth = dir_depth(file_out) diff --git a/R/rmarkdown.R b/R/rmarkdown.R deleted file mode 100644 index a68ced4cf..000000000 --- a/R/rmarkdown.R +++ /dev/null @@ -1,159 +0,0 @@ -#' Render RMarkdown document in a fresh session -#' -#' @noRd -render_rmarkdown <- function(pkg, - input, - output, - ..., - seed = NULL, - copy_images = TRUE, - new_process = TRUE, - quiet = TRUE, - call = caller_env()) { - - input_path <- path_abs(input, pkg$src_path) - output_path <- path_abs(output, pkg$dst_path) - - if (!file_exists(input_path)) { - cli::cli_abort("Can't find {src_path(input)}.", call = call) - } - - cli::cli_inform("Reading {src_path(input)}") - digest <- file_digest(output_path) - - args <- list( - input = input_path, - output_file = path_file(output_path), - output_dir = path_dir(output_path), - intermediates_dir = tempdir(), - encoding = "UTF-8", - seed = seed, - ..., - quiet = quiet - ) - - withr::local_envvar( - callr::rcmd_safe_env(), - BSTINPUTS = bst_paths(input_path), - TEXINPUTS = tex_paths(input_path), - BIBINPUTS = bib_paths(input_path), - R_CLI_NUM_COLORS = 256 - ) - - if (new_process) { - path <- withCallingHandlers( - callr::r_safe(rmarkdown_render_with_seed, args = args, show = !quiet), - error = function(cnd) { - lines <- strsplit(gsub("^\r?\n", "", cnd$stderr), "\r?\n")[[1]] - lines <- escape_cli(lines) - cli::cli_abort( - c( - "!" = "Failed to render {.path {input}}.", - set_names(lines, "x") - ), - parent = cnd$parent %||% cnd, - trace = cnd$parent$trace, - call = call - ) - } - ) - } else { - path <- inject(rmarkdown_render_with_seed(!!!args)) - } - - is_html <- identical(path_ext(path)[[1]], "html") - if (is_html) { - update_html( - path, - tweak_rmarkdown_html, - input_path = path_dir(input_path), - pkg = pkg - ) - } - if (digest != file_digest(output_path)) { - writing_file(path_rel(output_path, pkg$dst_path), output) - } - - # Copy over images needed by the document - if (copy_images && is_html) { - ext_src <- rmarkdown::find_external_resources(input_path) - - # temporarily copy the rendered html into the input path directory and scan - # again for additional external resources that may be been included by R code - tempfile <- path(path_dir(input_path), "--find-assets.html") - withr::defer(try(file_delete(tempfile))) - file_copy(path, tempfile) - ext_post <- rmarkdown::find_external_resources(tempfile) - - ext <- rbind(ext_src, ext_post) - ext <- ext[!duplicated(ext$path), ] - - # copy web + explicit files beneath vignettes/ - is_child <- path_has_parent(ext$path, ".") - ext_path <- ext$path[(ext$web | ext$explicit) & is_child] - - src <- path(path_dir(input_path), ext_path) - dst <- path(path_dir(output_path), ext_path) - # Make sure destination paths exist before copying files there - dir_create(unique(path_dir(dst))) - file_copy(src, dst, overwrite = TRUE) - } - - if (is_html) { - check_missing_images(pkg, input_path, output) - } - - invisible(path) -} - -#' Escapes a cli msg -#' -#' Removes empty lines and escapes braces -#' @param msg A character vector with messages to be escaped -#' @noRd -escape_cli <- function(msg) { - msg <- msg[nchar(msg) >0] - msg <- gsub("{", "{{", msg, fixed = TRUE) - msg <- gsub("}", "}}", msg, fixed = TRUE) - msg -} - -rmarkdown_render_with_seed <- function(..., seed = NULL) { - if (!is.null(seed)) { - set.seed(seed) - if (requireNamespace("htmlwidgets", quietly = TRUE)) { - htmlwidgets::setWidgetIdSeed(seed) - } - } - - # Ensure paths from output are not made relative to input - # https://github.com/yihui/knitr/issues/2171 - options(knitr.graphics.rel_path = FALSE) - - rmarkdown::render(envir = globalenv(), ...) -} - -# adapted from tools::texi2dvi -bst_paths <- function(path) { - paths <- c( - Sys.getenv("BSTINPUTS"), - path_dir(path), - path(R.home("share"), "texmf", "bibtex", "bst") - ) - paste(paths, collapse = .Platform$path.sep) -} -tex_paths <- function(path) { - paths <- c( - Sys.getenv("TEXINPUTS"), - path_dir(path), - path(R.home("share"), "texmf", "tex", "latex") - ) - paste(paths, collapse = .Platform$path.sep) -} -bib_paths <- function(path) { - paths <- c( - Sys.getenv("BIBINPUTS"), - tex_paths(path) - ) - paste(paths, collapse = .Platform$path.sep) -} diff --git a/man/build_articles.Rd b/man/build_articles.Rd index 2e8dc9431..f50c58a38 100644 --- a/man/build_articles.Rd +++ b/man/build_articles.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/build-articles.R +% Please edit documentation in R/build-articles.R, R/build-article.R \name{build_articles} \alias{build_articles} \alias{build_article} diff --git a/tests/testthat/_snaps/rmarkdown.md b/tests/testthat/_snaps/build-article.md similarity index 51% rename from tests/testthat/_snaps/rmarkdown.md rename to tests/testthat/_snaps/build-article.md index a6cf86de1..0535c3f1d 100644 --- a/tests/testthat/_snaps/rmarkdown.md +++ b/tests/testthat/_snaps/build-article.md @@ -1,3 +1,59 @@ +# warns about missing images + + Code + build_articles(pkg) + Message + -- Building articles ----------------------------------------------------------- + Writing `articles/index.html` + Reading vignettes/html-vignette.Rmd + Writing `articles/html-vignette.html` + Missing images in 'vignettes/html-vignette.Rmd': 'kitten.jpg' + i pkgdown can only use images in 'man/figures' and 'vignettes' + +# warns about missing alt-text + + Code + build_article("missing-images", pkg) + Message + Reading vignettes/missing-images.Rmd + Writing `articles/missing-images.html` + x Missing alt-text in 'vignettes/missing-images.Rmd' + * kitten.jpg + * missing-images_files/figure-html/unnamed-chunk-1-1.png + i Learn more in `vignette(accessibility)`. + +# can build article that uses html_vignette + + Code + expect_error(build_article("html-vignette", pkg), NA) + Message + Reading vignettes/html-vignette.Rmd + Writing `articles/html-vignette.html` + +# bad width gives nice error + + Code + build_rmarkdown_format(pkg, "article") + Condition + Error in `build_rmarkdown_format()`: + ! code.width must be a whole number, not the string "abc". + i Edit _pkgdown.yml to fix the problem. + +# output is reproducible by default, i.e. 'seed' is respected + + Code + cat(output) + Output + ## [1] 0.080750138 0.834333037 0.600760886 0.157208442 0.007399441 + +# reports on bad open graph meta-data + + Code + build_article(pkg = pkg, name = "bad-opengraph") + Condition + Error in `build_article()`: + ! 'vignettes/bad-opengraph.Rmd': opengraph.twitter must be a list, not the number 1. + # render_rmarkdown copies image files in subdirectories Code diff --git a/tests/testthat/_snaps/build-articles.md b/tests/testthat/_snaps/build-articles.md index 1d6155327..5297ad34c 100644 --- a/tests/testthat/_snaps/build-articles.md +++ b/tests/testthat/_snaps/build-articles.md @@ -1,44 +1,3 @@ -# warns about missing images - - Code - build_articles(pkg) - Message - -- Building articles ----------------------------------------------------------- - Writing `articles/index.html` - Reading vignettes/html-vignette.Rmd - Writing `articles/html-vignette.html` - Missing images in 'vignettes/html-vignette.Rmd': 'kitten.jpg' - i pkgdown can only use images in 'man/figures' and 'vignettes' - -# warns about missing alt-text - - Code - build_article("missing-images", pkg) - Message - Reading vignettes/missing-images.Rmd - Writing `articles/missing-images.html` - x Missing alt-text in 'vignettes/missing-images.Rmd' - * kitten.jpg - * missing-images_files/figure-html/unnamed-chunk-1-1.png - i Learn more in `vignette(accessibility)`. - -# can build article that uses html_vignette - - Code - expect_error(build_article("html-vignette", pkg), NA) - Message - Reading vignettes/html-vignette.Rmd - Writing `articles/html-vignette.html` - -# bad width gives nice error - - Code - build_rmarkdown_format(pkg, "article") - Condition - Error in `build_rmarkdown_format()`: - ! code.width must be a whole number, not the string "abc". - i Edit _pkgdown.yml to fix the problem. - # validates articles yaml Code @@ -143,18 +102,3 @@ ! 1 vignette missing from index: "c". i Edit _pkgdown.yml to fix the problem. -# output is reproducible by default, i.e. 'seed' is respected - - Code - cat(output) - Output - ## [1] 0.080750138 0.834333037 0.600760886 0.157208442 0.007399441 - -# reports on bad open graph meta-data - - Code - build_article(pkg = pkg, name = "bad-opengraph") - Condition - Error in `build_article()`: - ! 'vignettes/bad-opengraph.Rmd': opengraph.twitter must be a list, not the number 1. - diff --git a/tests/testthat/test-build-article.R b/tests/testthat/test-build-article.R new file mode 100644 index 000000000..0694a75f2 --- /dev/null +++ b/tests/testthat/test-build-article.R @@ -0,0 +1,285 @@ +test_that("image links relative to output", { + # weird path differences that I don't have the energy to dig into + skip_on_cran() + pkg <- local_pkgdown_site(test_path("assets/articles-images")) + + suppressMessages(init_site(pkg)) + suppressMessages(copy_figures(pkg)) + suppressMessages(build_article("kitten", pkg)) + + html <- xml2::read_html(path(pkg$dst_path, "articles", "kitten.html")) + src <- xpath_attr(html, "//main//img", "src") + + expect_equal(src, c( + # knitr::include_graphics() + "../reference/figures/kitten.jpg", + "another-kitten.jpg", + # rmarkdown image + "../reference/figures/kitten.jpg", + "another-kitten.jpg", + # magick::image_read() + "kitten_files/figure-html/magick-1.png", + # figure + "kitten_files/figure-html/plot-1.jpg" + )) + + # And files aren't copied + expect_false(dir_exists(path(pkg$dst_path, "man"))) +}) + +test_that("warns about missing images", { + # Added in #2509: I can't figure out why this is necessary :( + skip_on_covr() + + pkg <- local_pkgdown_site(test_path("assets/bad-images")) + expect_snapshot(build_articles(pkg)) +}) + +test_that("warns about missing alt-text", { + pkg <- local_pkgdown_site(test_path("assets/missing-alt")) + expect_snapshot(build_article("missing-images", pkg)) +}) + +test_that("articles don't include header-attrs.js script", { + pkg <- local_pkgdown_site(test_path("assets/articles")) + suppressMessages(init_site(pkg)) + + suppressMessages(path <- build_article("standard", pkg)) + + html <- xml2::read_html(path) + js <- xpath_attr(html, ".//body//script", "src") + # included for pandoc 2.7.3 - 2.9.2.1 improve accessibility + js <- js[path_file(js) != "empty-anchor.js"] + expect_equal(js, character()) +}) + +test_that("can build article that uses html_vignette", { + pkg <- local_pkgdown_site(test_path("assets/articles")) + suppressMessages(init_site(pkg)) + + # theme is not set since html_vignette doesn't support it + expect_snapshot(expect_error(build_article("html-vignette", pkg), NA)) +}) + +test_that("can override html_document() options", { + pkg <- local_pkgdown_site(test_path("assets/articles")) + suppressMessages(init_site(pkg)) + suppressMessages(path <- build_article("html-document", pkg)) + + # Check that number_sections is respected + html <- xml2::read_html(path) + expect_equal(xpath_text(html, ".//h2//span"), c("1", "2")) + + # But title isn't affected + expect_equal(xpath_text(html, ".//h1"), "html_document + as_is") + + # And no links or scripts are inlined + expect_equal(xpath_length(html, ".//body//link"), 0) + expect_equal(xpath_length(html, ".//body//script"), 0) +}) + +test_that("html widgets get needed css/js", { + pkg <- local_pkgdown_site(test_path("assets/articles")) + suppressMessages(init_site(pkg)) + suppressMessages(path <- build_article("widget", pkg)) + + html <- xml2::read_html(path) + css <- xpath_attr(html, ".//body//link", "href") + js <- xpath_attr(html, ".//body//script", "src") + + expect_true("diffviewer.css" %in% path_file(css)) + expect_true("diffviewer.js" %in% path_file(js)) +}) + +test_that("can override options with _output.yml", { + pkg <- local_pkgdown_site(test_path("assets/articles")) + suppressMessages(init_site(pkg)) + suppressMessages(path <- build_article("html-document", pkg)) + + # Check that number_sections is respected + html <- xml2::read_html(path) + expect_equal(xpath_text(html, ".//h2//span"), c("1", "2")) +}) + +test_that("can set width", { + pkg <- local_pkgdown_site(test_path("assets/articles"), " + code: + width: 50 + ") + suppressMessages(init_site(pkg)) + + suppressMessages(path <- build_article("width", pkg)) + html <- xml2::read_html(path) + expect_equal(xpath_text(html, ".//pre")[[2]], "## [1] 50") +}) + +test_that("bad width gives nice error", { + pkg <- local_pkgdown_site(meta = list(code = list(width = "abc"))) + expect_snapshot(build_rmarkdown_format(pkg, "article"), error = TRUE) +}) + +test_that("finds external resources referenced by R code in the article html", { + # weird path differences that I don't have the energy to dig into + skip_on_cran() + pkg <- local_pkgdown_site(test_path("assets", "articles-resources")) + + suppressMessages(path <- build_article("resources", pkg)) + + # ensure that we the HTML references `` directly + expect_equal( + xpath_attr(xml2::read_html(path), ".//img", "src"), + "external.png" + ) + + # expect that `external.png` was copied to the rendered article directory + expect_true( + file_exists(path(path_dir(path), "external.png")) + ) +}) + +test_that("BS5 article laid out correctly with and without TOC", { + pkg <- local_pkgdown_site(test_path("assets/articles"), " + template: + bootstrap: 5 + ") + + suppressMessages(init_site(pkg)) + suppressMessages(toc_true_path <- build_article("standard", pkg)) + suppressMessages(toc_false_path <- build_article("toc-false", pkg)) + + toc_true <- xml2::read_html(toc_true_path) + toc_false <- xml2::read_html(toc_false_path) + + # Always has class col-md-9 + expect_equal(xpath_attr(toc_false, ".//main", "class"), "col-md-9") + expect_equal(xpath_attr(toc_true, ".//main", "class"), "col-md-9") + + # The no sidebar without toc + expect_equal(xpath_length(toc_true, ".//aside"), 1) + expect_equal(xpath_length(toc_false, ".//aside"), 0) +}) + +test_that("pkgdown deps are included only once in articles", { + pkg <- local_pkgdown_site(test_path("assets/articles"), " + template: + bootstrap: 5 + ") + + suppressMessages(init_site(pkg)) + suppressMessages(path <- build_article("html-deps", pkg)) + + html <- xml2::read_html(path) + + # jquery is only loaded once, even though it's also added by code in the article + expect_equal(xpath_length(html, ".//script[(@src and contains(@src, '/jquery'))]"), 1) + + # same for bootstrap js and css + str_subset_bootstrap <- function(x) { + bs_rgx <- "bootstrap-[\\d.]+" # ex: bootstrap-5.1.0 not bootstrap-toc, + grep(bs_rgx, x, value = TRUE, perl = TRUE) + } + bs_js_src <- str_subset_bootstrap( + xpath_attr(html, ".//script[(@src and contains(@src, '/bootstrap'))]", "src") + ) + expect_length(bs_js_src, 1) + + bs_css_href <- str_subset_bootstrap( + xpath_attr(html, ".//link[(@href and contains(@href, '/bootstrap'))]", "href") + ) + expect_length(bs_css_href, 1) +}) + + +test_that("titles are escaped when needed", { + pkg <- local_pkgdown_site(test_path("assets/articles")) + suppressMessages(init_site(pkg)) + suppressMessages(build_article(pkg = pkg, name = "needs-escape")) + + html <- xml2::read_html(path(pkg$dst_path, "articles/needs-escape.html")) + expect_equal(xpath_text(html, "//title", trim = TRUE), "a <-> b • testpackage") + expect_equal(xpath_text(html, "//h1", trim = TRUE), "a <-> b") +}) + +test_that("output is reproducible by default, i.e. 'seed' is respected", { + pkg <- local_pkgdown_site(test_path("assets/articles")) + suppressMessages(init_site(pkg)) + suppressMessages(build_article(pkg = pkg, name = "random")) + + output <- xml2::read_html(path(pkg$dst_path, "articles/random.html")) %>% + rvest::html_node("main > pre") %>% + rvest::html_text() %>% + # replace line feeds with whitespace to make output platform independent + gsub("\r", "", .) + + expect_snapshot(cat(output)) +}) + +test_that("reports on bad open graph meta-data", { + pkg <- local_pkgdown_site(test_path("assets/articles")) + suppressMessages(init_site(pkg)) + expect_snapshot(build_article(pkg = pkg, name = "bad-opengraph"), error = TRUE) +}) + +# render_markdown -------------------------------------------------------------- + +test_that("render_rmarkdown copies image files in subdirectories", { + skip_if_no_pandoc() + tmp <- dir_create(file_temp()) + pkg <- list(src_path = test_path("."), dst_path = tmp, bs_version = 3) + + expect_snapshot( + render_rmarkdown(pkg, "assets/vignette-with-img.Rmd", "test.html") + ) + expect_equal( + as.character(path_rel(dir_ls(tmp, type = "file", recurse = TRUE), tmp)), + c("open-graph/logo.png", "test.html") + ) +}) + +test_that("render_rmarkdown yields useful error if pandoc fails", { + skip_on_cran() # fragile due to pandoc dependency + skip_if_no_pandoc("2.18") + + tmp <- dir_create(file_temp()) + pkg <- list(src_path = test_path("."), dst_path = tmp, bs_version = 3) + + format <- rmarkdown::html_document(pandoc_args = "--fail-if-warnings") + expect_snapshot( + render_rmarkdown(pkg, "assets/pandoc-fail.Rmd", "test.html", output_format = format), + error = TRUE + ) +}) + +test_that("render_rmarkdown yields useful error if R fails", { + skip_if_no_pandoc() + + tmp <- dir_create(file_temp()) + pkg <- list(src_path = test_path("."), dst_path = tmp, bs_version = 3) + + expect_snapshot( + { + "Test traceback" + summary(expect_error(render_rmarkdown(pkg, "assets/r-fail.Rmd", "test.html"))) + + "Just test that it works; needed for browser() etc" + expect_error(render_rmarkdown(pkg, "assets/r-fail.Rmd", "test.html", new_process = FALSE)) + }, + # work around xfun bug + transform = function(x) gsub("lines ?at lines", "lines", x) + ) +}) + +test_that("render_rmarkdown styles ANSI escapes", { + skip_if_no_pandoc() + tmp <- dir_create(file_temp()) + pkg <- list(src_path = test_path("."), dst_path = tmp, bs_version = 5) + + expect_snapshot({ + path <- render_rmarkdown(pkg, + input = "assets/vignette-with-crayon.Rmd", + output = "test.html" + ) + }) + html <- xml2::read_html(path) + expect_snapshot_output(xpath_xml(html, ".//code//span[@class='co']")) +}) diff --git a/tests/testthat/test-build-articles.R b/tests/testthat/test-build-articles.R index 2c20935d4..aeec8a05c 100644 --- a/tests/testthat/test-build-articles.R +++ b/tests/testthat/test-build-articles.R @@ -5,126 +5,6 @@ test_that("can recognise intro variants", { expect_true(article_is_intro("articles/pack-age", "pack.age")) }) -test_that("image links relative to output", { - # weird path differences that I don't have the energy to dig into - skip_on_cran() - pkg <- local_pkgdown_site(test_path("assets/articles-images")) - - suppressMessages(init_site(pkg)) - suppressMessages(copy_figures(pkg)) - suppressMessages(build_article("kitten", pkg)) - - html <- xml2::read_html(path(pkg$dst_path, "articles", "kitten.html")) - src <- xpath_attr(html, "//main//img", "src") - - expect_equal(src, c( - # knitr::include_graphics() - "../reference/figures/kitten.jpg", - "another-kitten.jpg", - # rmarkdown image - "../reference/figures/kitten.jpg", - "another-kitten.jpg", - # magick::image_read() - "kitten_files/figure-html/magick-1.png", - # figure - "kitten_files/figure-html/plot-1.jpg" - )) - - # And files aren't copied - expect_false(dir_exists(path(pkg$dst_path, "man"))) -}) - -test_that("warns about missing images", { - # Added in #2509: I can't figure out why this is necessary :( - skip_on_covr() - - pkg <- local_pkgdown_site(test_path("assets/bad-images")) - expect_snapshot(build_articles(pkg)) -}) - -test_that("warns about missing alt-text", { - pkg <- local_pkgdown_site(test_path("assets/missing-alt")) - expect_snapshot(build_article("missing-images", pkg)) -}) - -test_that("articles don't include header-attrs.js script", { - pkg <- local_pkgdown_site(test_path("assets/articles")) - suppressMessages(init_site(pkg)) - - suppressMessages(path <- build_article("standard", pkg)) - - html <- xml2::read_html(path) - js <- xpath_attr(html, ".//body//script", "src") - # included for pandoc 2.7.3 - 2.9.2.1 improve accessibility - js <- js[path_file(js) != "empty-anchor.js"] - expect_equal(js, character()) -}) - -test_that("can build article that uses html_vignette", { - pkg <- local_pkgdown_site(test_path("assets/articles")) - suppressMessages(init_site(pkg)) - - # theme is not set since html_vignette doesn't support it - expect_snapshot(expect_error(build_article("html-vignette", pkg), NA)) -}) - -test_that("can override html_document() options", { - pkg <- local_pkgdown_site(test_path("assets/articles")) - suppressMessages(init_site(pkg)) - suppressMessages(path <- build_article("html-document", pkg)) - - # Check that number_sections is respected - html <- xml2::read_html(path) - expect_equal(xpath_text(html, ".//h2//span"), c("1", "2")) - - # But title isn't affected - expect_equal(xpath_text(html, ".//h1"), "html_document + as_is") - - # And no links or scripts are inlined - expect_equal(xpath_length(html, ".//body//link"), 0) - expect_equal(xpath_length(html, ".//body//script"), 0) -}) - -test_that("html widgets get needed css/js", { - pkg <- local_pkgdown_site(test_path("assets/articles")) - suppressMessages(init_site(pkg)) - suppressMessages(path <- build_article("widget", pkg)) - - html <- xml2::read_html(path) - css <- xpath_attr(html, ".//body//link", "href") - js <- xpath_attr(html, ".//body//script", "src") - - expect_true("diffviewer.css" %in% path_file(css)) - expect_true("diffviewer.js" %in% path_file(js)) -}) - -test_that("can override options with _output.yml", { - pkg <- local_pkgdown_site(test_path("assets/articles")) - suppressMessages(init_site(pkg)) - suppressMessages(path <- build_article("html-document", pkg)) - - # Check that number_sections is respected - html <- xml2::read_html(path) - expect_equal(xpath_text(html, ".//h2//span"), c("1", "2")) -}) - -test_that("can set width", { - pkg <- local_pkgdown_site(test_path("assets/articles"), " - code: - width: 50 - ") - suppressMessages(init_site(pkg)) - - suppressMessages(path <- build_article("width", pkg)) - html <- xml2::read_html(path) - expect_equal(xpath_text(html, ".//pre")[[2]], "## [1] 50") -}) - -test_that("bad width gives nice error", { - pkg <- local_pkgdown_site(meta = list(code = list(width = "abc"))) - expect_snapshot(build_rmarkdown_format(pkg, "article"), error = TRUE) -}) - test_that("validates articles yaml", { data_articles_index_ <- function(x) { pkg <- local_pkgdown_site(meta = list(articles = x)) @@ -157,47 +37,6 @@ test_that("validates external-articles", { }) }) -test_that("finds external resources referenced by R code in the article html", { - # weird path differences that I don't have the energy to dig into - skip_on_cran() - pkg <- local_pkgdown_site(test_path("assets", "articles-resources")) - - suppressMessages(path <- build_article("resources", pkg)) - - # ensure that we the HTML references `` directly - expect_equal( - xpath_attr(xml2::read_html(path), ".//img", "src"), - "external.png" - ) - - # expect that `external.png` was copied to the rendered article directory - expect_true( - file_exists(path(path_dir(path), "external.png")) - ) -}) - -test_that("BS5 article laid out correctly with and without TOC", { - pkg <- local_pkgdown_site(test_path("assets/articles"), " - template: - bootstrap: 5 - ") - - suppressMessages(init_site(pkg)) - suppressMessages(toc_true_path <- build_article("standard", pkg)) - suppressMessages(toc_false_path <- build_article("toc-false", pkg)) - - toc_true <- xml2::read_html(toc_true_path) - toc_false <- xml2::read_html(toc_false_path) - - # Always has class col-md-9 - expect_equal(xpath_attr(toc_false, ".//main", "class"), "col-md-9") - expect_equal(xpath_attr(toc_true, ".//main", "class"), "col-md-9") - - # The no sidebar without toc - expect_equal(xpath_length(toc_true, ".//aside"), 1) - expect_equal(xpath_length(toc_false, ".//aside"), 0) -}) - test_that("data_articles includes external articles", { pkg <- local_pkgdown_site() dir_create(path(pkg$src_path, "vignettes")) @@ -218,60 +57,16 @@ test_that("articles in vignettes/articles/ are unnested into articles/", { # weird path differences that I don't have the energy to dig into skip_on_cran() - pkg <- local_pkgdown_site(test_path("assets/articles")) + pkg <- local_pkgdown_site(test_path("assets/articles"), meta = list( + url = "https://example.com" + )) suppressMessages(init_site(pkg)) - suppressMessages(path <- build_article("articles/nested", pkg)) - expect_equal( - path_real(path), - path_real(path(pkg$dst_path, "articles", "nested.html")) - ) + nested <- pkg$vignettes[pkg$vignettes$name == "articles/nested", ] + expect_equal(nested$file_out, "articles/nested.html") # Check automatic redirect from articles/articles/foo.html -> articles/foo.html - pkg$meta$url <- "https://example.com" expect_snapshot(build_redirects(pkg)) - - # Check that the redirect file exists in /articles/articles/ - redirect_path <- path(pkg$dst_path, "articles", "articles", "nested.html") - expect_true(file_exists(redirect_path)) - - # Check that we redirect to correct location - html <- xml2::read_html(redirect_path) - expect_match( - xpath_attr(html, ".//meta[@http-equiv = 'refresh']", "content"), - "https://example.com/articles/nested.html", - fixed = TRUE - ) -}) - -test_that("pkgdown deps are included only once in articles", { - pkg <- local_pkgdown_site(test_path("assets/articles"), " - template: - bootstrap: 5 - ") - - suppressMessages(init_site(pkg)) - suppressMessages(path <- build_article("html-deps", pkg)) - - html <- xml2::read_html(path) - - # jquery is only loaded once, even though it's also added by code in the article - expect_equal(xpath_length(html, ".//script[(@src and contains(@src, '/jquery'))]"), 1) - - # same for bootstrap js and css - str_subset_bootstrap <- function(x) { - bs_rgx <- "bootstrap-[\\d.]+" # ex: bootstrap-5.1.0 not bootstrap-toc, - grep(bs_rgx, x, value = TRUE, perl = TRUE) - } - bs_js_src <- str_subset_bootstrap( - xpath_attr(html, ".//script[(@src and contains(@src, '/bootstrap'))]", "src") - ) - expect_length(bs_js_src, 1) - - bs_css_href <- str_subset_bootstrap( - xpath_attr(html, ".//link[(@href and contains(@href, '/bootstrap'))]", "href") - ) - expect_length(bs_css_href, 1) }) test_that("warns about articles missing from index", { @@ -329,34 +124,3 @@ test_that("check doesn't include getting started vignette", { expect_error(data_articles_index(pkg), NA) }) - -test_that("titles are escaped when needed", { - pkg <- local_pkgdown_site(test_path("assets/articles")) - suppressMessages(init_site(pkg)) - suppressMessages(build_article(pkg = pkg, name = "needs-escape")) - - html <- xml2::read_html(path(pkg$dst_path, "articles/needs-escape.html")) - expect_equal(xpath_text(html, "//title", trim = TRUE), "a <-> b • testpackage") - expect_equal(xpath_text(html, "//h1", trim = TRUE), "a <-> b") -}) - - -test_that("output is reproducible by default, i.e. 'seed' is respected", { - pkg <- local_pkgdown_site(test_path("assets/articles")) - suppressMessages(init_site(pkg)) - suppressMessages(build_article(pkg = pkg, name = "random")) - - output <- xml2::read_html(path(pkg$dst_path, "articles/random.html")) %>% - rvest::html_node("main > pre") %>% - rvest::html_text() %>% - # replace line feeds with whitespace to make output platform independent - gsub("\r", "", .) - - expect_snapshot(cat(output)) -}) - -test_that("reports on bad open graph meta-data", { - pkg <- local_pkgdown_site(test_path("assets/articles")) - suppressMessages(init_site(pkg)) - expect_snapshot(build_article(pkg = pkg, name = "bad-opengraph"), error = TRUE) -}) diff --git a/tests/testthat/test-rmarkdown.R b/tests/testthat/test-rmarkdown.R deleted file mode 100644 index 48853e88a..000000000 --- a/tests/testthat/test-rmarkdown.R +++ /dev/null @@ -1,61 +0,0 @@ -test_that("render_rmarkdown copies image files in subdirectories", { - skip_if_no_pandoc() - tmp <- dir_create(file_temp()) - pkg <- list(src_path = test_path("."), dst_path = tmp, bs_version = 3) - - expect_snapshot( - render_rmarkdown(pkg, "assets/vignette-with-img.Rmd", "test.html") - ) - expect_equal( - as.character(path_rel(dir_ls(tmp, type = "file", recurse = TRUE), tmp)), - c("open-graph/logo.png", "test.html") - ) -}) - -test_that("render_rmarkdown yields useful error if pandoc fails", { - skip_on_cran() # fragile due to pandoc dependency - skip_if_no_pandoc("2.18") - - tmp <- dir_create(file_temp()) - pkg <- list(src_path = test_path("."), dst_path = tmp, bs_version = 3) - - format <- rmarkdown::html_document(pandoc_args = "--fail-if-warnings") - expect_snapshot( - render_rmarkdown(pkg, "assets/pandoc-fail.Rmd", "test.html", output_format = format), - error = TRUE - ) -}) - -test_that("render_rmarkdown yields useful error if R fails", { - skip_if_no_pandoc() - - tmp <- dir_create(file_temp()) - pkg <- list(src_path = test_path("."), dst_path = tmp, bs_version = 3) - - expect_snapshot( - { - "Test traceback" - summary(expect_error(render_rmarkdown(pkg, "assets/r-fail.Rmd", "test.html"))) - - "Just test that it works; needed for browser() etc" - expect_error(render_rmarkdown(pkg, "assets/r-fail.Rmd", "test.html", new_process = FALSE)) - }, - # work around xfun bug - transform = function(x) gsub("lines ?at lines", "lines", x) - ) -}) - -test_that("render_rmarkdown styles ANSI escapes", { - skip_if_no_pandoc() - tmp <- dir_create(file_temp()) - pkg <- list(src_path = test_path("."), dst_path = tmp, bs_version = 5) - - expect_snapshot({ - path <- render_rmarkdown(pkg, - input = "assets/vignette-with-crayon.Rmd", - output = "test.html" - ) - }) - html <- xml2::read_html(path) - expect_snapshot_output(xpath_xml(html, ".//code//span[@class='co']")) -})