Skip to content

Commit 4834682

Browse files
authored
Better support of YAML 1.2 for written YAML by this package using yaml R package following 1.1 spec (#272)
* use yaml::verbatim_logical directly * Handle string with leading 0 parsed as octal in Quarto's YAML 1.2 - Add function to quote strings following yaml R package pattern - Automatically quote the string that could be considered as octal in YAML 1.2 - Add tests for the function * Add to NEWS and bump version * Add to pkgdown * Use yaml package in example * Adapt snapshot test for width in CLI
1 parent a89f0cd commit 4834682

File tree

13 files changed

+393
-36
lines changed

13 files changed

+393
-36
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Imports:
3131
tools,
3232
utils,
3333
xfun,
34-
yaml
34+
yaml (>= 2.3.10)
3535
Suggests:
3636
bslib,
3737
callr,

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export(theme_colors_gt)
4545
export(theme_colors_plotly)
4646
export(theme_colors_thematic)
4747
export(write_yaml_metadata_block)
48+
export(yaml_quote_string)
4849
import(rlang)
4950
importFrom(cli,cli_abort)
5051
importFrom(cli,cli_inform)

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# quarto (development version)
22

3+
- Improved YAML 1.2 compatibility features to ensure proper parsing by Quarto's js-yaml parser.
4+
- The `yaml_quote_string()` function allows explicit control over string quoting in YAML output.
5+
- `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`.
6+
- This change also benefits other functions writing YAML like `quarto_render()` when using `metadata=` or `execute_params=` arguments.
7+
38
- Package is now licenced MIT like Quarto CLI.
49

510
- Added `has_parameters()` function to detect whether Quarto documents use parameters. The function works with both knitr and Jupyter engines: for knitr documents (.qmd), it checks for a "params" field in the document metadata; for Jupyter notebooks (.ipynb), it detects cells tagged with "parameters" using papermill convention. This enables programmatic identification of parameterized documents for automated workflows and document processing (#245).

R/metadata.R

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,44 @@
2727
#' If no metadata is provided (empty `...` and `NULL` or empty `.list`),
2828
#' the function returns `NULL` without generating any output.
2929
#'
30-
#' **Important**: When using this function in Quarto documents, you must set
31-
#' the chunk option `output: asis` (or `#| output: asis`) for the metadata
32-
#' block to be properly processed by Quarto.
33-
#'
3430
#' This addresses the limitation where Quarto metadata must be static and
3531
#' cannot be set dynamically from R code during document rendering.
3632
#'
33+
#' ## YAML 1.2 Compatibility:
34+
#' To ensure compatibility with Quarto's YAML 1.2 parser (js-yaml), the function
35+
#' automatically handles two key differences between R's yaml package (YAML 1.1)
36+
#' and YAML 1.2:
37+
#'
38+
#' ### Boolean values:
39+
#' R logical values (`TRUE`/`FALSE`) are converted to lowercase
40+
#' YAML 1.2 format (`true`/`false`) using [yaml::verbatim_logical()]. This prevents
41+
#' YAML 1.1 boolean representations like `yes`/`no` from being used.
42+
#'
43+
#' ### String quoting:
44+
#' Strings with leading zeros that contain digits 8 or 9 (like `"029"`, `"089"`)
45+
#' are automatically quoted to prevent them from being parsed as octal numbers,
46+
#' which would result in data corruption (e.g., `"029"` becoming `29`).
47+
#' Valid octal numbers containing only digits 0-7 (like `"0123"`) are handled
48+
#' by the underlying \pkg{yaml} package.
49+
#'
50+
#' For manual control over string quoting behavior, use [yaml_quote_string()].
51+
#'
52+
#' ## Quarto Usage:
53+
#' To use this function in a Quarto document, create an R code chunk with
54+
#' the `output: asis` option:
55+
#'
56+
#' ```
57+
#' ```{r}
58+
#' #| output: asis
59+
#' write_yaml_metadata_block(admin = TRUE, version = "1.0")
60+
#' ```
61+
#' ```
62+
#'
63+
#' Without the `output: asis` option, the YAML metadata block will be
64+
#' displayed as text rather than processed as metadata by Quarto.
65+
#'
66+
#' @inherit yaml_character_handler seealso
67+
#'
3768
#' @examples
3869
#' \dontrun{
3970
#' # In a Quarto document R chunk with `#| output: asis`:
@@ -47,6 +78,12 @@
4778
#' timestamp = Sys.Date()
4879
#' )
4980
#'
81+
#' # Strings with leading zeros are automatically quoted for YAML 1.2 compatibility
82+
#' write_yaml_metadata_block(
83+
#' zip_code = "029", # Automatically quoted as "029"
84+
#' build_id = "0123" # Quoted by yaml package (valid octal)
85+
#' )
86+
#'
5087
#' # Use with .list parameter
5188
#' metadata_list <- list(version = "1.0", debug = FALSE)
5289
#' write_yaml_metadata_block(.list = metadata_list)
@@ -65,20 +102,8 @@
65102
#' # :::
66103
#' }
67104
#'
68-
#' @section Quarto Usage:
69-
#' To use this function in a Quarto document, create an R code chunk with
70-
#' the `output: asis` option:
71-
#'
72-
#' ```
73-
#' ```{r}
74-
#' #| output: asis
75-
#' write_yaml_metadata_block(admin = TRUE, version = "1.0")
76-
#' ```
77-
#' ```
78-
#'
79-
#' Without the `output: asis` option, the YAML metadata block will be
80-
#' displayed as text rather than processed as metadata by Quarto.
81-
#'
105+
#' @seealso [yaml_quote_string()] for explicitly controlling which strings are quoted
106+
#' in YAML output when you encounter edge cases that need manual handling.
82107
#'
83108
#' @export
84109
write_yaml_metadata_block <- function(..., .list = NULL) {

R/utils.R

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,103 @@ relative_to_wd <- function(path) {
33
rmarkdown::relative_to(getwd(), path)
44
}
55

6+
#' Add quoted attribute to strings for YAML output
7+
#'
8+
#' This function allows users to explicitly mark strings that should be quoted
9+
#' in YAML output, giving full control over quoting behavior.
10+
#'
11+
#' This is particularly useful for special values that might be misinterpreted
12+
#' as \pkg{yaml} uses YAML 1.1 and Quarto expects YAML 1.2.
13+
#'
14+
#' The `quoted` attribute is a convention used by [yaml::as.yaml()]
15+
#'
16+
#' @param x A character vector or single string
17+
#' @return The input with quoted attributes applied
18+
#' @examples
19+
#' yaml::as.yaml(list(id = yaml_quote_string("1.0")))
20+
#' yaml::as.yaml(list(id = "1.0"))
21+
#'
22+
#' @export
23+
yaml_quote_string <- function(x) {
24+
if (!is.character(x)) {
25+
cli::cli_abort("yaml_quote_string() only works with character vectors")
26+
}
27+
28+
result <- vector("list", length(x))
29+
for (i in seq_along(x)) {
30+
val <- x[i]
31+
attr(val, "quoted") <- TRUE
32+
result[[i]] <- val
33+
}
34+
35+
if (length(result) == 1) {
36+
return(result[[1]])
37+
}
38+
39+
result
40+
}
41+
42+
is_valid_yaml11_octal <- function(val) {
43+
# Check if the value is a valid YAML 1.1 octal number
44+
# Valid octals are 0o[0-7]+, but we only quote those with leading zeros
45+
# that contain digits 8 or 9, which are invalid in octal, as they
46+
# would not be quoted already by the R yaml package.
47+
# YAML 1.1 spec for int: https://yaml.org/type/int.html
48+
invalid <- !is.na(val) &&
49+
val != "" &&
50+
val != "0" &&
51+
grepl("^0[0-9]+$", val) &&
52+
grepl("[89]", val)
53+
!invalid
54+
}
55+
56+
#' YAML character handler for YAML 1.1 to 1.2 compatibility
57+
#'
58+
#' This handler bridges the gap between R's yaml package (YAML 1.1) and
59+
#' js-yaml (YAML 1.2) by quoting strings with leading zeros that would be
60+
#' misinterpreted as octal numbers.
61+
#'
62+
#' According to YAML 1.1 spec, octal integers are `0o[0-7]+`. The R yaml
63+
#' package only quotes valid octals (containing only digits 0-7), but js-yaml
64+
#' attempts to parse ANY leading zero string as octal, causing data corruption
65+
#' for invalid octals like "029" → 29.
66+
#'
67+
#' @seealso [YAML 1.1 int spec](https://yaml.org/type/int.html)
68+
#'
69+
#' @param x A character vector
70+
#' @return The input with quoted attributes applied where needed
71+
#' @keywords internal
72+
yaml_character_handler <- function(x) {
73+
apply_quote <- function(x) {
74+
# Skip if already has quoted attribute (user control via yaml_quote_string())
75+
if (!is.null(attr(x, "quoted")) && attr(x, "quoted")) {
76+
return(x)
77+
}
78+
# Quote leading zero strings that are NOT valid octals (YAML 1.1 vs 1.2 gap)
79+
# Valid octals contain only digits 0-7, invalid ones contain 8 or 9
80+
if (!(is_valid_yaml11_octal(x))) {
81+
attr(x, "quoted") <- TRUE
82+
}
83+
return(x)
84+
}
85+
# For single elements, process directly
86+
if (length(x) == 1) {
87+
return(apply_quote(x))
88+
} else {
89+
# For vectors, process each element and return as list to preserve attributes
90+
result <- vector("list", length(x))
91+
for (i in seq_along(x)) {
92+
result[[i]] <- apply_quote(x[i])
93+
}
94+
return(result)
95+
}
96+
}
97+
698
# Specific YAML handlers
799
# as quarto expects YAML 1.2 and yaml R package supports 1.1
8100
yaml_handlers <- list(
9-
# Handle yes/no from 1.1 to 1.2
10-
# https://github.com/vubiostat/r-yaml/issues/131
11-
logical = function(x) {
12-
value <- ifelse(x, "true", "false")
13-
structure(value, class = "verbatim")
14-
}
101+
logical = yaml::verbatim_logical,
102+
character = yaml_character_handler
15103
)
16104

17105
#' @importFrom yaml as.yaml

_pkgdown.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,17 @@ reference:
6767
contents:
6868
- starts_with("tbl_qmd_")
6969

70+
- title: "YAML Helpers"
71+
desc: >
72+
These functions are used to help with YAML metadata in Quarto documents:
73+
contents:
74+
- write_yaml_metadata_block
75+
- yaml_quote_string
76+
7077
- title: "Miscellaneous"
7178
desc: >
7279
These functions are used to help with Quarto documents and projects:
7380
contents:
74-
- write_yaml_metadata_block
7581
- add_spin_preamble
7682
- qmd_to_r_script
7783
- detect_bookdown_crossrefs

man/write_yaml_metadata_block.Rd

Lines changed: 39 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/yaml_character_handler.Rd

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/yaml_quote_string.Rd

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/convert-bookdown.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -486,17 +486,16 @@
486486
-- File: '<test_file_basename>' --
487487
488488
-- Theorem Block Unlabeled references:
489-
* Line 2 ('<test_file_basename>:2'): `` ```{theorem name="Pythagorean theorem"}
490-
`` -> `Manual conversion required: Use ::: {#thm-<id>} syntax. See
489+
* Line 2 ('<test_file_basename>:2'): `` ```{theorem name="Pythagorean theorem"} `` -> `Manual
490+
conversion required: Use ::: {#thm-<id>} syntax. See
491491
https://quarto.org/docs/authoring/cross-references.html#theorems-and-proofs`
492492
493493
i Summary of conversion requirements:
494494
* 1 Theorem Block Unlabeled reference
495495
! Theorem environments require manual restructuring
496496
Bookdown old syntax WITHOUT label: ```{theorem chunk_name}
497497
Quarto syntax: :::{#thm-label}
498-
See:
499-
<https://quarto.org/docs/authoring/cross-references.html#theorems-and-proofs>
498+
See: <https://quarto.org/docs/authoring/cross-references.html#theorems-and-proofs>
500499
501500

502501
# detects theorem div syntax correctly

0 commit comments

Comments
 (0)