Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: quarto
Title: R Interface to 'Quarto' Markdown Publishing System
Version: 1.4.4.9027
Version: 1.4.4.9028
Authors@R: c(
person("JJ", "Allaire", , "[email protected]", role = "aut",
comment = c(ORCID = "0000-0003-0174-9868")),
Expand Down Expand Up @@ -31,7 +31,7 @@ Imports:
tools,
utils,
xfun,
yaml
yaml (>= 2.3.10)
Suggests:
bslib,
callr,
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export(theme_colors_gt)
export(theme_colors_plotly)
export(theme_colors_thematic)
export(write_yaml_metadata_block)
export(yaml_quote_string)
import(rlang)
importFrom(cli,cli_abort)
importFrom(cli,cli_inform)
Expand Down
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# quarto (development version)

- Improved YAML 1.2 compatibility features to ensure proper parsing by Quarto's js-yaml parser.
- The `yaml_quote_string()` function allows explicit control over string quoting in YAML output.
- `write_yaml_metadata_block()` automatically handles data corruption prevention from leading zero strings like `"029"` that would be misinterpreted as octal numbers (becoming `29`) (thanks, @Mosk915, quarto-dev/quarto-cli#12736, #242). Boolean values are also correctly formatted as lowercase (`true`/`false`) instead of YAML 1.1 variants like `yes`/`no`.
- This change also benefits other functions writing YAML like `quarto_render()` when using `metadata=` or `execute_params=` arguments.

- Package is now licenced MIT like Quarto CLI.

- Added `detect_bookdown_crossrefs()` function to help users migrate from bookdown to Quarto by identifying cross-references that need manual conversion. The function scans R Markdown or Quarto files to detect bookdown-specific cross-reference syntax (like `\@ref(fig:label)` and `(\#eq:label)`) and provides detailed guidance on converting them to Quarto syntax (like `@fig-label` and `{#eq-label}`). It offers both compact and verbose reporting modes, with context-aware warnings that only show syntax patterns actually found in your files.
Expand Down
61 changes: 43 additions & 18 deletions R/metadata.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,44 @@
#' If no metadata is provided (empty `...` and `NULL` or empty `.list`),
#' the function returns `NULL` without generating any output.
#'
#' **Important**: When using this function in Quarto documents, you must set
#' the chunk option `output: asis` (or `#| output: asis`) for the metadata
#' block to be properly processed by Quarto.
#'
#' This addresses the limitation where Quarto metadata must be static and
#' cannot be set dynamically from R code during document rendering.
#'
#' ## YAML 1.2 Compatibility:
#' To ensure compatibility with Quarto's YAML 1.2 parser (js-yaml), the function
#' automatically handles two key differences between R's yaml package (YAML 1.1)
#' and YAML 1.2:
#'
#' ### Boolean values:
#' R logical values (`TRUE`/`FALSE`) are converted to lowercase
#' YAML 1.2 format (`true`/`false`) using [yaml::verbatim_logical()]. This prevents
#' YAML 1.1 boolean representations like `yes`/`no` from being used.
#'
#' ### String quoting:
#' Strings with leading zeros that contain digits 8 or 9 (like `"029"`, `"089"`)
#' are automatically quoted to prevent them from being parsed as octal numbers,
#' which would result in data corruption (e.g., `"029"` becoming `29`).
#' Valid octal numbers containing only digits 0-7 (like `"0123"`) are handled
#' by the underlying \pkg{yaml} package.
#'
#' For manual control over string quoting behavior, use [yaml_quote_string()].
#'
#' ## Quarto Usage:
#' To use this function in a Quarto document, create an R code chunk with
#' the `output: asis` option:
#'
#' ```
#' ```{r}
#' #| output: asis
#' write_yaml_metadata_block(admin = TRUE, version = "1.0")
#' ```
#' ```
#'
#' Without the `output: asis` option, the YAML metadata block will be
#' displayed as text rather than processed as metadata by Quarto.
#'
#' @inherit yaml_character_handler seealso
#'
#' @examples
#' \dontrun{
#' # In a Quarto document R chunk with `#| output: asis`:
Expand All @@ -47,6 +78,12 @@
#' timestamp = Sys.Date()
#' )
#'
#' # Strings with leading zeros are automatically quoted for YAML 1.2 compatibility
#' write_yaml_metadata_block(
#' zip_code = "029", # Automatically quoted as "029"
#' build_id = "0123" # Quoted by yaml package (valid octal)
#' )
#'
#' # Use with .list parameter
#' metadata_list <- list(version = "1.0", debug = FALSE)
#' write_yaml_metadata_block(.list = metadata_list)
Expand All @@ -65,20 +102,8 @@
#' # :::
#' }
#'
#' @section Quarto Usage:
#' To use this function in a Quarto document, create an R code chunk with
#' the `output: asis` option:
#'
#' ```
#' ```{r}
#' #| output: asis
#' write_yaml_metadata_block(admin = TRUE, version = "1.0")
#' ```
#' ```
#'
#' Without the `output: asis` option, the YAML metadata block will be
#' displayed as text rather than processed as metadata by Quarto.
#'
#' @seealso [yaml_quote_string()] for explicitly controlling which strings are quoted
#' in YAML output when you encounter edge cases that need manual handling.
#'
#' @export
write_yaml_metadata_block <- function(..., .list = NULL) {
Expand Down
100 changes: 94 additions & 6 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,103 @@ relative_to_wd <- function(path) {
rmarkdown::relative_to(getwd(), path)
}

#' Add quoted attribute to strings for YAML output
#'
#' This function allows users to explicitly mark strings that should be quoted
#' in YAML output, giving full control over quoting behavior.
#'
#' This is particularly useful for special values that might be misinterpreted
#' as \pkg{yaml} uses YAML 1.1 and Quarto expects YAML 1.2.
#'
#' The `quoted` attribute is a convention used by [yaml::as.yaml()]
#'
#' @param x A character vector or single string
#' @return The input with quoted attributes applied
#' @examples
#' yaml::as.yaml(list(id = yaml_quote_string("1.0")))
#' yaml::as.yaml(list(id = "1.0"))
#'
#' @export
yaml_quote_string <- function(x) {
if (!is.character(x)) {
cli::cli_abort("yaml_quote_string() only works with character vectors")
}

result <- vector("list", length(x))
for (i in seq_along(x)) {
val <- x[i]
attr(val, "quoted") <- TRUE
result[[i]] <- val
}

if (length(result) == 1) {
return(result[[1]])
}

result
}

is_valid_yaml11_octal <- function(val) {
# Check if the value is a valid YAML 1.1 octal number
# Valid octals are 0o[0-7]+, but we only quote those with leading zeros
# that contain digits 8 or 9, which are invalid in octal, as they
# would not be quoted already by the R yaml package.
# YAML 1.1 spec for int: https://yaml.org/type/int.html
invalid <- !is.na(val) &&
val != "" &&
val != "0" &&
grepl("^0[0-9]+$", val) &&
grepl("[89]", val)
!invalid
}

#' YAML character handler for YAML 1.1 to 1.2 compatibility
#'
#' This handler bridges the gap between R's yaml package (YAML 1.1) and
#' js-yaml (YAML 1.2) by quoting strings with leading zeros that would be
#' misinterpreted as octal numbers.
#'
#' According to YAML 1.1 spec, octal integers are `0o[0-7]+`. The R yaml
#' package only quotes valid octals (containing only digits 0-7), but js-yaml
#' attempts to parse ANY leading zero string as octal, causing data corruption
#' for invalid octals like "029" → 29.
#'
#' @seealso [YAML 1.1 int spec](https://yaml.org/type/int.html)
#'
#' @param x A character vector
#' @return The input with quoted attributes applied where needed
#' @keywords internal
yaml_character_handler <- function(x) {
apply_quote <- function(x) {
# Skip if already has quoted attribute (user control via yaml_quote_string())
if (!is.null(attr(x, "quoted")) && attr(x, "quoted")) {
return(x)
}
# Quote leading zero strings that are NOT valid octals (YAML 1.1 vs 1.2 gap)
# Valid octals contain only digits 0-7, invalid ones contain 8 or 9
if (!(is_valid_yaml11_octal(x))) {
attr(x, "quoted") <- TRUE
}
return(x)
}
# For single elements, process directly
if (length(x) == 1) {
return(apply_quote(x))
} else {
# For vectors, process each element and return as list to preserve attributes
result <- vector("list", length(x))
for (i in seq_along(x)) {
result[[i]] <- apply_quote(x[i])
}
return(result)
}
}

# Specific YAML handlers
# as quarto expects YAML 1.2 and yaml R package supports 1.1
yaml_handlers <- list(
# Handle yes/no from 1.1 to 1.2
# https://github.com/vubiostat/r-yaml/issues/131
logical = function(x) {
value <- ifelse(x, "true", "false")
structure(value, class = "verbatim")
}
logical = yaml::verbatim_logical,
character = yaml_character_handler
)

#' @importFrom yaml as.yaml
Expand Down
8 changes: 7 additions & 1 deletion _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,17 @@ reference:
contents:
- starts_with("tbl_qmd_")

- title: "YAML Helpers"
desc: >
These functions are used to help with YAML metadata in Quarto documents:
contents:
- write_yaml_metadata_block
- yaml_quote_string

- title: "Miscellaneous"
desc: >
These functions are used to help with Quarto documents and projects:
contents:
- write_yaml_metadata_block
- add_spin_preamble
- qmd_to_r_script
- detect_bookdown_crossrefs
45 changes: 39 additions & 6 deletions man/write_yaml_metadata_block.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions man/yaml_character_handler.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions man/yaml_quote_string.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading