Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ Imports:
utils,
xfun,
yaml (>= 2.3.10)
Suggests:
bslib,
Suggests:
brand.yml,
bslib (>= 0.9.0),
callr,
curl,
dplyr,
Expand Down
4 changes: 4 additions & 0 deletions R/list.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ quarto_list_extensions <- function() {
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)
# Quarto CLI prepends a "Quarto version: X.Y.Z" line to stderr when log level
# is DEBUG (auto-enabled in GHA debug mode). Strip it so read.table() can use
# the real header row. See https://github.com/quarto-dev/quarto-cli/issues/14532.
stderr_cleaned <- sub("^Quarto version:[^\n]*\n", "", stderr_cleaned)
if (grepl("No extensions are installed", stderr_cleaned)) {
invisible()
} else {
Expand Down
28 changes: 28 additions & 0 deletions R/quarto-args.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,31 @@ append_cli_args <- function(new, append_to = NULL, after = length(append_to)) {
}
new
}

cli_arg_metadata <- function(metadata = NULL, metadata_file = NULL) {
if (is.null(metadata) && is.null(metadata_file)) {
return(list(args = character(), tmp_file = NULL))
}
if (is.null(metadata)) {
return(list(
args = c("--metadata-file", metadata_file),
tmp_file = NULL
))
}
if (!is.null(metadata_file)) {
file_content <- yaml::read_yaml(metadata_file, eval.expr = FALSE)
metadata <- merge_list(file_content, metadata)
}
tmp <- tempfile(pattern = "quarto-meta", fileext = ".yml")
# Remove tmp if write_yaml() errors before we hand the path to the
# caller; the caller (e.g. quarto_render()) registers its own
# on.exit cleanup for the success path.
success <- FALSE
on.exit(if (!success) unlink(tmp), add = TRUE)
write_yaml(metadata, tmp)
success <- TRUE
list(
args = c("--metadata-file", tmp),
tmp_file = tmp
)
}
18 changes: 16 additions & 2 deletions R/remove.R
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,23 @@ quarto_remove_extension <- function(
rlang::check_required(extension)

installed_extensions <- quarto_list_extensions()
if (is.null(installed_extensions)) {
# TODO: adapt based on https://github.com/quarto-dev/quarto-r/issues/301
if (quarto_available(max = "1.9")) {
if (is.null(installed_extensions)) {
if (!quiet) {
cli::cli_alert_warning("No extensions installed.")
}
return(invisible(FALSE))
}
}

# Check extension is installed
is_installed <- extension %in% installed_extensions$Id
if (!is_installed) {
if (!quiet) {
cli::cli_alert_warning("No extensions installed.")
cli::cli_alert_warning(
"{.str {extension}} is not among installed extensions."
)
}
return(invisible(FALSE))
}
Expand Down
16 changes: 4 additions & 12 deletions R/render.R
Original file line number Diff line number Diff line change
Expand Up @@ -197,18 +197,10 @@ quarto_render <- function(
args <- c(args, "--cache-refresh")
}
# metadata to pass to quarto render
if (!is.null(metadata)) {
# We merge meta if there is metadata_file passed
if (!missing(metadata_file)) {
file_content <- yaml::read_yaml(metadata_file, eval.expr = FALSE)
metadata <- merge_list(file_content, metadata)
}
meta_file <- tempfile(pattern = "quarto-meta", fileext = ".yml")
on.exit(unlink(meta_file), add = TRUE)
write_yaml(metadata, meta_file)
args <- c(args, "--metadata-file", meta_file)
} else if (!missing(metadata_file)) {
args <- c(args, "--metadata-file", metadata_file)
meta_result <- cli_arg_metadata(metadata, metadata_file)
args <- c(args, meta_result$args)
if (!is.null(meta_result$tmp_file)) {
on.exit(unlink(meta_result$tmp_file), add = TRUE)
}
if (isTRUE(debug)) {
args <- c(args, "--debug")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Pandoc
Meta
{ unMeta =
fromList
[ ( "custom-key" , MetaInlines [ Str "overridden" ] )
, ( "title" , MetaInlines [ Str "Doc" ] )
, ( "yml-only-key" , MetaInlines [ Str "kept" ] )
]
}
[]
4 changes: 2 additions & 2 deletions tests/testthat/_snaps/quarto.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Caused by error:
x Error returned by quarto CLI.
i Rerun with `quiet = FALSE` to see the full error message.
Caused by error:
Caused by error in `processx::run()`:
! System command 'quarto' failed

# quarto_run report full quarto cli error message
Expand All @@ -26,7 +26,7 @@
Stack trace:
<stack trace>

Caused by error:
Caused by error in `processx::run()`:
! System command 'quarto' failed

# is_using_quarto correctly check directory
Expand Down
8 changes: 5 additions & 3 deletions tests/testthat/_snaps/remove.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# Removing an extension

Code
expect_false(quarto_remove_extension("quarto-ext/fontawesome", no_prompt = TRUE))
withr::with_dir(wd, expect_false(quarto_remove_extension(
"quarto-ext/fontawesome", no_prompt = TRUE)))
Message
! No extensions installed.
! "quarto-ext/fontawesome" is not among installed extensions.

---

Code
expect_true(quarto_remove_extension("quarto-ext/fontawesome", no_prompt = TRUE))
withr::with_dir(wd, expect_true(quarto_remove_extension(
"quarto-ext/fontawesome", no_prompt = TRUE)))
Message
v Extension `quarto-ext/fontawesome` successfully removed.

6 changes: 0 additions & 6 deletions tests/testthat/_snaps/render/metadata-file.test.out

This file was deleted.

10 changes: 0 additions & 10 deletions tests/testthat/_snaps/render/metadata-merged.test.out

This file was deleted.

6 changes: 0 additions & 6 deletions tests/testthat/_snaps/render/metadata.test.out

This file was deleted.

5 changes: 5 additions & 0 deletions tests/testthat/helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ transform_quarto_cli_in_output <- function(

return(
function(lines) {
# Quarto CLI prepends "Quarto version: X.Y.Z" on stderr under DEBUG log
# level (auto-on in GHA debug mode). Strip so snapshots stay stable.
# See https://github.com/quarto-dev/quarto-cli/issues/14532.
lines <- lines[!grepl("^\\s*Quarto version:\\s+[0-9]", lines)]

if (hide_stack) {
# Hide possible stack first
stack_trace_index <- which(grepl("\\s*Stack trace\\:", lines))
Expand Down
8 changes: 5 additions & 3 deletions tests/testthat/test-list.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ test_that("Listing extensions", {
skip_if_offline("github.com")
qmd <- local_qmd_file(c("content"))
withr::local_dir(dirname(qmd))
expect_null(quarto_list_extensions())
# TODO: Adapt depending on https://github.com/quarto-dev/quarto-r/issues/301
# expect_null(quarto_list_extensions())
default <- quarto_list_extensions()$Id
quarto_add_extension("quarto-ext/fontawesome", no_prompt = TRUE, quiet = TRUE)
expect_true(dir.exists("_extensions/quarto-ext/fontawesome"))
expect_identical(
quarto_list_extensions()$Id,
setdiff(quarto_list_extensions()$Id, default),
c("quarto-ext/fontawesome")
)
quarto_add_extension("quarto-ext/lightbox", no_prompt = TRUE, quiet = TRUE)
expect_true(dir.exists("_extensions/quarto-ext/lightbox"))
expect_identical(
quarto_list_extensions()$Id,
setdiff(quarto_list_extensions()$Id, default),
c("quarto-ext/fontawesome", "quarto-ext/lightbox")
)
})
110 changes: 110 additions & 0 deletions tests/testthat/test-metadata-args.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
test_that("cli_arg_metadata returns empty args when both inputs NULL", {
result <- cli_arg_metadata(metadata = NULL, metadata_file = NULL)
expect_identical(result$args, character())
expect_null(result$tmp_file)
})

test_that("cli_arg_metadata with metadata only writes temp YAML and uses --metadata-file", {
skip_if_not_installed("withr")
skip_if_not_installed("yaml")
result <- cli_arg_metadata(
metadata = list(title = "test", lang = "fr"),
metadata_file = NULL
)
withr::defer(unlink(result$tmp_file))

expect_length(result$args, 2L)
expect_identical(result$args[1], "--metadata-file")
expect_true(file.exists(result$args[2]))
expect_identical(result$args[2], result$tmp_file)

written <- yaml::read_yaml(result$tmp_file)
expect_identical(written, list(title = "test", lang = "fr"))
})

test_that("cli_arg_metadata with metadata_file only passes path verbatim", {
skip_if_not_installed("withr")
yml <- withr::local_tempfile(fileext = ".yml")
yaml::write_yaml(list(title = "file-title"), yml)

result <- cli_arg_metadata(metadata = NULL, metadata_file = yml)
expect_identical(result$args, c("--metadata-file", yml))
expect_null(result$tmp_file)
})

test_that("cli_arg_metadata merges metadata over metadata_file with metadata winning", {
skip_if_not_installed("withr")
skip_if_not_installed("yaml")
yml <- withr::local_tempfile(fileext = ".yml")
yaml::write_yaml(list(title = "from-file", other = "kept"), yml)

result <- cli_arg_metadata(
metadata = list(title = "from-list", new = "added"),
metadata_file = yml
)
withr::defer(unlink(result$tmp_file))

expect_identical(result$args[1], "--metadata-file")
expect_identical(result$args[2], result$tmp_file)

written <- yaml::read_yaml(result$tmp_file)
expect_identical(
written,
list(title = "from-list", other = "kept", new = "added")
)
})

test_that("cli_arg_metadata preserves nested list structure in temp YAML", {
skip_if_not_installed("withr")
skip_if_not_installed("yaml")
nested <- list(format = list(html = list(`toc-title` = "Custom")))
result <- cli_arg_metadata(metadata = nested, metadata_file = NULL)
withr::defer(unlink(result$tmp_file))

written <- yaml::read_yaml(result$tmp_file)
expect_identical(written, nested)
})

test_that("cli_arg_metadata removes temp file if write_yaml errors", {
skip_if_not_installed("withr")
captured_tmp <- NULL
local_mocked_bindings(
write_yaml = function(x, file) {
captured_tmp <<- file
file.create(file)
stop("simulated write failure")
}
)

expect_error(
cli_arg_metadata(metadata = list(title = "x"), metadata_file = NULL),
"simulated write failure"
)
expect_false(is.null(captured_tmp))
expect_false(file.exists(captured_tmp))
})

test_that("cli_arg_metadata merge is shallow (top-level keys replaced wholesale)", {
# Documents current behavior - metadata replaces top-level keys entirely.
# If we ever switch to deep merge, this test should be updated, not silently broken.
skip_if_not_installed("withr")
skip_if_not_installed("yaml")
yml <- withr::local_tempfile(fileext = ".yml")
yaml::write_yaml(
list(format = list(html = list(toc = TRUE, `toc-title` = "Original"))),
yml
)

result <- cli_arg_metadata(
metadata = list(format = list(html = list(`toc-title` = "Overridden"))),
metadata_file = yml
)
withr::defer(unlink(result$tmp_file))

written <- yaml::read_yaml(result$tmp_file)
# toc=TRUE from file is LOST because metadata's `format` replaces wholesale
expect_identical(
written,
list(format = list(html = list(`toc-title` = "Overridden")))
)
})
Loading
Loading