diff --git a/DESCRIPTION b/DESCRIPTION index 64c0f754..536414ba 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: quarto Title: R Interface to 'Quarto' Markdown Publishing System -Version: 1.4.4.9028 +Version: 1.4.4.9029 Authors@R: c( person("JJ", "Allaire", , "jj@posit.co", role = "aut", comment = c(ORCID = "0000-0003-0174-9868")), diff --git a/NEWS.md b/NEWS.md index 2b35f6e9..1dfd2d3e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -81,10 +81,11 @@ - `qmd_to_r_script()` extracts R code cells from Quarto documents and creates R scripts. This experimental function preserves chunk options - using `#|` syntax, adds YAML metadata as spin-style headers, and handles - mixed-language documents by filtering only R cells. Complements the - existing `add_spin_preamble()` function for working with R scripts in - Quarto workflows (#208, quarto-dev/quarto-cli#9112). + using `#|` syntax, adds YAML metadata as spin-style headers, handles + mixed-language documents by filtering only R cells, skips chunks with + `purl: false`, and properly processes `eval: false` chunks by commenting + out their code. Complements the existing `add_spin_preamble()` function + for working with R scripts in Quarto workflows (#208, #277, quarto-dev/quarto-cli#9112). - `quarto_available()` checks if Quarto CLI is found (thanks, @hadley, #187). diff --git a/R/utils-extract.R b/R/utils-extract.R index 3aaec6d0..e6488f48 100644 --- a/R/utils-extract.R +++ b/R/utils-extract.R @@ -23,6 +23,11 @@ #' - Adding the document's YAML metadata as a spin-style header #' - Creating an R script that can be rendered with the same options #' +#' ## Chunk option handling: +#' - Chunks with `purl: false` are completely skipped and not included in the output +#' - Chunks with `eval: false` have their code commented out (prefixed with `# `) in the R script +#' - All other chunk options are preserved as `#|` comment headers +#' #' ## File handling: #' - If the output R script already exists, the function will abort with an error #' - Non-R code cells (e.g., Python, Julia, Observable JS) are ignored @@ -35,7 +40,15 @@ #' more details on rendering R scripts with Quarto. #' #' The resulting R script uses Quarto's executable cell format with `#|` -#' comments to preserve chunk options like `echo`, `eval`, `output`, etc. +#' comments to preserve chunk options like `label`, `echo`, `output`, etc. +#' +#' The resulting R script could also be `source()`d in R, as any `eval = FALSE` will be commented out. +#' +#' ## Limitations: +#' This function relies on static analysis of the Quarto document by `quarto inspect`. This means that +#' any \pkg{knitr} specific options like `child=` or specific feature like [knitr::read_chunk()] are not supported. +#' They rely on tangling or knitting by \pkg{knitr} itself. For this support, +#' one should look at [knitr::hook_purl()] or [knitr::purl()]. #' #' @return Invisibly returns the path to the created R script file, or #' `NULL` if no R code cells were found. @@ -110,12 +123,22 @@ qmd_to_r_script <- function(qmd, script = NULL) { } r_codeCells <- codeCells[codeCells$language == "r", ] - content <- character(nrow(r_codeCells)) for (i in seq_len(nrow(r_codeCells))) { row <- r_codeCells[i, ] metadata_list <- as.list(row$metadata) metadata_clean <- metadata_list[!is.na(metadata_list)] + if (isFALSE(metadata_clean$purl)) { + # cell with purl: false should be skipped + next + } + if (isFALSE(metadata_clean$eval)) { + # cell with eval: false should be commented out in R script + row$source <- paste( + c(paste0("# ", head(xfun::split_lines(row$source), -1)), ""), + collapse = "\n" + ) + } content[i] <- paste( c(create_code_preamble(metadata_clean), row$source), collapse = "\n" diff --git a/man/qmd_to_r_script.Rd b/man/qmd_to_r_script.Rd index 61cb7a4b..922547a4 100644 --- a/man/qmd_to_r_script.Rd +++ b/man/qmd_to_r_script.Rd @@ -36,6 +36,14 @@ This function processes a Quarto document by: \item Adding the document's YAML metadata as a spin-style header \item Creating an R script that can be rendered with the same options } +\subsection{Chunk option handling:}{ +\itemize{ +\item Chunks with \code{purl: false} are completely skipped and not included in the output +\item Chunks with \code{eval: false} have their code commented out (prefixed with \verb{# }) in the R script +\item All other chunk options are preserved as \verb{#|} comment headers +} +} + \subsection{File handling:}{ \itemize{ \item If the output R script already exists, the function will abort with an error @@ -52,7 +60,17 @@ See \url{https://quarto.org/docs/computations/render-scripts.html#knitr} for more details on rendering R scripts with Quarto. The resulting R script uses Quarto's executable cell format with \verb{#|} -comments to preserve chunk options like \code{echo}, \code{eval}, \code{output}, etc. +comments to preserve chunk options like \code{label}, \code{echo}, \code{output}, etc. + +The resulting R script could also be \code{source()}d in R, as any \code{eval = FALSE} will be commented out. +} + +\subsection{Limitations:}{ + +This function relies on static analysis of the Quarto document by \verb{quarto inspect}. This means that +any \pkg{knitr} specific options like \verb{child=} or specific feature like \code{\link[knitr:read_chunk]{knitr::read_chunk()}} are not supported. +They rely on tangling or knitting by \pkg{knitr} itself. For this support, +one should look at \code{\link[knitr:chunk_hook]{knitr::hook_purl()}} or \code{\link[knitr:knit]{knitr::purl()}}. } } \examples{ diff --git a/tests/testthat/_snaps/utils-extract/purl.R b/tests/testthat/_snaps/utils-extract/purl.R index b29ef2ee..8bc32a62 100644 --- a/tests/testthat/_snaps/utils-extract/purl.R +++ b/tests/testthat/_snaps/utils-extract/purl.R @@ -4,6 +4,7 @@ #' --- #' +#| label: my-label #| echo: false #| output: asis cat("Hello, world") @@ -11,3 +12,8 @@ cat("Hello, world") #| echo: true cat("more") +#| eval: false +# # This code should not run. +# 1 + a + + diff --git a/tests/testthat/_snaps/utils-extract/purl.md b/tests/testthat/_snaps/utils-extract/purl.md index 1ac4a257..3a10a0d1 100644 --- a/tests/testthat/_snaps/utils-extract/purl.md +++ b/tests/testthat/_snaps/utils-extract/purl.md @@ -14,3 +14,10 @@ cat("more") more ::: :::: + +::: cell +``` {.r .cell-code} +# # This code should not run. +# 1 + a +``` +::: diff --git a/tests/testthat/resources/purl-r.qmd b/tests/testthat/resources/purl-r.qmd index eefaf225..d3df6c6a 100644 --- a/tests/testthat/resources/purl-r.qmd +++ b/tests/testthat/resources/purl-r.qmd @@ -6,6 +6,7 @@ format: html ## Section ```{r} +#| label: my-label #| echo: false #| output: asis cat("Hello, world") @@ -16,4 +17,21 @@ cat("Hello, world") ```{r} #| echo: true cat("more") +``` + +## A section that should be commented out + +```{r} +#| eval: false +# This code should not run. +1 + a +``` + +## A section explicitly commented out with `purl = FALSE` + +```{r} +#| purl: false +# This code should not be included in the purl output. +# but it works +1 + 1 ``` \ No newline at end of file diff --git a/tests/testthat/test-utils-extract.R b/tests/testthat/test-utils-extract.R index 962c8dd2..181f875c 100644 --- a/tests/testthat/test-utils-extract.R +++ b/tests/testthat/test-utils-extract.R @@ -46,6 +46,47 @@ test_that("qmd_to_r_script() writes R file that renders", { ) }) +test_that("qmd_to_r_script() comment cells with eval = TRUE", { + r_script <- withr::local_tempfile(pattern = "purl", fileext = ".R") + + qmd_to_r_script( + resources_path("purl-r.qmd"), + script = r_script + ) + content <- xfun::file_string(r_script) + expect_match( + content, + "# # This code should not run.", + fixed = TRUE + ) + expect_no_match( + content, + "(?