From d7bf1ccfb500b627f7a732f484d0acec3a6bee27 Mon Sep 17 00:00:00 2001 From: "Jennifer (Jenny) Bryan" Date: Mon, 25 Nov 2024 11:08:23 -0800 Subject: [PATCH] Assist with Quarto vignettes and articles (#2085) * Get use_vignette() working for qmd * Another test * Attempt to get "or" between 2 .vals Approach taken from https://github.com/r-lib/cli/issues/681#issuecomment-2314892316 * Catch up on the article side * Add NEWS bullet * Remove comment --- NEWS.md | 7 +- R/vignette.R | 119 ++++++++++++++++++++++++------ inst/templates/article.qmd | 12 +++ inst/templates/vignette.qmd | 16 ++++ man/use_vignette.Rd | 29 ++++++-- tests/testthat/_snaps/vignette.md | 9 +++ tests/testthat/test-vignette.R | 72 ++++++++++++++++-- 7 files changed, 226 insertions(+), 38 deletions(-) create mode 100644 inst/templates/article.qmd create mode 100644 inst/templates/vignette.qmd diff --git a/NEWS.md b/NEWS.md index b1c58551b..5bbdada9d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,11 +1,14 @@ # usethis (development version) +* `use_vignette()` and `use_article()` now support Quarto. The `name` of the new + vignette or article can optionally include a file extension to signal whether + `.Rmd` or `.qmd` is desired, with `.Rmd` remaining the default for now. Thanks + to @olivroy for getting the ball rolling (#1997). + * `use_tidy_upkeep_issue()` now records the year it is being run in the `Config/usethis/upkeep` field in DESCRIPTION. If this value exists it is furthermore used to filter the checklist when making the issue. -## Bug fixes and minor improvements - * `use_package()` now decreases a package minimum version required when `min_version` is lower than what is currently specified in the DESCRIPTION file (@jplecavalier, #1957). diff --git a/R/vignette.R b/R/vignette.R index ceab26aa0..515f28c08 100644 --- a/R/vignette.R +++ b/R/vignette.R @@ -10,43 +10,85 @@ #' * Adds `inst/doc` to `.gitignore` so built vignettes aren't tracked. #' * Adds `vignettes/*.html` and `vignettes/*.R` to `.gitignore` so #' you never accidentally track rendered vignettes. -#' @param name Base for file name to use for new vignette. Should consist only -#' of numbers, letters, `_` and `-`. Lower case is recommended. -#' @param title The title of the vignette. -#' @seealso The [vignettes chapter](https://r-pkgs.org/vignettes.html) of -#' [R Packages](https://r-pkgs.org). +#' * For `*.qmd`, adds Quarto-related patterns to `.gitignore` and +#' `.Rbuildignore`. +#' @param name File name to use for new vignette. Should consist only of +#' numbers, letters, `_` and `-`. Lower case is recommended. Can include the +#' `".Rmd"` or `".qmd"` file extension, which also dictates whether to place +#' an R Markdown or Quarto vignette. R Markdown (`".Rmd"`) is the current +#' default, but it is anticipated that Quarto (`".qmd"`) will become the +#' default in the future. +#' @param title The title of the vignette. If not provided, a title is generated +#' from `name`. +#' @seealso +#' * The [vignettes chapter](https://r-pkgs.org/vignettes.html) of +#' [R Packages](https://r-pkgs.org) +#' * The pkgdown vignette on Quarto: +#' `vignette("quarto", package = "pkgdown")` +#' * The quarto (as in the R package) vignette on HTML vignettes: +#' `vignette("hello", package = "quarto")` #' @export #' @examples #' \dontrun{ #' use_vignette("how-to-do-stuff", "How to do stuff") +#' use_vignette("r-markdown-is-classic.Rmd", "R Markdown is classic") +#' use_vignette("quarto-is-cool.qmd", "Quarto is cool") #' } -use_vignette <- function(name, title = name) { +use_vignette <- function(name, title = NULL) { check_is_package("use_vignette()") check_required(name) + maybe_name(title) + + ext <- get_vignette_extension(name) + if (ext == "qmd") { + check_installed("quarto") + check_installed("pkgdown", version = "2.1.0") + } + + name <- path_ext_remove(name) check_vignette_name(name) + title <- title %||% name use_dependency("knitr", "Suggests") - use_dependency("rmarkdown", "Suggests") - - proj_desc_field_update("VignetteBuilder", "knitr", overwrite = TRUE) use_git_ignore("inst/doc") - use_vignette_template("vignette.Rmd", name, title) + if (tolower(ext) == "rmd") { + use_dependency("rmarkdown", "Suggests") + proj_desc_field_update("VignetteBuilder", "knitr", overwrite = TRUE, append = TRUE) + use_vignette_template("vignette.Rmd", name, title) + } else { + use_dependency("quarto", "Suggests") + proj_desc_field_update("VignetteBuilder", "quarto", overwrite = TRUE, append = TRUE) + use_vignette_template("vignette.qmd", name, title) + } invisible() } #' @export #' @rdname use_vignette -use_article <- function(name, title = name) { +use_article <- function(name, title = NULL) { check_is_package("use_article()") + check_required(name) + maybe_name(title) - deps <- proj_deps() - if (!"rmarkdown" %in% deps$package) { - proj_desc_field_update("Config/Needs/website", "rmarkdown", append = TRUE) + ext <- get_vignette_extension(name) + if (ext == "qmd") { + check_installed("quarto") + check_installed("pkgdown", version = "2.1.0") } - use_vignette_template("article.Rmd", name, title, subdir = "articles") + name <- path_ext_remove(name) + title <- title %||% name + + if (tolower(ext) == "rmd") { + proj_desc_field_update("Config/Needs/website", "rmarkdown", overwrite = TRUE, append = TRUE) + use_vignette_template("article.Rmd", name, title, subdir = "articles") + } else { + use_dependency("quarto", "Suggests") + proj_desc_field_update("Config/Needs/website", "quarto", overwrite = TRUE, append = TRUE) + use_vignette_template("article.qmd", name, title, subdir = "articles") + } use_build_ignore("vignettes/articles") invisible() @@ -58,18 +100,26 @@ use_vignette_template <- function(template, name, title, subdir = NULL) { check_name(title) maybe_name(subdir) - use_directory("vignettes") - if (!is.null(subdir)) { - use_directory(path("vignettes", subdir)) - } - use_git_ignore(c("*.html", "*.R"), directory = "vignettes") + ext <- get_vignette_extension(template) if (is.null(subdir)) { - path <- path("vignettes", asciify(name), ext = "Rmd") + target_dir <- "vignettes" } else { - path <- path("vignettes", subdir, asciify(name), ext = "Rmd") + target_dir <- path("vignettes", subdir) + } + + use_directory(target_dir) + + use_git_ignore(c("*.html", "*.R"), directory = target_dir) + if (ext == "qmd") { + use_git_ignore("**/.quarto/") + use_git_ignore("*_files", target_dir) + use_build_ignore(path(target_dir, ".quarto")) + use_build_ignore(path(target_dir, "*_files")) } + path <- path(target_dir, asciify(name), ext = ext) + data <- list( Package = project_name(), vignette_title = title, @@ -102,3 +152,28 @@ check_vignette_name <- function(name) { valid_vignette_name <- function(x) { grepl("^[[:alpha:]][[:alnum:]_-]*$", x) } + +check_vignette_extension <- function(ext) { + # Quietly accept "rmd" here, tho we'll always write ".Rmd" in such a filepath + if (! ext %in% c("Rmd", "rmd", "qmd")) { + valid_exts_cli <- cli::cli_vec( + c("Rmd", "qmd"), + style = list("vec-sep2" = " or ") + ) + ui_abort(c( + "Unsupported file extension: {.val {ext}}", + "usethis can only create a vignette or article with one of these + extensions: {.val {valid_exts_cli}}." + )) + } +} + +get_vignette_extension <- function(name) { + ext <- path_ext(name) + if (nzchar(ext)) { + check_vignette_extension(ext) + } else { + ext <- "Rmd" + } + ext +} diff --git a/inst/templates/article.qmd b/inst/templates/article.qmd new file mode 100644 index 000000000..a4f4ac1fa --- /dev/null +++ b/inst/templates/article.qmd @@ -0,0 +1,12 @@ +--- +title: "{{{ vignette_title }}}" +knitr: + opts_chunk: + collapse: true + comment: '#>' +--- + +```{r} +#| label: setup +library({{Package}}) +``` diff --git a/inst/templates/vignette.qmd b/inst/templates/vignette.qmd new file mode 100644 index 000000000..e46284de0 --- /dev/null +++ b/inst/templates/vignette.qmd @@ -0,0 +1,16 @@ +--- +title: "{{{ vignette_title }}}" +vignette: > + %\VignetteIndexEntry{{{ braced_vignette_title }}} + %\VignetteEngine{quarto::html} + %\VignetteEncoding{UTF-8} +knitr: + opts_chunk: + collapse: true + comment: '#>' +--- + +```{r} +#| label: setup +library({{Package}}) +``` diff --git a/man/use_vignette.Rd b/man/use_vignette.Rd index 8c5fa70fa..9dcf81700 100644 --- a/man/use_vignette.Rd +++ b/man/use_vignette.Rd @@ -5,15 +5,20 @@ \alias{use_article} \title{Create a vignette or article} \usage{ -use_vignette(name, title = name) +use_vignette(name, title = NULL) -use_article(name, title = name) +use_article(name, title = NULL) } \arguments{ -\item{name}{Base for file name to use for new vignette. Should consist only -of numbers, letters, \verb{_} and \code{-}. Lower case is recommended.} +\item{name}{File name to use for new vignette. Should consist only of +numbers, letters, \verb{_} and \code{-}. Lower case is recommended. Can include the +\code{".Rmd"} or \code{".qmd"} file extension, which also dictates whether to place +an R Markdown or Quarto vignette. R Markdown (\code{".Rmd"}) is the current +default, but it is anticipated that Quarto (\code{".qmd"}) will become the +default in the future.} -\item{title}{The title of the vignette.} +\item{title}{The title of the vignette. If not provided, a title is generated +from \code{name}.} } \description{ Creates a new vignette or article in \verb{vignettes/}. Articles are a special @@ -28,15 +33,25 @@ automatically). \item Adds \code{inst/doc} to \code{.gitignore} so built vignettes aren't tracked. \item Adds \verb{vignettes/*.html} and \verb{vignettes/*.R} to \code{.gitignore} so you never accidentally track rendered vignettes. +\item For \verb{*.qmd}, adds Quarto-related patterns to \code{.gitignore} and +\code{.Rbuildignore}. } } \examples{ \dontrun{ use_vignette("how-to-do-stuff", "How to do stuff") +use_vignette("r-markdown-is-classic.Rmd", "R Markdown is classic") +use_vignette("quarto-is-cool.qmd", "Quarto is cool") } } \seealso{ -The \href{https://r-pkgs.org/vignettes.html}{vignettes chapter} of -\href{https://r-pkgs.org}{R Packages}. +\itemize{ +\item The \href{https://r-pkgs.org/vignettes.html}{vignettes chapter} of +\href{https://r-pkgs.org}{R Packages} +\item The pkgdown vignette on Quarto: +\code{vignette("quarto", package = "pkgdown")} +\item The quarto (as in the R package) vignette on HTML vignettes: +\code{vignette("hello", package = "quarto")} +} } diff --git a/tests/testthat/_snaps/vignette.md b/tests/testthat/_snaps/vignette.md index e7359ae1b..dbbd22c5f 100644 --- a/tests/testthat/_snaps/vignette.md +++ b/tests/testthat/_snaps/vignette.md @@ -13,3 +13,12 @@ i Start with a letter. i Contain only letters, numbers, '_', and '-'. +# we error informatively for bad vignette extension + + Code + check_vignette_extension("Rnw") + Condition + Error in `check_vignette_extension()`: + x Unsupported file extension: "Rnw" + i usethis can only create a vignette or article with one of these extensions: "Rmd" or "qmd". + diff --git a/tests/testthat/test-vignette.R b/tests/testthat/test-vignette.R index 82b7a4690..c23681fe7 100644 --- a/tests/testthat/test-vignette.R +++ b/tests/testthat/test-vignette.R @@ -15,7 +15,7 @@ test_that("use_vignette() gives useful errors", { }) }) -test_that("use_vignette() does the promised setup", { +test_that("use_vignette() does the promised setup, Rmd", { create_local_package() use_vignette("name", "title") @@ -32,28 +32,79 @@ test_that("use_vignette() does the promised setup", { expect_identical(proj_desc()$get_field("VignetteBuilder"), "knitr") }) -# use_article ------------------------------------------------------------- +test_that("use_vignette() does the promised setup, qmd", { + create_local_package() + local_check_installed() + + use_vignette("name.qmd", "title") + expect_proj_file("vignettes/name.qmd") + + ignores <- read_utf8(proj_path(".gitignore")) + expect_true("inst/doc" %in% ignores) + + deps <- proj_deps() + expect_true( + all(c("knitr", "quarto") %in% deps$package[deps$type == "Suggests"]) + ) + + expect_identical(proj_desc()$get_field("VignetteBuilder"), "quarto") +}) -test_that("use_article goes in article subdirectory", { +test_that("use_vignette() does the promised setup, mix of Rmd and qmd", { create_local_package() + local_check_installed() + + use_vignette("older-vignette", "older Rmd vignette") + use_vignette("newer-vignette.qmd", "newer qmd vignette") + expect_proj_file("vignettes/older-vignette.Rmd") + expect_proj_file("vignettes/newer-vignette.qmd") + + deps <- proj_deps() + expect_true( + all(c("knitr", "quarto", "rmarkdown") %in% deps$package[deps$type == "Suggests"]) + ) - use_article("test") - expect_proj_file("vignettes/articles/test.Rmd") + vignette_builder <- proj_desc()$get_field("VignetteBuilder") + expect_match(vignette_builder, "knitr", fixed = TRUE) + expect_match(vignette_builder, "quarto", fixed = TRUE) }) -test_that("use_article() adds rmarkdown to Config/Needs/website", { +# use_article ------------------------------------------------------------- +test_that("use_article() does the promised setup, Rmd", { create_local_package() local_interactive(FALSE) - proj_desc_field_update("Config/Needs/website", "somepackage", append = TRUE) + # Let's have another package already in Config/Needs/website + proj_desc_field_update("Config/Needs/website", "somepackage") use_article("name", "title") + expect_proj_file("vignettes/articles/name.Rmd") + expect_setequal( proj_desc()$get_list("Config/Needs/website"), c("rmarkdown", "somepackage") ) }) +# Note that qmd articles seem to cause problems for build_site() rn +# https://github.com/r-lib/pkgdown/issues/2821 +test_that("use_article() does the promised setup, qmd", { + create_local_package() + local_check_installed() + local_interactive(FALSE) + + # Let's have another package already in Config/Needs/website + proj_desc_field_update("Config/Needs/website", "somepackage") + use_article("name.qmd", "title") + + expect_proj_file("vignettes/articles/name.qmd") + + expect_setequal( + proj_desc()$get_list("Config/Needs/website"), + c("quarto", "somepackage") + ) +}) + # helpers ----------------------------------------------------------------- test_that("valid_vignette_name() works", { @@ -61,3 +112,10 @@ test_that("valid_vignette_name() works", { expect_false(valid_vignette_name("01-test")) expect_false(valid_vignette_name("test.1")) }) + +test_that("we error informatively for bad vignette extension", { + expect_snapshot( + error = TRUE, + check_vignette_extension("Rnw") + ) +})