From ed545dd6a5771238c5aebd0a64104f8df30835aa Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Thu, 21 Nov 2024 19:22:02 -0800 Subject: [PATCH 1/6] Get use_vignette() working for qmd --- R/vignette.R | 86 +++++++++++++++++++++++++++++----- inst/templates/article.qmd | 12 +++++ inst/templates/vignette.qmd | 16 +++++++ man/use_vignette.Rd | 27 ++++++++--- tests/testthat/test-vignette.R | 39 ++++++++++++++- 5 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 inst/templates/article.qmd create mode 100644 inst/templates/vignette.qmd diff --git a/R/vignette.R b/R/vignette.R index ceab26aa0..389bdb423 100644 --- a/R/vignette.R +++ b/R/vignette.R @@ -10,28 +10,57 @@ #' * 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() } @@ -58,16 +87,23 @@ use_vignette_template <- function(template, name, title, subdir = NULL) { check_name(title) maybe_name(subdir) + ext <- get_vignette_extension(template) + use_directory("vignettes") if (!is.null(subdir)) { use_directory(path("vignettes", subdir)) } use_git_ignore(c("*.html", "*.R"), directory = "vignettes") + if (ext == "qmd") { + use_build_ignore("vignettes/.quarto") + use_build_ignore("vignettes/*_files") + use_git_ignore("*_files", "vignettes") + } if (is.null(subdir)) { - path <- path("vignettes", asciify(name), ext = "Rmd") + path <- path("vignettes", asciify(name), ext = ext) } else { - path <- path("vignettes", subdir, asciify(name), ext = "Rmd") + path <- path("vignettes", subdir, asciify(name), ext = ext) } data <- list( @@ -102,3 +138,29 @@ check_vignette_name <- function(name) { valid_vignette_name <- function(x) { grepl("^[[:alpha:]][[:alnum:]_-]*$", x) } + +check_vignette_extension <- function(ext) { + valid_exts <- c("Rmd", "rmd", "qmd") + valid_exts_cli <- cli::cli_vec( + valid_exts, + style = list("vec-last" = ", or ") + ) + if (! ext %in% valid_exts) { + ui_abort(c( + "Invalid 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..dfd7dac07 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) } \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/test-vignette.R b/tests/testthat/test-vignette.R index 82b7a4690..c580faa7f 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,6 +32,43 @@ test_that("use_vignette() does the promised setup", { expect_identical(proj_desc()$get_field("VignetteBuilder"), "knitr") }) +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_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"]) + ) + + vignette_builder <- proj_desc()$get_field("VignetteBuilder") + expect_match(vignette_builder, "knitr", fixed = TRUE) + expect_match(vignette_builder, "quarto", fixed = TRUE) +}) + # use_article ------------------------------------------------------------- test_that("use_article goes in article subdirectory", { From 577e6bb486e8cdce8874d33711a43f5ea89dfae6 Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Fri, 22 Nov 2024 13:00:11 -0800 Subject: [PATCH 2/6] Another test --- R/vignette.R | 15 +++++++-------- tests/testthat/_snaps/vignette.md | 9 +++++++++ tests/testthat/test-vignette.R | 7 +++++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/R/vignette.R b/R/vignette.R index 389bdb423..f60517bda 100644 --- a/R/vignette.R +++ b/R/vignette.R @@ -140,18 +140,17 @@ valid_vignette_name <- function(x) { } check_vignette_extension <- function(ext) { - valid_exts <- c("Rmd", "rmd", "qmd") - valid_exts_cli <- cli::cli_vec( - valid_exts, - style = list("vec-last" = ", or ") - ) - if (! ext %in% valid_exts) { + # 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-last" = ", or ") + ) ui_abort(c( - "Invalid file extension: {.val {ext}}", + "Unsupported file extension: {.val {ext}}", "usethis can only create a vignette or article with one of these extensions: {.val {valid_exts_cli}}." )) - } } 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 c580faa7f..bb2d67633 100644 --- a/tests/testthat/test-vignette.R +++ b/tests/testthat/test-vignette.R @@ -98,3 +98,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") + ) +}) From 742c2d89521428d1cc5ce21496b35f87788effa5 Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Fri, 22 Nov 2024 13:46:54 -0800 Subject: [PATCH 3/6] Attempt to get "or" between 2 .vals Approach taken from https://github.com/r-lib/cli/issues/681#issuecomment-2314892316 --- R/vignette.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/vignette.R b/R/vignette.R index f60517bda..2b7fcaaaf 100644 --- a/R/vignette.R +++ b/R/vignette.R @@ -144,7 +144,7 @@ check_vignette_extension <- function(ext) { if (! ext %in% c("Rmd", "rmd", "qmd")) { valid_exts_cli <- cli::cli_vec( c("Rmd", "qmd"), - style = list("vec-last" = ", or ") + style = list("vec-last" = " or ", "vec-sep2" = " or ") ) ui_abort(c( "Unsupported file extension: {.val {ext}}", From c18a327a265dc92f4243ca595cb0e01762f0aff2 Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Sat, 23 Nov 2024 08:02:21 -0800 Subject: [PATCH 4/6] Catch up on the article side --- R/vignette.R | 51 ++++++++++++++++++++++------------ man/use_vignette.Rd | 2 +- tests/testthat/test-vignette.R | 30 ++++++++++++++------ 3 files changed, 56 insertions(+), 27 deletions(-) diff --git a/R/vignette.R b/R/vignette.R index 2b7fcaaaf..59b4c32c0 100644 --- a/R/vignette.R +++ b/R/vignette.R @@ -67,15 +67,29 @@ use_vignette <- function(name, title = NULL) { #' @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 { + # check this dependency stuff + 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() @@ -89,22 +103,23 @@ use_vignette_template <- function(template, name, title, subdir = NULL) { ext <- get_vignette_extension(template) - use_directory("vignettes") - if (!is.null(subdir)) { - use_directory(path("vignettes", subdir)) + if (is.null(subdir)) { + target_dir <- "vignettes" + } else { + target_dir <- path("vignettes", subdir) } - use_git_ignore(c("*.html", "*.R"), directory = "vignettes") + + use_directory(target_dir) + + use_git_ignore(c("*.html", "*.R"), directory = target_dir) if (ext == "qmd") { - use_build_ignore("vignettes/.quarto") - use_build_ignore("vignettes/*_files") - use_git_ignore("*_files", "vignettes") + use_git_ignore("**/.quarto/") + use_git_ignore("*_files", target_dir) + use_build_ignore(path(target_dir, ".quarto")) + use_build_ignore(path(target_dir, "*_files")) } - if (is.null(subdir)) { - path <- path("vignettes", asciify(name), ext = ext) - } else { - path <- path("vignettes", subdir, asciify(name), ext = ext) - } + path <- path(target_dir, asciify(name), ext = ext) data <- list( Package = project_name(), @@ -144,7 +159,7 @@ check_vignette_extension <- function(ext) { if (! ext %in% c("Rmd", "rmd", "qmd")) { valid_exts_cli <- cli::cli_vec( c("Rmd", "qmd"), - style = list("vec-last" = " or ", "vec-sep2" = " or ") + style = list("vec-sep2" = " or ") ) ui_abort(c( "Unsupported file extension: {.val {ext}}", diff --git a/man/use_vignette.Rd b/man/use_vignette.Rd index dfd7dac07..9dcf81700 100644 --- a/man/use_vignette.Rd +++ b/man/use_vignette.Rd @@ -7,7 +7,7 @@ \usage{ use_vignette(name, title = NULL) -use_article(name, title = name) +use_article(name, title = NULL) } \arguments{ \item{name}{File name to use for new vignette. Should consist only of diff --git a/tests/testthat/test-vignette.R b/tests/testthat/test-vignette.R index bb2d67633..c23681fe7 100644 --- a/tests/testthat/test-vignette.R +++ b/tests/testthat/test-vignette.R @@ -70,24 +70,38 @@ test_that("use_vignette() does the promised setup, mix of Rmd and qmd", { }) # use_article ------------------------------------------------------------- - -test_that("use_article goes in article subdirectory", { +test_that("use_article() does the promised setup, Rmd", { create_local_package() + local_interactive(FALSE) + + # Let's have another package already in Config/Needs/website + proj_desc_field_update("Config/Needs/website", "somepackage") + use_article("name", "title") - use_article("test") - expect_proj_file("vignettes/articles/test.Rmd") + expect_proj_file("vignettes/articles/name.Rmd") + + expect_setequal( + proj_desc()$get_list("Config/Needs/website"), + c("rmarkdown", "somepackage") + ) }) -test_that("use_article() adds rmarkdown to Config/Needs/website", { +# 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) - proj_desc_field_update("Config/Needs/website", "somepackage", append = TRUE) - use_article("name", "title") + # 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("rmarkdown", "somepackage") + c("quarto", "somepackage") ) }) From ab1d3a838892eb55972652468bcbb680436ca2c7 Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Sat, 23 Nov 2024 09:44:17 -0800 Subject: [PATCH 5/6] Add NEWS bullet --- NEWS.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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). From 3190e8aed8e83875dc2b877b710df1b7908b03da Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Sun, 24 Nov 2024 09:13:36 -0800 Subject: [PATCH 6/6] Remove comment --- R/vignette.R | 1 - 1 file changed, 1 deletion(-) diff --git a/R/vignette.R b/R/vignette.R index 59b4c32c0..515f28c08 100644 --- a/R/vignette.R +++ b/R/vignette.R @@ -85,7 +85,6 @@ use_article <- function(name, title = NULL) { proj_desc_field_update("Config/Needs/website", "rmarkdown", overwrite = TRUE, append = TRUE) use_vignette_template("article.Rmd", name, title, subdir = "articles") } else { - # check this dependency stuff 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")