diff --git a/NAMESPACE b/NAMESPACE index 9e9fa9f..8a7e6d5 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 new file mode 100644 index 0000000..6aa76d8 --- /dev/null +++ b/R/list.R @@ -0,0 +1,33 @@ +#' List Installed Quarto extensions +#' +#' List Quarto Extensions in this folder or project by running `quarto list` +#' +#' @inheritParams quarto_render +#' +#' @examples +#' \dontrun{ +#' # List Quarto Extensions in this folder or project +#' 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_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(utils::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 0000000..cc8f54b --- /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 0000000..1d6dee8 --- /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 update, 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, ...) +} diff --git a/R/utils-prompt.R b/R/utils-prompt.R index fb0c00f..1407baf 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,33 @@ check_extension_approval <- function(no_prompt = FALSE, what = "Something", see_ cli::cli_inform("{what} not installed.") return(invisible(FALSE)) } + return(invisible(TRUE)) } } + +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 +is_interactive <- function(...) { + rlang::is_interactive(...) +} diff --git a/man/quarto_list_extensions.Rd b/man/quarto_list_extensions.Rd new file mode 100644 index 0000000..aba1743 --- /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 df6c011..8e6ae6f 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 0000000..e296ba1 --- /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 0000000..372b1eb --- /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") +} + +} diff --git a/tests/testthat/test-list.R b/tests/testthat/test-list.R new file mode 100644 index 0000000..02afda8 --- /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 0000000..bfe652f --- /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 0000000..c1867a2 --- /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")) +}) diff --git a/tests/testthat/test-utils-prompt.R b/tests/testthat/test-utils-prompt.R new file mode 100644 index 0000000..52eaa4e --- /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") + }) +})