From 4745a0278dbcc4aaa87c9362a4da66cf1fa6cb12 Mon Sep 17 00:00:00 2001 From: parmsam Date: Mon, 8 Jul 2024 15:10:07 -0400 Subject: [PATCH 01/20] ensure check extension approval returns true on y --- R/utils-prompt.R | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/R/utils-prompt.R b/R/utils-prompt.R index fb0c00fd..7e6c9351 100644 --- a/R/utils-prompt.R +++ b/R/utils-prompt.R @@ -1,7 +1,7 @@ check_extension_approval <- function(no_prompt = FALSE, what = "Something", see_more_at = NULL) { if (no_prompt) return(TRUE) - if (!rlang::is_interactive()) { + if (!is_interactive()) { cli::cli_abort(c( "{ what } requires explicit approval.", ">" = "Set {.arg no_prompt = TRUE} if you agree.", @@ -19,5 +19,13 @@ check_extension_approval <- function(no_prompt = FALSE, what = "Something", see_ cli::cli_inform("{what} not installed.") return(invisible(FALSE)) } + return(invisible(TRUE)) } } + +# Add binding to base R function for testthat mocking +readline <- NULL +# Add binding to function from other package for mocking later on +is_interactive <- function(...) { + rlang::is_interactive(...) +} From 7e424704c841f383ffba13444cc102c469fd7e58 Mon Sep 17 00:00:00 2001 From: parmsam Date: Mon, 8 Jul 2024 15:10:21 -0400 Subject: [PATCH 02/20] add unit tests with mocking in check extesnion approval func --- tests/testthat/test-utils-prompt.R | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/testthat/test-utils-prompt.R diff --git a/tests/testthat/test-utils-prompt.R b/tests/testthat/test-utils-prompt.R new file mode 100644 index 00000000..52eaa4e5 --- /dev/null +++ b/tests/testthat/test-utils-prompt.R @@ -0,0 +1,36 @@ +test_that("Checking extension with approval prompt mocked y", { + local_mocked_bindings( + readline = function(...) "y", + is_interactive = function() TRUE + ) + expect_true({ + check_extension_approval(FALSE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + }) +}) + +test_that("Checking extension with approval prompt mocked n", { + local_mocked_bindings( + readline = function(...) "n", + is_interactive = function() TRUE + ) + expect_false({ + check_extension_approval(FALSE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + }) +}) + +test_that("Checking extension approval", { + skip_if_no_quarto() + skip_if_offline("github.com") + + expect_true(check_extension_approval(TRUE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html")) + expect_true(check_extension_approval(TRUE, "Quarto templates", "https://quarto.org/docs/extensions/formats.html#distributing-formats")) + + expect_error({ + local_reproducible_output(rlang_interactive = FALSE) + check_extension_approval(FALSE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + }) + expect_error({ + local_reproducible_output(rlang_interactive = FALSE) + check_extension_approval(TRUE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + }) +}) From 2c847b4c03aec7dd190867ffabff14e42fb5c817 Mon Sep 17 00:00:00 2001 From: parmsam Date: Tue, 9 Jul 2024 10:58:56 -0400 Subject: [PATCH 03/20] add check removal approval func --- R/utils-prompt.R | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/R/utils-prompt.R b/R/utils-prompt.R index 7e6c9351..1407baf5 100644 --- a/R/utils-prompt.R +++ b/R/utils-prompt.R @@ -23,6 +23,26 @@ check_extension_approval <- function(no_prompt = FALSE, what = "Something", see_ } } +check_removal_approval <- function(no_prompt = FALSE, what = "Something", see_more_at = NULL) { + if (no_prompt) return(TRUE) + + if (!is_interactive()) { + cli::cli_abort(c( + "{ what } requires explicit approval.", + ">" = "Set {.arg no_prompt = TRUE} if you agree.", + if (!is.null(see_more_at)) { + c(i = "See more at {.url {see_more_at}}") + } + )) + } else { + prompt_value <- tolower(readline(sprintf("? Are you sure you'd like to remove %s (Y/n)? ", what))) + if (!prompt_value %in% "y") { + return(invisible(FALSE)) + } + return(invisible(TRUE)) + } +} + # Add binding to base R function for testthat mocking readline <- NULL # Add binding to function from other package for mocking later on From f499d22e65f85bd14caca682b426a6f449ec6a20 Mon Sep 17 00:00:00 2001 From: parmsam Date: Tue, 9 Jul 2024 10:59:08 -0400 Subject: [PATCH 04/20] create list, remove, and update funcs --- R/list.R | 38 ++++++++++++++++++++++++++++++++++++++ R/remove.R | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ R/update.R | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 R/list.R create mode 100644 R/remove.R create mode 100644 R/update.R diff --git a/R/list.R b/R/list.R new file mode 100644 index 00000000..a555554b --- /dev/null +++ b/R/list.R @@ -0,0 +1,38 @@ +#' List Installed Quarto extensions +#' +#' List Quarto Extensions in this folder or project by running `quarto list` +#' +#' # Extension Trust +#' +#' Quarto extensions may execute code when documents are rendered. Therefore, if +#' you do not trust the author of an extension, we recommend that you do not +#' install or use the extension. +#' +#' @inheritParams quarto_render +#' +#' @examples +#' \dontrun{ +#' # List Quarto Extensions in this folder or project +#' quarto_list_extensions() +#' } +#' +#' @importFrom rlang is_interactive +#' @importFrom cli cli_abort +#' @export +quarto_list_extensions <- function(quiet = FALSE, quarto_args = NULL){ + quarto_bin <- find_quarto() + + args <- c("extensions", if (quiet) cli_arg_quiet(), quarto_args) + x <- quarto_list(args, quarto_bin = quarto_bin, echo = TRUE) + # Clean the stderr output to remove extra spaces and ensure consistent formatting + stderr_cleaned <- gsub("\\s+$", "", x$stderr) + if (grepl("No extensions are installed", stderr_cleaned)) { + invisible() + } else{ + invisible(read.table(text = stderr_cleaned, header = TRUE, fill = TRUE, sep = "", stringsAsFactors = FALSE)) + } +} + +quarto_list <- function(args = character(), ...){ + quarto_run_what("list", args = args, ...) +} diff --git a/R/remove.R b/R/remove.R new file mode 100644 index 00000000..49f8edf4 --- /dev/null +++ b/R/remove.R @@ -0,0 +1,48 @@ +#' Remove a Quarto extensions +#' +#' Remove an extension in this folder or project by running `quarto remove` +#' +#' # Extension Trust +#' +#' Quarto extensions may execute code when documents are rendered. Therefore, if +#' you do not trust the author of an extension, we recommend that you do not +#' install or use the extension. +#' By default `no_prompt = FALSE` which means that +#' the function will ask for explicit approval when used interactively, or +#' disallow installation. +#' +#' @inheritParams quarto_render +#' +#' @param extension The extension to remove, either an archive or a GitHub +#' repository as described in the documentation +#' . +#' +#' @param no_prompt Do not prompt to confirm approval to download external extension. +#' +#' @examples +#' \dontrun{ +#' # Remove an already installed extension +#' quarto_remove_extension("quarto-ext/fontawesome") +#' +#' @importFrom rlang is_interactive +#' @importFrom cli cli_abort +#' @export +quarto_remove_extension <- function(extension = NULL, no_prompt = FALSE, quiet = FALSE, quarto_args = NULL) { + rlang::check_required(extension) + + quarto_bin <- find_quarto() + + # This will ask for approval or stop installation + approval <- check_removal_approval(no_prompt, extension, "https://quarto.org/docs/extensions/managing.html") + + if (approval) { + args <- c(extension, "--no-prompt", if (quiet) cli_arg_quiet(), quarto_args) + quarto_remove(args, quarto_bin = quarto_bin, echo = TRUE) + } + + invisible() +} + +quarto_remove <- function(args = character(), ...) { + quarto_run_what("remove", args = args, ...) +} diff --git a/R/update.R b/R/update.R new file mode 100644 index 00000000..41054070 --- /dev/null +++ b/R/update.R @@ -0,0 +1,52 @@ +#' Update a Quarto extensions +#' +#' Update an extension to this folder or project by running `quarto update` +#' +#' # Extension Trust +#' +#' Quarto extensions may execute code when documents are rendered. Therefore, if +#' you do not trust the author of an extension, we recommend that you do not +#' install or use the extension. +#' By default `no_prompt = FALSE` which means that +#' the function will ask for explicit approval when used interactively, or +#' disallow installation. +#' +#' @inheritParams quarto_render +#' +#' @param extension The extension to install, either an archive or a GitHub +#' repository as described in the documentation +#' . +#' +#' @param no_prompt Do not prompt to confirm approval to download external extension. +#' +#' @examples +#' \dontrun{ +#' # Update a template and set up a draft document from a GitHub repository +#' quarto_update_extension("quarto-ext/fontawesome") +#' +#' # Update a template and set up a draft document from a ZIP archive +#' quarto_update_extension("https://github.com/quarto-ext/fontawesome/archive/refs/heads/main.zip") +#' } +#' +#' @importFrom rlang is_interactive +#' @importFrom cli cli_abort +#' @export +quarto_update_extension <- function(extension = NULL, no_prompt = FALSE, quiet = FALSE, quarto_args = NULL) { + rlang::check_required(extension) + + quarto_bin <- find_quarto() + + # This will ask for approval or stop installation + approval <- check_extension_approval(no_prompt, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + + if (approval) { + args <- c(extension, "--no-prompt", if (quiet) cli_arg_quiet(), quarto_args) + quarto_update(args, quarto_bin = quarto_bin, echo = TRUE) + } + + invisible() +} + +quarto_update <- function(args = character(), ...) { + quarto_run_what("update", args = args, ...) +} From dd63903d8a1f996576ebcfcd7befa111ccb9da1f Mon Sep 17 00:00:00 2001 From: parmsam Date: Tue, 9 Jul 2024 10:59:15 -0400 Subject: [PATCH 05/20] add unit test for new funcs --- tests/testthat/test-list.R | 13 +++++++++++++ tests/testthat/test-remove.R | 11 +++++++++++ tests/testthat/test-update.R | 11 +++++++++++ 3 files changed, 35 insertions(+) create mode 100644 tests/testthat/test-list.R create mode 100644 tests/testthat/test-remove.R create mode 100644 tests/testthat/test-update.R diff --git a/tests/testthat/test-list.R b/tests/testthat/test-list.R new file mode 100644 index 00000000..02afda8e --- /dev/null +++ b/tests/testthat/test-list.R @@ -0,0 +1,13 @@ +test_that("Listing extensions", { + skip_if_no_quarto() + skip_if_offline("github.com") + qmd <- local_qmd_file(c("content")) + withr::local_dir(dirname(qmd)) + expect_null(quarto_list_extensions()) + quarto_add_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) + expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) + expect_equal(nrow(quarto_list_extensions()), 1) + quarto_add_extension("quarto-ext/lightbox", no_prompt = TRUE, quiet = TRUE) + expect_true(dir.exists("_extensions/quarto-ext/lightbox")) + expect_equal(nrow(quarto_list_extensions()), 2) +}) diff --git a/tests/testthat/test-remove.R b/tests/testthat/test-remove.R new file mode 100644 index 00000000..bfe652f7 --- /dev/null +++ b/tests/testthat/test-remove.R @@ -0,0 +1,11 @@ +test_that("Removing an extension", { + skip_if_no_quarto() + skip_if_offline("github.com") + qmd <- local_qmd_file(c("content")) + withr::local_dir(dirname(qmd)) + expect_null(quarto_remove_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE)) + quarto_add_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) + expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) + quarto_remove_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) + expect_true(!dir.exists("_extensions/quarto-ext/fontawesome")) +}) diff --git a/tests/testthat/test-update.R b/tests/testthat/test-update.R new file mode 100644 index 00000000..c1867a23 --- /dev/null +++ b/tests/testthat/test-update.R @@ -0,0 +1,11 @@ +test_that("Updating an extension", { + skip_if_no_quarto() + skip_if_offline("github.com") + qmd <- local_qmd_file(c("content")) + withr::local_dir(dirname(qmd)) + expect_error(quarto_add_extension("quarto-ext/fontawesome@v0.0.1"), "explicit approval") + quarto_update_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) + expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) + current_version <- yaml::read_yaml("_extensions/quarto-ext/fontawesome/_extension.yml")$version + expect_false(identical(current_version, "v0.0.1")) +}) From 33663ca1c8d9e5cd07fa5a7a40a714e0e1a3328d Mon Sep 17 00:00:00 2001 From: parmsam Date: Tue, 9 Jul 2024 11:03:04 -0400 Subject: [PATCH 06/20] adjust roxygen2 headers --- R/list.R | 6 ------ R/update.R | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/R/list.R b/R/list.R index a555554b..50d92ae0 100644 --- a/R/list.R +++ b/R/list.R @@ -2,12 +2,6 @@ #' #' List Quarto Extensions in this folder or project by running `quarto list` #' -#' # Extension Trust -#' -#' Quarto extensions may execute code when documents are rendered. Therefore, if -#' you do not trust the author of an extension, we recommend that you do not -#' install or use the extension. -#' #' @inheritParams quarto_render #' #' @examples diff --git a/R/update.R b/R/update.R index 41054070..1d6dee8b 100644 --- a/R/update.R +++ b/R/update.R @@ -13,7 +13,7 @@ #' #' @inheritParams quarto_render #' -#' @param extension The extension to install, either an archive or a GitHub +#' @param extension The extension to update, either an archive or a GitHub #' repository as described in the documentation #' . #' From fa71abf33e6fcbc4cfe8f2467e3f895e058760ff Mon Sep 17 00:00:00 2001 From: parmsam Date: Tue, 9 Jul 2024 11:22:46 -0400 Subject: [PATCH 07/20] rebuild documentation --- NAMESPACE | 4 +++ R/list.R | 3 ++- R/remove.R | 2 +- man/quarto_list_extensions.Rd | 26 ++++++++++++++++++ man/quarto_publish_doc.Rd | 14 +++++----- man/quarto_remove_extension.Rd | 45 +++++++++++++++++++++++++++++++ man/quarto_update_extension.Rd | 49 ++++++++++++++++++++++++++++++++++ 7 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 man/quarto_list_extensions.Rd create mode 100644 man/quarto_remove_extension.Rd create mode 100644 man/quarto_update_extension.Rd diff --git a/NAMESPACE b/NAMESPACE index 9e9fa9fc..8a7e6d58 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,14 +5,17 @@ export(quarto_add_extension) export(quarto_binary_sitrep) export(quarto_create_project) export(quarto_inspect) +export(quarto_list_extensions) export(quarto_path) export(quarto_preview) export(quarto_preview_stop) export(quarto_publish_app) export(quarto_publish_doc) export(quarto_publish_site) +export(quarto_remove_extension) export(quarto_render) export(quarto_serve) +export(quarto_update_extension) export(quarto_use_template) export(quarto_version) import(rlang) @@ -28,4 +31,5 @@ importFrom(rstudioapi,isAvailable) importFrom(rstudioapi,viewer) importFrom(tools,vignetteEngine) importFrom(utils,browseURL) +importFrom(utils,read.table) importFrom(yaml,write_yaml) diff --git a/R/list.R b/R/list.R index 50d92ae0..6aa76d81 100644 --- a/R/list.R +++ b/R/list.R @@ -12,6 +12,7 @@ #' #' @importFrom rlang is_interactive #' @importFrom cli cli_abort +#' @importFrom utils read.table #' @export quarto_list_extensions <- function(quiet = FALSE, quarto_args = NULL){ quarto_bin <- find_quarto() @@ -23,7 +24,7 @@ quarto_list_extensions <- function(quiet = FALSE, quarto_args = NULL){ if (grepl("No extensions are installed", stderr_cleaned)) { invisible() } else{ - invisible(read.table(text = stderr_cleaned, header = TRUE, fill = TRUE, sep = "", stringsAsFactors = FALSE)) + invisible(utils::read.table(text = stderr_cleaned, header = TRUE, fill = TRUE, sep = "", stringsAsFactors = FALSE)) } } diff --git a/R/remove.R b/R/remove.R index 49f8edf4..cc8f54b8 100644 --- a/R/remove.R +++ b/R/remove.R @@ -23,7 +23,7 @@ #' \dontrun{ #' # Remove an already installed extension #' quarto_remove_extension("quarto-ext/fontawesome") -#' +#' } #' @importFrom rlang is_interactive #' @importFrom cli cli_abort #' @export diff --git a/man/quarto_list_extensions.Rd b/man/quarto_list_extensions.Rd new file mode 100644 index 00000000..aba1743d --- /dev/null +++ b/man/quarto_list_extensions.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/list.R +\name{quarto_list_extensions} +\alias{quarto_list_extensions} +\title{List Installed Quarto extensions} +\usage{ +quarto_list_extensions(quiet = FALSE, quarto_args = NULL) +} +\arguments{ +\item{quiet}{Suppress warning and other messages.} + +\item{quarto_args}{Character vector of other \code{quarto} CLI arguments to append +to the Quarto command executed by this function. This is mainly intended for +advanced usage and useful for CLI arguments which are not yet mirrored in a +dedicated parameter of this \R function. See \verb{quarto render --help} for options.} +} +\description{ +List Quarto Extensions in this folder or project by running \verb{quarto list} +} +\examples{ +\dontrun{ +# List Quarto Extensions in this folder or project +quarto_list_extensions() +} + +} diff --git a/man/quarto_publish_doc.Rd b/man/quarto_publish_doc.Rd index df6c0114..8e6ae6f6 100644 --- a/man/quarto_publish_doc.Rd +++ b/man/quarto_publish_doc.Rd @@ -50,11 +50,12 @@ Defaults to the name of the \code{input}.} supplied, will often be displayed in favor of the name. When deploying a new document, you may supply only the title to receive an auto-generated name} -\item{account, server}{Uniquely identify a remote server with either your -user \code{account}, the \code{server} name, or both. If neither are supplied, and -there are multiple options, you'll be prompted to pick one. +\item{server}{Server name. Required only if you use the same account name on +multiple servers.} -Use \code{\link[rsconnect:accounts]{accounts()}} to see the full list of available options.} +\item{account}{Account to deploy application to. This parameter is only +required for the initial deployment of an application when there are +multiple accounts configured on the system (see \link[rsconnect]{accounts}).} \item{render}{\code{local} to render locally before publishing; \code{server} to render on the server; \code{none} to use whatever rendered content currently @@ -62,10 +63,7 @@ exists locally. (defaults to \code{local})} \item{metadata}{Additional metadata fields to save with the deployment record. These fields will be returned on subsequent calls to -\code{\link[rsconnect:deployments]{deployments()}}. - -Multi-value fields are recorded as comma-separated values and returned in -that form. Custom value serialization is the responsibility of the caller.} +\code{\link[rsconnect:deployments]{deployments()}}.} \item{...}{Named parameters to pass along to \code{rsconnect::deployApp()}} } diff --git a/man/quarto_remove_extension.Rd b/man/quarto_remove_extension.Rd new file mode 100644 index 00000000..e296ba14 --- /dev/null +++ b/man/quarto_remove_extension.Rd @@ -0,0 +1,45 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/remove.R +\name{quarto_remove_extension} +\alias{quarto_remove_extension} +\title{Remove a Quarto extensions} +\usage{ +quarto_remove_extension( + extension = NULL, + no_prompt = FALSE, + quiet = FALSE, + quarto_args = NULL +) +} +\arguments{ +\item{extension}{The extension to remove, either an archive or a GitHub +repository as described in the documentation +\url{https://quarto.org/docs/extensions/managing.html}.} + +\item{no_prompt}{Do not prompt to confirm approval to download external extension.} + +\item{quiet}{Suppress warning and other messages.} + +\item{quarto_args}{Character vector of other \code{quarto} CLI arguments to append +to the Quarto command executed by this function. This is mainly intended for +advanced usage and useful for CLI arguments which are not yet mirrored in a +dedicated parameter of this \R function. See \verb{quarto render --help} for options.} +} +\description{ +Remove an extension in this folder or project by running \verb{quarto remove} +} +\section{Extension Trust}{ +Quarto extensions may execute code when documents are rendered. Therefore, if +you do not trust the author of an extension, we recommend that you do not +install or use the extension. +By default \code{no_prompt = FALSE} which means that +the function will ask for explicit approval when used interactively, or +disallow installation. +} + +\examples{ +\dontrun{ +# Remove an already installed extension +quarto_remove_extension("quarto-ext/fontawesome") +} +} diff --git a/man/quarto_update_extension.Rd b/man/quarto_update_extension.Rd new file mode 100644 index 00000000..372b1eb7 --- /dev/null +++ b/man/quarto_update_extension.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/update.R +\name{quarto_update_extension} +\alias{quarto_update_extension} +\title{Update a Quarto extensions} +\usage{ +quarto_update_extension( + extension = NULL, + no_prompt = FALSE, + quiet = FALSE, + quarto_args = NULL +) +} +\arguments{ +\item{extension}{The extension to update, either an archive or a GitHub +repository as described in the documentation +\url{https://quarto.org/docs/extensions/managing.html}.} + +\item{no_prompt}{Do not prompt to confirm approval to download external extension.} + +\item{quiet}{Suppress warning and other messages.} + +\item{quarto_args}{Character vector of other \code{quarto} CLI arguments to append +to the Quarto command executed by this function. This is mainly intended for +advanced usage and useful for CLI arguments which are not yet mirrored in a +dedicated parameter of this \R function. See \verb{quarto render --help} for options.} +} +\description{ +Update an extension to this folder or project by running \verb{quarto update} +} +\section{Extension Trust}{ +Quarto extensions may execute code when documents are rendered. Therefore, if +you do not trust the author of an extension, we recommend that you do not +install or use the extension. +By default \code{no_prompt = FALSE} which means that +the function will ask for explicit approval when used interactively, or +disallow installation. +} + +\examples{ +\dontrun{ +# Update a template and set up a draft document from a GitHub repository +quarto_update_extension("quarto-ext/fontawesome") + +# Update a template and set up a draft document from a ZIP archive +quarto_update_extension("https://github.com/quarto-ext/fontawesome/archive/refs/heads/main.zip") +} + +} From ba02f7b2287441ed4dbbd6f8718559e7fed3eb0a Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 7 May 2025 15:55:53 +0200 Subject: [PATCH 08/20] format with air --- R/list.R | 14 ++++++++---- R/remove.R | 13 +++++++++-- R/theme.R | 20 ++++++++++------- R/update.R | 13 +++++++++-- R/utils-prompt.R | 11 +++++++-- tests/testthat/test-remove.R | 12 ++++++++-- tests/testthat/test-theme.R | 2 -- tests/testthat/test-update.R | 15 ++++++++++--- tests/testthat/test-utils-prompt.R | 36 +++++++++++++++++++++++++----- 9 files changed, 105 insertions(+), 31 deletions(-) diff --git a/R/list.R b/R/list.R index 6aa76d81..c1469158 100644 --- a/R/list.R +++ b/R/list.R @@ -14,7 +14,7 @@ #' @importFrom cli cli_abort #' @importFrom utils read.table #' @export -quarto_list_extensions <- function(quiet = FALSE, quarto_args = NULL){ +quarto_list_extensions <- function(quiet = FALSE, quarto_args = NULL) { quarto_bin <- find_quarto() args <- c("extensions", if (quiet) cli_arg_quiet(), quarto_args) @@ -23,11 +23,17 @@ quarto_list_extensions <- function(quiet = FALSE, quarto_args = NULL){ stderr_cleaned <- gsub("\\s+$", "", x$stderr) if (grepl("No extensions are installed", stderr_cleaned)) { invisible() - } else{ - invisible(utils::read.table(text = stderr_cleaned, header = TRUE, fill = TRUE, sep = "", stringsAsFactors = FALSE)) + } else { + invisible(utils::read.table( + text = stderr_cleaned, + header = TRUE, + fill = TRUE, + sep = "", + stringsAsFactors = FALSE + )) } } -quarto_list <- function(args = character(), ...){ +quarto_list <- function(args = character(), ...) { quarto_run_what("list", args = args, ...) } diff --git a/R/remove.R b/R/remove.R index cc8f54b8..27856956 100644 --- a/R/remove.R +++ b/R/remove.R @@ -27,13 +27,22 @@ #' @importFrom rlang is_interactive #' @importFrom cli cli_abort #' @export -quarto_remove_extension <- function(extension = NULL, no_prompt = FALSE, quiet = FALSE, quarto_args = NULL) { +quarto_remove_extension <- function( + extension = NULL, + no_prompt = FALSE, + quiet = FALSE, + quarto_args = NULL +) { rlang::check_required(extension) quarto_bin <- find_quarto() # This will ask for approval or stop installation - approval <- check_removal_approval(no_prompt, extension, "https://quarto.org/docs/extensions/managing.html") + approval <- check_removal_approval( + no_prompt, + extension, + "https://quarto.org/docs/extensions/managing.html" + ) if (approval) { args <- c(extension, "--no-prompt", if (quiet) cli_arg_quiet(), quarto_args) diff --git a/R/theme.R b/R/theme.R index 9fd5e17c..d58d54c6 100644 --- a/R/theme.R +++ b/R/theme.R @@ -10,7 +10,6 @@ #' @param fg The foreground color #' @param brand_yml The path to a brand.yml file - #' @rdname theme_helpers #' #' @export @@ -41,7 +40,8 @@ theme_colors_ggplot <- function(bg, fg) { if (!requireNamespace("ggplot2", quietly = TRUE)) { return(NULL) } - ggplot2::`%+%`(ggplot2::theme_minimal(base_size = 11), + ggplot2::`%+%`( + ggplot2::theme_minimal(base_size = 11), ggplot2::theme( panel.border = ggplot2::element_blank(), panel.grid.major.y = ggplot2::element_blank(), @@ -54,7 +54,8 @@ theme_colors_ggplot <- function(bg, fg) { plot.background = ggplot2::element_rect(fill = bg, colour = NA), axis.line = ggplot2::element_line(colour = fg), axis.ticks = ggplot2::element_line(colour = fg) - )) + ) + ) } #' @rdname theme_helpers @@ -92,10 +93,12 @@ theme_brand_gt <- function(brand_yml) { #' @export theme_colors_plotly <- function(bg, fg) { (function(plot) { - plot |> plotly::layout(paper_bgcolor = bg, - plot_bgcolor = bg, - font = list(color = fg) - ) + plot |> + plotly::layout( + paper_bgcolor = bg, + plot_bgcolor = bg, + font = list(color = fg) + ) }) } @@ -116,7 +119,8 @@ theme_colors_thematic <- function(bg, fg) { thematic::thematic_rmd( bg = bg, fg = fg, - )}) + ) + }) } #' @rdname theme_helpers diff --git a/R/update.R b/R/update.R index 1d6dee8b..565241a5 100644 --- a/R/update.R +++ b/R/update.R @@ -31,13 +31,22 @@ #' @importFrom rlang is_interactive #' @importFrom cli cli_abort #' @export -quarto_update_extension <- function(extension = NULL, no_prompt = FALSE, quiet = FALSE, quarto_args = NULL) { +quarto_update_extension <- function( + extension = NULL, + no_prompt = FALSE, + quiet = FALSE, + quarto_args = NULL +) { rlang::check_required(extension) quarto_bin <- find_quarto() # This will ask for approval or stop installation - approval <- check_extension_approval(no_prompt, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + approval <- check_extension_approval( + no_prompt, + "Quarto extensions", + "https://quarto.org/docs/extensions/managing.html" + ) if (approval) { args <- c(extension, "--no-prompt", if (quiet) cli_arg_quiet(), quarto_args) diff --git a/R/utils-prompt.R b/R/utils-prompt.R index d5c310f4..ea3143e2 100644 --- a/R/utils-prompt.R +++ b/R/utils-prompt.R @@ -32,7 +32,11 @@ check_extension_approval <- function( } } -check_removal_approval <- function(no_prompt = FALSE, what = "Something", see_more_at = NULL) { +check_removal_approval <- function( + no_prompt = FALSE, + what = "Something", + see_more_at = NULL +) { if (no_prompt) return(TRUE) if (!is_interactive()) { @@ -44,7 +48,10 @@ check_removal_approval <- function(no_prompt = FALSE, what = "Something", see_mo } )) } else { - prompt_value <- tolower(readline(sprintf("? Are you sure you'd like to remove %s (Y/n)? ", what))) + prompt_value <- tolower(readline(sprintf( + "? Are you sure you'd like to remove %s (Y/n)? ", + what + ))) if (!prompt_value %in% "y") { return(invisible(FALSE)) } diff --git a/tests/testthat/test-remove.R b/tests/testthat/test-remove.R index bfe652f7..9c5334be 100644 --- a/tests/testthat/test-remove.R +++ b/tests/testthat/test-remove.R @@ -3,9 +3,17 @@ test_that("Removing an extension", { skip_if_offline("github.com") qmd <- local_qmd_file(c("content")) withr::local_dir(dirname(qmd)) - expect_null(quarto_remove_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE)) + expect_null(quarto_remove_extension( + "quarto-ext/fontawesome", + no_prompt = TRUE, + quiet = TRUE + )) quarto_add_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) - quarto_remove_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) + quarto_remove_extension( + "quarto-ext/fontawesome", + no_prompt = TRUE, + quiet = TRUE + ) expect_true(!dir.exists("_extensions/quarto-ext/fontawesome")) }) diff --git a/tests/testthat/test-theme.R b/tests/testthat/test-theme.R index 23622b3a..692976d9 100644 --- a/tests/testthat/test-theme.R +++ b/tests/testthat/test-theme.R @@ -1,4 +1,3 @@ - test_that("render flextable", { skip_if_no_quarto() quarto_render("theme/flextable.qmd", quiet = TRUE) @@ -45,4 +44,3 @@ test_that("render thematic", { expect_true(file.exists("theme/thematic.html")) unlink("theme/thematic.html") }) - diff --git a/tests/testthat/test-update.R b/tests/testthat/test-update.R index c1867a23..7ed6ac3d 100644 --- a/tests/testthat/test-update.R +++ b/tests/testthat/test-update.R @@ -3,9 +3,18 @@ test_that("Updating an extension", { skip_if_offline("github.com") qmd <- local_qmd_file(c("content")) withr::local_dir(dirname(qmd)) - expect_error(quarto_add_extension("quarto-ext/fontawesome@v0.0.1"), "explicit approval") - quarto_update_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) + expect_error( + quarto_add_extension("quarto-ext/fontawesome@v0.0.1"), + "explicit approval" + ) + quarto_update_extension( + "quarto-ext/fontawesome", + no_prompt = TRUE, + quiet = TRUE + ) expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) - current_version <- yaml::read_yaml("_extensions/quarto-ext/fontawesome/_extension.yml")$version + current_version <- yaml::read_yaml( + "_extensions/quarto-ext/fontawesome/_extension.yml" + )$version expect_false(identical(current_version, "v0.0.1")) }) diff --git a/tests/testthat/test-utils-prompt.R b/tests/testthat/test-utils-prompt.R index 52eaa4e5..28a32eeb 100644 --- a/tests/testthat/test-utils-prompt.R +++ b/tests/testthat/test-utils-prompt.R @@ -4,7 +4,11 @@ test_that("Checking extension with approval prompt mocked y", { is_interactive = function() TRUE ) expect_true({ - check_extension_approval(FALSE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + check_extension_approval( + FALSE, + "Quarto extensions", + "https://quarto.org/docs/extensions/managing.html" + ) }) }) @@ -14,7 +18,11 @@ test_that("Checking extension with approval prompt mocked n", { is_interactive = function() TRUE ) expect_false({ - check_extension_approval(FALSE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + check_extension_approval( + FALSE, + "Quarto extensions", + "https://quarto.org/docs/extensions/managing.html" + ) }) }) @@ -22,15 +30,31 @@ test_that("Checking extension approval", { skip_if_no_quarto() skip_if_offline("github.com") - expect_true(check_extension_approval(TRUE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html")) - expect_true(check_extension_approval(TRUE, "Quarto templates", "https://quarto.org/docs/extensions/formats.html#distributing-formats")) + expect_true(check_extension_approval( + TRUE, + "Quarto extensions", + "https://quarto.org/docs/extensions/managing.html" + )) + expect_true(check_extension_approval( + TRUE, + "Quarto templates", + "https://quarto.org/docs/extensions/formats.html#distributing-formats" + )) expect_error({ local_reproducible_output(rlang_interactive = FALSE) - check_extension_approval(FALSE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + check_extension_approval( + FALSE, + "Quarto extensions", + "https://quarto.org/docs/extensions/managing.html" + ) }) expect_error({ local_reproducible_output(rlang_interactive = FALSE) - check_extension_approval(TRUE, "Quarto extensions", "https://quarto.org/docs/extensions/managing.html") + check_extension_approval( + TRUE, + "Quarto extensions", + "https://quarto.org/docs/extensions/managing.html" + ) }) }) From c651265c10904ce4deee55587c9a937f74c15a5c Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 7 May 2025 16:15:51 +0200 Subject: [PATCH 09/20] tweak quarto_list_extensions() --- NAMESPACE | 1 - NEWS.md | 3 +++ R/list.R | 16 +++++++--------- man/quarto_list_extensions.Rd | 11 +++-------- tests/testthat/_snaps/list.md | 17 +++++++++++++++++ tests/testthat/test-list.R | 6 ++++-- 6 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 tests/testthat/_snaps/list.md diff --git a/NAMESPACE b/NAMESPACE index 038dd3ea..7fe0658d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -41,5 +41,4 @@ importFrom(rstudioapi,isAvailable) importFrom(rstudioapi,viewer) importFrom(tools,vignetteEngine) importFrom(utils,browseURL) -importFrom(utils,read.table) importFrom(yaml,write_yaml) diff --git a/NEWS.md b/NEWS.md index 8e231348..a081691a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # quarto (development version) +- Add several new wrapper function: + - `quarto_list_extensions()` to list installed extensions using `quarto list extensions` + - `quarto_preview()` gains a `quiet` argument to suppress any output from R or Quarto CLI (thanks, @cwickham, #232.) - Add some helpers function `theme_brand_*` and `theme_colors_*` to help theme with dark and light brand using some common graph and table packages (thanks, @gordonwoodhull, [#234](https://github.com/quarto-dev/quarto-r/issues/234)). diff --git a/R/list.R b/R/list.R index c1469158..811fa88f 100644 --- a/R/list.R +++ b/R/list.R @@ -2,7 +2,7 @@ #' #' List Quarto Extensions in this folder or project by running `quarto list` #' -#' @inheritParams quarto_render +#' @return A data frame with the installed extensions or NULL (invisibly) if no extensions are installed. #' #' @examples #' \dontrun{ @@ -10,27 +10,25 @@ #' quarto_list_extensions() #' } #' -#' @importFrom rlang is_interactive -#' @importFrom cli cli_abort -#' @importFrom utils read.table #' @export -quarto_list_extensions <- function(quiet = FALSE, quarto_args = NULL) { +quarto_list_extensions <- function() { quarto_bin <- find_quarto() - args <- c("extensions", if (quiet) cli_arg_quiet(), quarto_args) - x <- quarto_list(args, quarto_bin = quarto_bin, echo = TRUE) + # quarto list extensions --quiet will return nothing so we need to prevent that. + args <- c("extensions") + x <- quarto_list(args, quarto_bin = quarto_bin, echo = FALSE) # Clean the stderr output to remove extra spaces and ensure consistent formatting stderr_cleaned <- gsub("\\s+$", "", x$stderr) if (grepl("No extensions are installed", stderr_cleaned)) { invisible() } else { - invisible(utils::read.table( + utils::read.table( text = stderr_cleaned, header = TRUE, fill = TRUE, sep = "", stringsAsFactors = FALSE - )) + ) } } diff --git a/man/quarto_list_extensions.Rd b/man/quarto_list_extensions.Rd index aba1743d..ffbff6a0 100644 --- a/man/quarto_list_extensions.Rd +++ b/man/quarto_list_extensions.Rd @@ -4,15 +4,10 @@ \alias{quarto_list_extensions} \title{List Installed Quarto extensions} \usage{ -quarto_list_extensions(quiet = FALSE, quarto_args = NULL) +quarto_list_extensions() } -\arguments{ -\item{quiet}{Suppress warning and other messages.} - -\item{quarto_args}{Character vector of other \code{quarto} CLI arguments to append -to the Quarto command executed by this function. This is mainly intended for -advanced usage and useful for CLI arguments which are not yet mirrored in a -dedicated parameter of this \R function. See \verb{quarto render --help} for options.} +\value{ +A data frame with the installed extensions or NULL (invisibly) if no extensions are installed. } \description{ List Quarto Extensions in this folder or project by running \verb{quarto list} diff --git a/tests/testthat/_snaps/list.md b/tests/testthat/_snaps/list.md new file mode 100644 index 00000000..7f99b9b8 --- /dev/null +++ b/tests/testthat/_snaps/list.md @@ -0,0 +1,17 @@ +# Listing extensions + + Code + quarto_list_extensions() + Output + Id Version Contributes + 1 quarto-ext/fontawesome 1.2.0 shortcodes + +--- + + Code + quarto_list_extensions() + Output + Id Version Contributes + 1 quarto-ext/fontawesome 1.2.0 shortcodes + 2 quarto-ext/lightbox 0.1.9 filters + diff --git a/tests/testthat/test-list.R b/tests/testthat/test-list.R index 02afda8e..0d93fe43 100644 --- a/tests/testthat/test-list.R +++ b/tests/testthat/test-list.R @@ -1,4 +1,6 @@ test_that("Listing extensions", { + # don't try to install extensions on CRAN + skip_on_cran() skip_if_no_quarto() skip_if_offline("github.com") qmd <- local_qmd_file(c("content")) @@ -6,8 +8,8 @@ test_that("Listing extensions", { expect_null(quarto_list_extensions()) quarto_add_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) - expect_equal(nrow(quarto_list_extensions()), 1) + expect_snapshot(quarto_list_extensions()) quarto_add_extension("quarto-ext/lightbox", no_prompt = TRUE, quiet = TRUE) expect_true(dir.exists("_extensions/quarto-ext/lightbox")) - expect_equal(nrow(quarto_list_extensions()), 2) + expect_snapshot(quarto_list_extensions()) }) From 5f2d28674d12bdc026050f91d0d3e445aa0df55f Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 7 May 2025 16:46:14 +0200 Subject: [PATCH 10/20] tweak quarto_remove_extensions --- NAMESPACE | 2 +- NEWS.md | 1 + R/remove.R | 41 +++++---- R/utils-prompt.R | 87 ++++++++++--------- ...tension.Rd => quarto_remove_extensions.Rd} | 27 +++--- tests/testthat/_snaps/remove.md | 14 +++ tests/testthat/test-remove.R | 16 ++-- 7 files changed, 103 insertions(+), 85 deletions(-) rename man/{quarto_remove_extension.Rd => quarto_remove_extensions.Rd} (55%) create mode 100644 tests/testthat/_snaps/remove.md diff --git a/NAMESPACE b/NAMESPACE index 7fe0658d..8c988570 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,7 +12,7 @@ export(quarto_preview_stop) export(quarto_publish_app) export(quarto_publish_doc) export(quarto_publish_site) -export(quarto_remove_extension) +export(quarto_remove_extensions) export(quarto_render) export(quarto_serve) export(quarto_update_extension) diff --git a/NEWS.md b/NEWS.md index a081691a..8a0c41ba 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ - Add several new wrapper function: - `quarto_list_extensions()` to list installed extensions using `quarto list extensions` + - `quarto_remove_extensions()` to remove an installed extension using `quarto remove extensions` - `quarto_preview()` gains a `quiet` argument to suppress any output from R or Quarto CLI (thanks, @cwickham, #232.) diff --git a/R/remove.R b/R/remove.R index 27856956..ed936b69 100644 --- a/R/remove.R +++ b/R/remove.R @@ -2,32 +2,24 @@ #' #' Remove an extension in this folder or project by running `quarto remove` #' -#' # Extension Trust -#' -#' Quarto extensions may execute code when documents are rendered. Therefore, if -#' you do not trust the author of an extension, we recommend that you do not -#' install or use the extension. -#' By default `no_prompt = FALSE` which means that -#' the function will ask for explicit approval when used interactively, or -#' disallow installation. -#' #' @inheritParams quarto_render #' -#' @param extension The extension to remove, either an archive or a GitHub -#' repository as described in the documentation -#' . +#' @param extension The extension name to remove, as in `quarto remove `. #' #' @param no_prompt Do not prompt to confirm approval to download external extension. #' +#' +#' @return Returns invisibly `TRUE` if the extension was removed, `FALSE` otherwise. +#' +#' @seealso `quarto_add_extension()` and [Quarto Website](https://quarto.org/docs/extensions/managing.html). +#' #' @examples #' \dontrun{ #' # Remove an already installed extension -#' quarto_remove_extension("quarto-ext/fontawesome") +#' quarto_remove_extensions("quarto-ext/fontawesome") #' } -#' @importFrom rlang is_interactive -#' @importFrom cli cli_abort #' @export -quarto_remove_extension <- function( +quarto_remove_extensions <- function( extension = NULL, no_prompt = FALSE, quiet = FALSE, @@ -35,6 +27,14 @@ quarto_remove_extension <- function( ) { rlang::check_required(extension) + installed_extensions <- quarto_list_extensions() + if (is.null(installed_extensions)) { + if (!quiet) { + cli::cli_alert_warning("No extensions installed.") + } + return(invisible(FALSE)) + } + quarto_bin <- find_quarto() # This will ask for approval or stop installation @@ -46,10 +46,15 @@ quarto_remove_extension <- function( if (approval) { args <- c(extension, "--no-prompt", if (quiet) cli_arg_quiet(), quarto_args) - quarto_remove(args, quarto_bin = quarto_bin, echo = TRUE) + quarto_remove(args, quarto_bin = quarto_bin, echo = FALSE) + if (!quiet) { + cli::cli_alert_success( + "Extension {.code {extension}} successfully removed." + ) + } } - invisible() + invisible(TRUE) } quarto_remove <- function(args = character(), ...) { diff --git a/R/utils-prompt.R b/R/utils-prompt.R index ea3143e2..f2944285 100644 --- a/R/utils-prompt.R +++ b/R/utils-prompt.R @@ -1,7 +1,9 @@ -check_extension_approval <- function( +check_approval <- function( no_prompt = FALSE, what = "Something", - see_more_at = NULL + see_more_at = NULL, + prompt_message = NULL, + interactive_info = NULL # could use `{ what }` as used in `cli_inform()` ) { if (no_prompt) return(TRUE) @@ -14,54 +16,57 @@ check_extension_approval <- function( } )) } else { - cli::cli_inform(c( - "{what} may execute code when documents are rendered. ", - "*" = "If you do not trust the author(s) of this {what}, we recommend that you do not install or use this {what}." - )) - prompt_value <- tolower(readline(sprintf( - "Do you trust the authors of this %s (Y/n)? ", - what - ))) - if (!prompt_value %in% "y") { - cli::cli_inform("{what} not installed.") - return(invisible(FALSE)) - } else { - return(invisible(TRUE)) + if (!is.null(interactive_info)) { + cli::cli_inform(interactive_info) + } + prompt_value <- tolower(readline(prompt_message)) + if (!prompt_value %in% c("", "y")) { + cli::cli_inform(paste0(what, " not installed.")) } - return(invisible(TRUE)) + return(invisible(FALSE)) } + return(invisible(TRUE)) } -check_removal_approval <- function( +check_extension_approval <- function( no_prompt = FALSE, what = "Something", see_more_at = NULL ) { - if (no_prompt) return(TRUE) + interactive_info <- c( + "{what} may execute code when documents are rendered. ", + "*" = "If you do not trust the author(s) of this {what}, we recommend that you do not install or use this {what}." + ) - if (!is_interactive()) { - cli::cli_abort(c( - "{ what } requires explicit approval.", - ">" = "Set {.arg no_prompt = TRUE} if you agree.", - if (!is.null(see_more_at)) { - c(i = "See more at {.url {see_more_at}}") - } - )) - } else { - prompt_value <- tolower(readline(sprintf( - "? Are you sure you'd like to remove %s (Y/n)? ", - what - ))) - if (!prompt_value %in% "y") { - return(invisible(FALSE)) - } - return(invisible(TRUE)) - } + prompt_message <- sprintf( + "Do you trust the authors of this %s (Y/n)? ", + what + ) + + check_approval( + no_prompt = no_prompt, + what = what, + see_more_at = see_more_at, + prompt_message = prompt_message, + interactive_info = interactive_info + ) } -# Add binding to base R function for testthat mocking -readline <- NULL -# Add binding to function from other package for mocking later on -is_interactive <- function(...) { - rlang::is_interactive(...) +check_removal_approval <- function( + no_prompt = FALSE, + what = "Something", + see_more_at = NULL +) { + prompt_message <- sprintf( + "Are you sure you'd like to remove %s (Y/n)? ", + what + ) + + check_approval( + no_prompt = no_prompt, + what = what, + see_more_at = see_more_at, + prompt_message = prompt_message, + interactive_info = NULL + ) } diff --git a/man/quarto_remove_extension.Rd b/man/quarto_remove_extensions.Rd similarity index 55% rename from man/quarto_remove_extension.Rd rename to man/quarto_remove_extensions.Rd index e296ba14..eb44bebe 100644 --- a/man/quarto_remove_extension.Rd +++ b/man/quarto_remove_extensions.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/remove.R -\name{quarto_remove_extension} -\alias{quarto_remove_extension} +\name{quarto_remove_extensions} +\alias{quarto_remove_extensions} \title{Remove a Quarto extensions} \usage{ -quarto_remove_extension( +quarto_remove_extensions( extension = NULL, no_prompt = FALSE, quiet = FALSE, @@ -12,9 +12,7 @@ quarto_remove_extension( ) } \arguments{ -\item{extension}{The extension to remove, either an archive or a GitHub -repository as described in the documentation -\url{https://quarto.org/docs/extensions/managing.html}.} +\item{extension}{The extension name to remove, as in \verb{quarto remove }.} \item{no_prompt}{Do not prompt to confirm approval to download external extension.} @@ -25,21 +23,18 @@ to the Quarto command executed by this function. This is mainly intended for advanced usage and useful for CLI arguments which are not yet mirrored in a dedicated parameter of this \R function. See \verb{quarto render --help} for options.} } +\value{ +Returns invisibly \code{TRUE} if the extension was removed, \code{FALSE} otherwise. +} \description{ Remove an extension in this folder or project by running \verb{quarto remove} } -\section{Extension Trust}{ -Quarto extensions may execute code when documents are rendered. Therefore, if -you do not trust the author of an extension, we recommend that you do not -install or use the extension. -By default \code{no_prompt = FALSE} which means that -the function will ask for explicit approval when used interactively, or -disallow installation. -} - \examples{ \dontrun{ # Remove an already installed extension -quarto_remove_extension("quarto-ext/fontawesome") +quarto_remove_extensions("quarto-ext/fontawesome") +} } +\seealso{ +\code{quarto_add_extension()} and \href{https://quarto.org/docs/extensions/managing.html}{Quarto Website}. } diff --git a/tests/testthat/_snaps/remove.md b/tests/testthat/_snaps/remove.md new file mode 100644 index 00000000..52b77684 --- /dev/null +++ b/tests/testthat/_snaps/remove.md @@ -0,0 +1,14 @@ +# Removing an extension + + Code + expect_false(quarto_remove_extension("quarto-ext/fontawesome", no_prompt = TRUE)) + Message + ! No extensions installed. + +--- + + Code + expect_true(quarto_remove_extension("quarto-ext/fontawesome", no_prompt = TRUE)) + Message + v Extension `quarto-ext/fontawesome` successfully removed. + diff --git a/tests/testthat/test-remove.R b/tests/testthat/test-remove.R index 9c5334be..5ccb8bc8 100644 --- a/tests/testthat/test-remove.R +++ b/tests/testthat/test-remove.R @@ -3,17 +3,15 @@ test_that("Removing an extension", { skip_if_offline("github.com") qmd <- local_qmd_file(c("content")) withr::local_dir(dirname(qmd)) - expect_null(quarto_remove_extension( + expect_snapshot(expect_false(quarto_remove_extension( "quarto-ext/fontawesome", - no_prompt = TRUE, - quiet = TRUE - )) + no_prompt = TRUE + ))) quarto_add_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) - quarto_remove_extension( + expect_snapshot(expect_true(quarto_remove_extension( "quarto-ext/fontawesome", - no_prompt = TRUE, - quiet = TRUE - ) - expect_true(!dir.exists("_extensions/quarto-ext/fontawesome")) + no_prompt = TRUE + ))) + expect_false(dir.exists("_extensions")) }) From 719bafdb5109b98e349796ed651ce7bd392f3e23 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 9 May 2025 14:09:22 +0200 Subject: [PATCH 11/20] Tweak update wrapper --- NAMESPACE | 2 +- NEWS.md | 3 ++- R/remove.R | 4 +-- R/update.R | 27 ++++++++++--------- ...tensions.Rd => quarto_remove_extension.Rd} | 8 +++--- man/quarto_update_extension.Rd | 13 +++++---- tests/testthat/test-add.R | 1 + tests/testthat/test-update.R | 17 +++++++----- 8 files changed, 43 insertions(+), 32 deletions(-) rename man/{quarto_remove_extensions.Rd => quarto_remove_extension.Rd} (89%) diff --git a/NAMESPACE b/NAMESPACE index 8c988570..7fe0658d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,7 +12,7 @@ export(quarto_preview_stop) export(quarto_publish_app) export(quarto_publish_doc) export(quarto_publish_site) -export(quarto_remove_extensions) +export(quarto_remove_extension) export(quarto_render) export(quarto_serve) export(quarto_update_extension) diff --git a/NEWS.md b/NEWS.md index 8a0c41ba..b1c2f55d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,8 @@ - Add several new wrapper function: - `quarto_list_extensions()` to list installed extensions using `quarto list extensions` - - `quarto_remove_extensions()` to remove an installed extension using `quarto remove extensions` + - `quarto_remove_extension()` to remove an installed extension using `quarto remove extensions` + - `quarto_update_extension()` to update an installed extension using `quarto update extensions` - `quarto_preview()` gains a `quiet` argument to suppress any output from R or Quarto CLI (thanks, @cwickham, #232.) diff --git a/R/remove.R b/R/remove.R index ed936b69..7dd68a0a 100644 --- a/R/remove.R +++ b/R/remove.R @@ -16,10 +16,10 @@ #' @examples #' \dontrun{ #' # Remove an already installed extension -#' quarto_remove_extensions("quarto-ext/fontawesome") +#' quarto_remove_extension("quarto-ext/fontawesome") #' } #' @export -quarto_remove_extensions <- function( +quarto_remove_extension <- function( extension = NULL, no_prompt = FALSE, quiet = FALSE, diff --git a/R/update.R b/R/update.R index 565241a5..3d5e2076 100644 --- a/R/update.R +++ b/R/update.R @@ -13,11 +13,13 @@ #' #' @inheritParams quarto_render #' -#' @param extension The extension to update, either an archive or a GitHub -#' repository as described in the documentation -#' . +#' @param extension The extension to update, either by its name (i.e ` quarto update extension /`), an archive (` quarto update extension `) or a url (`quarto update extension `). #' -#' @param no_prompt Do not prompt to confirm approval to download external extension. +#' @param no_prompt Do not prompt to confirm approval to download external extension. Setting `no_prompt = FALSE` means [Extension Trust](#extension-trust) is accepted. +#' +#' @seealso [quarto_add_extension()], [quarto_remove_extension()], and [Quarto website](https://quarto.org/docs/extensions/managing.html). +#' +#' @return Returns invisibly `TRUE` if the extension was updated, `FALSE` otherwise. #' #' @examples #' \dontrun{ @@ -27,9 +29,6 @@ #' # Update a template and set up a draft document from a ZIP archive #' quarto_update_extension("https://github.com/quarto-ext/fontawesome/archive/refs/heads/main.zip") #' } -#' -#' @importFrom rlang is_interactive -#' @importFrom cli cli_abort #' @export quarto_update_extension <- function( extension = NULL, @@ -44,16 +43,20 @@ quarto_update_extension <- function( # This will ask for approval or stop installation approval <- check_extension_approval( no_prompt, - "Quarto extensions", + "Quarto extension", "https://quarto.org/docs/extensions/managing.html" ) - if (approval) { - args <- c(extension, "--no-prompt", if (quiet) cli_arg_quiet(), quarto_args) - quarto_update(args, quarto_bin = quarto_bin, echo = TRUE) + if (!approval) { + return(invisible(FALSE)) } - invisible() + args <- c(extension, "--no-prompt", if (quiet) cli_arg_quiet(), quarto_args) + quarto_update(args, quarto_bin = quarto_bin, echo = TRUE) + if (!quiet) { + cli::cli_inform("Extension {.code {extension}} updated.") + } + invisible(TRUE) } quarto_update <- function(args = character(), ...) { diff --git a/man/quarto_remove_extensions.Rd b/man/quarto_remove_extension.Rd similarity index 89% rename from man/quarto_remove_extensions.Rd rename to man/quarto_remove_extension.Rd index eb44bebe..707d7ef2 100644 --- a/man/quarto_remove_extensions.Rd +++ b/man/quarto_remove_extension.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/remove.R -\name{quarto_remove_extensions} -\alias{quarto_remove_extensions} +\name{quarto_remove_extension} +\alias{quarto_remove_extension} \title{Remove a Quarto extensions} \usage{ -quarto_remove_extensions( +quarto_remove_extension( extension = NULL, no_prompt = FALSE, quiet = FALSE, @@ -32,7 +32,7 @@ Remove an extension in this folder or project by running \verb{quarto remove} \examples{ \dontrun{ # Remove an already installed extension -quarto_remove_extensions("quarto-ext/fontawesome") +quarto_remove_extension("quarto-ext/fontawesome") } } \seealso{ diff --git a/man/quarto_update_extension.Rd b/man/quarto_update_extension.Rd index 372b1eb7..5e6d8fed 100644 --- a/man/quarto_update_extension.Rd +++ b/man/quarto_update_extension.Rd @@ -12,11 +12,9 @@ quarto_update_extension( ) } \arguments{ -\item{extension}{The extension to update, either an archive or a GitHub -repository as described in the documentation -\url{https://quarto.org/docs/extensions/managing.html}.} +\item{extension}{The extension to update, either by its name (i.e \verb{ quarto update extension /}), an archive (\verb{ quarto update extension }) or a url (\verb{quarto update extension }).} -\item{no_prompt}{Do not prompt to confirm approval to download external extension.} +\item{no_prompt}{Do not prompt to confirm approval to download external extension. Setting \code{no_prompt = FALSE} means \href{#extension-trust}{Extension Trust} is accepted.} \item{quiet}{Suppress warning and other messages.} @@ -25,6 +23,9 @@ to the Quarto command executed by this function. This is mainly intended for advanced usage and useful for CLI arguments which are not yet mirrored in a dedicated parameter of this \R function. See \verb{quarto render --help} for options.} } +\value{ +Returns invisibly \code{TRUE} if the extension was updated, \code{FALSE} otherwise. +} \description{ Update an extension to this folder or project by running \verb{quarto update} } @@ -45,5 +46,7 @@ quarto_update_extension("quarto-ext/fontawesome") # Update a template and set up a draft document from a ZIP archive quarto_update_extension("https://github.com/quarto-ext/fontawesome/archive/refs/heads/main.zip") } - +} +\seealso{ +\code{\link[=quarto_add_extension]{quarto_add_extension()}}, \code{\link[=quarto_remove_extension]{quarto_remove_extension()}}, and \href{https://quarto.org/docs/extensions/managing.html}{Quarto website}. } diff --git a/tests/testthat/test-add.R b/tests/testthat/test-add.R index 96fa7850..5c6f05e2 100644 --- a/tests/testthat/test-add.R +++ b/tests/testthat/test-add.R @@ -3,6 +3,7 @@ test_that("Installing an extension", { skip_if_offline("github.com") qmd <- local_qmd_file(c("content")) withr::local_dir(dirname(qmd)) + rlang::local_interactive(FALSE) expect_error( quarto_add_extension("quarto-ext/fontawesome"), "explicit approval" diff --git a/tests/testthat/test-update.R b/tests/testthat/test-update.R index 7ed6ac3d..25263ec2 100644 --- a/tests/testthat/test-update.R +++ b/tests/testthat/test-update.R @@ -3,18 +3,21 @@ test_that("Updating an extension", { skip_if_offline("github.com") qmd <- local_qmd_file(c("content")) withr::local_dir(dirname(qmd)) - expect_error( - quarto_add_extension("quarto-ext/fontawesome@v0.0.1"), - "explicit approval" + quarto_add_extension( + "quarto-ext/fontawesome@v0.0.1", + no_prompt = TRUE, + quiet = TRUE ) + expect_equal(quarto_list_extensions()$Version, "0.0.1") quarto_update_extension( "quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE ) expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) - current_version <- yaml::read_yaml( - "_extensions/quarto-ext/fontawesome/_extension.yml" - )$version - expect_false(identical(current_version, "v0.0.1")) + expect_true( + as.numeric_version(current_version <- quarto_list_extensions()$Version) > + "0.0.1" + ) + expect_false(identical(current_version, "0.0.1")) }) From daa0958bee8bc66226f222aa807ce2aa760bba6c Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 9 May 2025 14:41:09 +0200 Subject: [PATCH 12/20] Fix mocking --- R/utils-prompt.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/R/utils-prompt.R b/R/utils-prompt.R index f2944285..8b53f54d 100644 --- a/R/utils-prompt.R +++ b/R/utils-prompt.R @@ -70,3 +70,6 @@ check_removal_approval <- function( interactive_info = NULL ) } + +# Needed for testthat to mock base function +readline <- NULL From 5b2c26f73fe211a4526530dba96930664874c64f Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 9 May 2025 15:34:03 +0200 Subject: [PATCH 13/20] rewrites test of prompts --- R/utils-prompt.R | 27 +++--- tests/testthat/_snaps/utils-prompt.md | 118 +++++++++++++++++++++++++ tests/testthat/test-utils-prompt.R | 121 +++++++++++++++++--------- 3 files changed, 216 insertions(+), 50 deletions(-) create mode 100644 tests/testthat/_snaps/utils-prompt.md diff --git a/R/utils-prompt.R b/R/utils-prompt.R index 8b53f54d..1340598c 100644 --- a/R/utils-prompt.R +++ b/R/utils-prompt.R @@ -1,29 +1,34 @@ check_approval <- function( no_prompt = FALSE, what = "Something", + not_action = "approved", see_more_at = NULL, prompt_message = NULL, - interactive_info = NULL # could use `{ what }` as used in `cli_inform()` + interactive_info = NULL, # could use `{ what }` as used in `cli_inform()` + .call = rlang::caller_env() ) { if (no_prompt) return(TRUE) if (!is_interactive()) { - cli::cli_abort(c( - "{ what } requires explicit approval.", - ">" = "Set {.arg no_prompt = TRUE} if you agree.", - if (!is.null(see_more_at)) { - c(i = "See more at {.url {see_more_at}}") - } - )) + cli::cli_abort( + c( + "{ what } requires explicit approval.", + ">" = "Set {.arg no_prompt = TRUE} if you agree.", + if (!is.null(see_more_at)) { + c(i = "See more at {.url {see_more_at}}") + } + ), + call = .call + ) } else { if (!is.null(interactive_info)) { cli::cli_inform(interactive_info) } prompt_value <- tolower(readline(prompt_message)) if (!prompt_value %in% c("", "y")) { - cli::cli_inform(paste0(what, " not installed.")) + cli::cli_alert_info(paste0(what, " not {not_action}")) + return(invisible(FALSE)) } - return(invisible(FALSE)) } return(invisible(TRUE)) } @@ -46,6 +51,7 @@ check_extension_approval <- function( check_approval( no_prompt = no_prompt, what = what, + not_action = "installed", see_more_at = see_more_at, prompt_message = prompt_message, interactive_info = interactive_info @@ -65,6 +71,7 @@ check_removal_approval <- function( check_approval( no_prompt = no_prompt, what = what, + not_action = "removed", see_more_at = see_more_at, prompt_message = prompt_message, interactive_info = NULL diff --git a/tests/testthat/_snaps/utils-prompt.md b/tests/testthat/_snaps/utils-prompt.md new file mode 100644 index 00000000..9d39c474 --- /dev/null +++ b/tests/testthat/_snaps/utils-prompt.md @@ -0,0 +1,118 @@ +# Checking non interactive approval + + Code + expect_true(check_approval(TRUE, "My thing")) + +--- + + Code + check_approval(FALSE, "My thing") + Condition + Error: + ! My thing requires explicit approval. + > Set `no_prompt = TRUE` if you agree. + +--- + + Code + check_approval(FALSE, "My thing", see_more_at = "https://example.com") + Condition + Error: + ! My thing requires explicit approval. + > Set `no_prompt = TRUE` if you agree. + i See more at + +# Checking interactive approval with prompt mocked n + + Code + expect_false({ + check_approval(FALSE, "my-thing", see_more_at = "https://example.com") + }) + Message + i my-thing not approved + +# Checking non interactive extension approval + + Code + expect_true(check_extension_approval(TRUE, "My thing")) + +--- + + Code + check_extension_approval(FALSE, "My thing") + Condition + Error in `check_extension_approval()`: + ! My thing requires explicit approval. + > Set `no_prompt = TRUE` if you agree. + +--- + + Code + check_extension_approval(FALSE, "My thing", see_more_at = "https://example.com") + Condition + Error in `check_extension_approval()`: + ! My thing requires explicit approval. + > Set `no_prompt = TRUE` if you agree. + i See more at + +# Checking interactive extension approval with prompt mocked y + + Code + expect_true({ + check_extension_approval(FALSE, "my-thing") + }) + Message + my-thing may execute code when documents are rendered. + * If you do not trust the author(s) of this my-thing, we recommend that you do not install or use this my-thing. + +# Checking interactive extension approval with prompt mocked n + + Code + expect_false({ + check_extension_approval(FALSE, "my-thing") + }) + Message + my-thing may execute code when documents are rendered. + * If you do not trust the author(s) of this my-thing, we recommend that you do not install or use this my-thing. + i my-thing not installed + +# Checking non interactive removal approval + + Code + expect_true(check_removal_approval(TRUE, "My thing")) + +--- + + Code + check_removal_approval(FALSE, "My thing") + Condition + Error in `check_removal_approval()`: + ! My thing requires explicit approval. + > Set `no_prompt = TRUE` if you agree. + +--- + + Code + check_removal_approval(FALSE, "My thing", see_more_at = "https://example.com") + Condition + Error in `check_removal_approval()`: + ! My thing requires explicit approval. + > Set `no_prompt = TRUE` if you agree. + i See more at + +# Checking interactive removal approval with prompt mocked y + + Code + expect_true({ + check_removal_approval(FALSE, "my-thing") + }) + +# Checking interactive removal approval with prompt mocked n + + Code + expect_false({ + check_removal_approval(FALSE, "my-thing") + }) + Message + i my-thing not removed + diff --git a/tests/testthat/test-utils-prompt.R b/tests/testthat/test-utils-prompt.R index 28a32eeb..5b3a0e9a 100644 --- a/tests/testthat/test-utils-prompt.R +++ b/tests/testthat/test-utils-prompt.R @@ -1,60 +1,101 @@ -test_that("Checking extension with approval prompt mocked y", { +test_that("Checking non interactive approval", { + rlang::local_interactive(FALSE) + expect_snapshot(expect_true(check_approval(TRUE, "My thing"))) + expect_snapshot(error = TRUE, { + check_approval(FALSE, "My thing") + }) + expect_snapshot(error = TRUE, { + check_approval(FALSE, "My thing", see_more_at = "https://example.com") + }) +}) + + +test_that("Checking interactive approval with prompt mocked y", { local_mocked_bindings( - readline = function(...) "y", - is_interactive = function() TRUE + readline = function(...) "y" ) + rlang::local_interactive(TRUE) expect_true({ - check_extension_approval( - FALSE, - "Quarto extensions", - "https://quarto.org/docs/extensions/managing.html" - ) + check_approval(FALSE, "my-thing") }) }) -test_that("Checking extension with approval prompt mocked n", { +test_that("Checking interactive approval with prompt mocked n", { local_mocked_bindings( - readline = function(...) "n", - is_interactive = function() TRUE + readline = function(...) "n" ) - expect_false({ + rlang::local_interactive(TRUE) + expect_snapshot(expect_false({ + check_approval(FALSE, "my-thing", see_more_at = "https://example.com") + })) +}) + +test_that("Checking non interactive extension approval", { + rlang::local_interactive(FALSE) + expect_snapshot(expect_true(check_extension_approval(TRUE, "My thing"))) + expect_snapshot(error = TRUE, { + check_extension_approval(FALSE, "My thing") + }) + expect_snapshot(error = TRUE, { check_extension_approval( FALSE, - "Quarto extensions", - "https://quarto.org/docs/extensions/managing.html" + "My thing", + see_more_at = "https://example.com" ) }) }) -test_that("Checking extension approval", { - skip_if_no_quarto() - skip_if_offline("github.com") +test_that("Checking interactive extension approval with prompt mocked y", { + local_mocked_bindings( + readline = function(...) "y" + ) + rlang::local_interactive(TRUE) + expect_snapshot(expect_true({ + check_extension_approval(FALSE, "my-thing") + })) +}) - expect_true(check_extension_approval( - TRUE, - "Quarto extensions", - "https://quarto.org/docs/extensions/managing.html" - )) - expect_true(check_extension_approval( - TRUE, - "Quarto templates", - "https://quarto.org/docs/extensions/formats.html#distributing-formats" - )) +test_that("Checking interactive extension approval with prompt mocked n", { + local_mocked_bindings( + readline = function(...) "n" + ) + rlang::local_interactive(TRUE) + expect_snapshot(expect_false({ + check_extension_approval(FALSE, "my-thing") + })) +}) - expect_error({ - local_reproducible_output(rlang_interactive = FALSE) - check_extension_approval( - FALSE, - "Quarto extensions", - "https://quarto.org/docs/extensions/managing.html" - ) +test_that("Checking non interactive removal approval", { + rlang::local_interactive(FALSE) + expect_snapshot(expect_true(check_removal_approval(TRUE, "My thing"))) + expect_snapshot(error = TRUE, { + check_removal_approval(FALSE, "My thing") }) - expect_error({ - local_reproducible_output(rlang_interactive = FALSE) - check_extension_approval( - TRUE, - "Quarto extensions", - "https://quarto.org/docs/extensions/managing.html" + expect_snapshot(error = TRUE, { + check_removal_approval( + FALSE, + "My thing", + see_more_at = "https://example.com" ) }) }) + +test_that("Checking interactive removal approval with prompt mocked y", { + local_mocked_bindings( + readline = function(...) "y" + ) + rlang::local_interactive(TRUE) + expect_snapshot(expect_true({ + check_removal_approval(FALSE, "my-thing") + })) +}) + +test_that("Checking interactive removal approval with prompt mocked n", { + local_mocked_bindings( + readline = function(...) "n" + ) + rlang::local_interactive(TRUE) + expect_snapshot(expect_false({ + check_removal_approval(FALSE, "my-thing") + })) +}) From 83c1ab450f6fc3f703006f4a899a6265ab7c7589 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 9 May 2025 16:28:19 +0200 Subject: [PATCH 14/20] sort extension table by Id --- R/list.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/list.R b/R/list.R index 811fa88f..597ef8ef 100644 --- a/R/list.R +++ b/R/list.R @@ -22,13 +22,14 @@ quarto_list_extensions <- function() { if (grepl("No extensions are installed", stderr_cleaned)) { invisible() } else { - utils::read.table( + df <- utils::read.table( text = stderr_cleaned, header = TRUE, fill = TRUE, sep = "", stringsAsFactors = FALSE ) + df[order(df$Id), ] } } From 8d3d928cb6bb564c7b758149a4ebf4c0b986c49d Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 9 May 2025 16:40:15 +0200 Subject: [PATCH 15/20] Don't use snapshot --- tests/testthat/_snaps/list.md | 17 ----------------- tests/testthat/test-list.R | 10 ++++++++-- 2 files changed, 8 insertions(+), 19 deletions(-) delete mode 100644 tests/testthat/_snaps/list.md diff --git a/tests/testthat/_snaps/list.md b/tests/testthat/_snaps/list.md deleted file mode 100644 index 7f99b9b8..00000000 --- a/tests/testthat/_snaps/list.md +++ /dev/null @@ -1,17 +0,0 @@ -# Listing extensions - - Code - quarto_list_extensions() - Output - Id Version Contributes - 1 quarto-ext/fontawesome 1.2.0 shortcodes - ---- - - Code - quarto_list_extensions() - Output - Id Version Contributes - 1 quarto-ext/fontawesome 1.2.0 shortcodes - 2 quarto-ext/lightbox 0.1.9 filters - diff --git a/tests/testthat/test-list.R b/tests/testthat/test-list.R index 0d93fe43..12a8d827 100644 --- a/tests/testthat/test-list.R +++ b/tests/testthat/test-list.R @@ -8,8 +8,14 @@ test_that("Listing extensions", { expect_null(quarto_list_extensions()) quarto_add_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE) expect_true(dir.exists("_extensions/quarto-ext/fontawesome")) - expect_snapshot(quarto_list_extensions()) + expect_identical( + quarto_list_extensions()$Id, + c("quarto-ext/fontawesome") + ) quarto_add_extension("quarto-ext/lightbox", no_prompt = TRUE, quiet = TRUE) expect_true(dir.exists("_extensions/quarto-ext/lightbox")) - expect_snapshot(quarto_list_extensions()) + expect_identical( + quarto_list_extensions()$Id, + c("quarto-ext/fontawesome", "quarto-ext/lightbox") + ) }) From 0ea6e115b19e76223d18f28180f9ca516b9d5118 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 9 May 2025 17:31:33 +0200 Subject: [PATCH 16/20] Update snapshot --- tests/testthat/_snaps/quarto.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/testthat/_snaps/quarto.md b/tests/testthat/_snaps/quarto.md index d5aac1ed..ef012f62 100644 --- a/tests/testthat/_snaps/quarto.md +++ b/tests/testthat/_snaps/quarto.md @@ -22,14 +22,14 @@ Stack trace: at throwInputNotFound () at findInputs () - at eventLoopTick (ext:core/01_core.js:175:7) + at eventLoopTick (ext:core/01_core.js:178:7) at async findChapters () at async bookRenderItems () at async Object.bookProjectConfig [as config] () at async projectContext () at async inspectConfig () - at async Command.actionHandler () - at async Command.execute () + at async _Command.actionHandler () + at async _Command.execute () Caused by error: ! System command 'quarto' failed From 8b904c9744d1ef66d566403f65462716cfe6a751 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 9 May 2025 18:03:38 +0200 Subject: [PATCH 17/20] don't try to keep stack trace in snapshot, just hide it --- tests/testthat/_snaps/quarto.md | 11 +---------- tests/testthat/helper.R | 35 ++++++++++++++++++++++++++++++++- tests/testthat/test-quarto.R | 3 ++- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/tests/testthat/_snaps/quarto.md b/tests/testthat/_snaps/quarto.md index ef012f62..93f6a882 100644 --- a/tests/testthat/_snaps/quarto.md +++ b/tests/testthat/_snaps/quarto.md @@ -20,16 +20,7 @@ ERROR: Book chapter 'intro.qmd' not found Stack trace: - at throwInputNotFound () - at findInputs () - at eventLoopTick (ext:core/01_core.js:178:7) - at async findChapters () - at async bookRenderItems () - at async Object.bookProjectConfig [as config] () - at async projectContext () - at async inspectConfig () - at async _Command.actionHandler () - at async _Command.execute () + Caused by error: ! System command 'quarto' failed diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index 7272f95c..19f210b2 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -134,7 +134,8 @@ expect_snapshot_qmd_output <- function(name, input, output_file = NULL, ...) { transform_quarto_cli_in_output <- function( full_path = FALSE, version = FALSE, - dir_only = FALSE + dir_only = FALSE, + hide_stack = FALSE ) { hide_path <- function(lines, real_path) { gsub( @@ -147,6 +148,38 @@ transform_quarto_cli_in_output <- function( return( function(lines) { + if (hide_stack) { + # Hide possible stack first + stack_trace_index <- which(grepl("\\s*Stack trace\\:", lines)) + if ( + length(stack_trace_index) > 0 && stack_trace_index < length(lines) + ) { + at_lines_indices <- which(grepl("^\\s*at ", lines)) + at_lines_after_stack <- at_lines_indices[ + at_lines_indices > stack_trace_index + ] + if (length(at_lines_after_stack) > 0) { + # Find the continuous sequence (no gaps) + gaps <- diff(at_lines_after_stack) > 1 + end_pos <- if (any(gaps)) which(gaps)[1] else + length(at_lines_after_stack) + consecutive_indices <- at_lines_after_stack[1:end_pos] + + stack_line <- lines[stack_trace_index] + indentation <- regmatches(stack_line, regexpr("^\\s*", stack_line)) + lines[consecutive_indices[1]] <- paste0( + indentation, + "" + ) + if (length(consecutive_indices) > 1) { + lines <- lines[ + -consecutive_indices[2:length(consecutive_indices)] + ] + } + } + } + } + if (full_path) { quarto_found <- find_quarto() if (dir_only) { diff --git a/tests/testthat/test-quarto.R b/tests/testthat/test-quarto.R index f98da147..c900fb65 100644 --- a/tests/testthat/test-quarto.R +++ b/tests/testthat/test-quarto.R @@ -29,7 +29,8 @@ test_that("quarto_run report full quarto cli error message", { quarto_inspect(), transform = transform_quarto_cli_in_output( full_path = TRUE, - dir_only = TRUE + dir_only = TRUE, + hide_stack = TRUE ) ) }) From 9946e2569a211f2d01cfb37c7809e77d4fbb7f6a Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 9 May 2025 18:44:39 +0200 Subject: [PATCH 18/20] Add missing reference page to website --- _pkgdown.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/_pkgdown.yml b/_pkgdown.yml index 916b2b91..9436a263 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -21,11 +21,12 @@ reference: - title: "Extensions" desc: > - This functions enable you to install extensions and use template from extensions in your folder and projects. + These functions enable you to manage Quarto extensions and use template from extensions in your folder and projects. More about Quarto extensions at contents: - - starts_with("quarto_add") - - starts_with("quarto_use") + - ends_with("_extension") + - ends_with("_extensions") + - quarto_use_template - title: "Projects" desc: > From 8dcd5d161714b35f9fa4be3bcb6169525ce73278 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 9 May 2025 18:59:41 +0200 Subject: [PATCH 19/20] update NEWS --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index d2f50d98..3da14390 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # quarto (development version) -- Add several new wrapper function: +- Add several new wrapper function (thanks, @parmsam, #192): - `quarto_list_extensions()` to list installed extensions using `quarto list extensions` - `quarto_remove_extension()` to remove an installed extension using `quarto remove extensions` - `quarto_update_extension()` to update an installed extension using `quarto update extensions` From 0d1952a52588168dc8ca5c15ef76e22e52602784 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 9 May 2025 18:59:57 +0200 Subject: [PATCH 20/20] Bump version [skip ci] --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 7771968b..fe958dc8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: quarto Title: R Interface to 'Quarto' Markdown Publishing System -Version: 1.4.4.9007 +Version: 1.4.4.9008 Authors@R: c( person("JJ", "Allaire", , "jj@posit.co", role = "aut", comment = c(ORCID = "0000-0003-0174-9868")),