Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve support for knitr::spin(format = 'qmd') #2320

Merged
merged 9 commits into from
Mar 28, 2024
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: knitr
Type: Package
Title: A General-Purpose Package for Dynamic Report Generation in R
Version: 1.45.14
Version: 1.45.15
Authors@R: c(
person("Yihui", "Xie", role = c("aut", "cre"), email = "xie@yihui.name", comment = c(ORCID = "0000-0003-0645-5666")),
person("Abhraneel", "Sarma", role = "ctb"),
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

- `knitr::spin()` now recognizes `# %%` as a valid code chunk delimiter (thanks, @kylebutts, #2307).

- `knitr::spin()` also recognizes `#|` comments as code chunks now (thanks, @kylebutts, #2320).

- Chunk hooks can have the `...` argument now. Previously, only arguments `before`, `options`, `envir`, and `name` are accepted. If a chunk hook function has the `...` argument, all the aforementioned four arguments are passed to the hook. This means the hook function no longer has to have the four arguments explicitly in its signature, e.g., `function(before, ...)` is also valid if only the `before` argument is used inside the hook. See <https://yihui.org/knitr/hooks/#chunk-hooks> for more information.

- For package vignettes, PNG plots will be optimized by `optipng` and `pngquant` if they are installed and can be found from the system `PATH` variable. This can help reduce the package size if vignettes contain PNG plots (thanks, @nanxstats, <https://nanx.me/blog/post/rpkgs-pngquant-ragg/>).
Expand Down
37 changes: 25 additions & 12 deletions R/spin.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
#' This function takes a specially formatted R script and converts it to a
#' literate programming document. By default normal text (documentation) should
#' be written after the roxygen comment (\code{#'}) and code chunk options are
#' written after \code{#+} or \code{# \%\%} or \code{#-} or \code{# ----} or
#' any of these combinations replacing \code{#} with \code{--}.
#' written after \code{#|} or \code{#+} or \code{# \%\%} or \code{# ----}.
#'
#' Obviously the goat's hair is the original R script, and the wool is the
#' literate programming document (ready to be knitted).
Expand Down Expand Up @@ -79,7 +78,6 @@ spin = function(

r = rle((is_matchable & grepl(doc, x)) | i) # inline expressions are treated as doc instead of code
n = length(r$lengths); txt = vector('list', n); idx = c(0L, cumsum(r$lengths))
p1 = gsub('\\{', '\\\\{', paste0('^', p[1L], '.*', p[2L], '$'))

for (i in seq_len(n)) {
block = x[seq(idx[i] + 1L, idx[i + 1])]
Expand All @@ -90,18 +88,24 @@ spin = function(
# R code; #+/- indicates chunk options
block = strip_white(block) # rm white lines in beginning and end
if (!length(block)) next
rc <- '^(#|--)+(\\+|-|\\s+%%| ----+| @knitr)'
if (length(opt <- grep(rc, block))) {
opts = gsub(paste0(rc, '\\s*|-*\\s*$'), '', block[opt])
opts = paste0(ifelse(opts == '', '', ' '), opts)
block[opt] = paste0(p[1L], opts, p[2L])

rc = '^(#|--)+(\\+|-| %%| ----+| @knitr)(.*?)\\s*-*\\s*$'
j1 = grep(rc, block)
# pipe comments (#|) should start a code chunk if they are not preceded by
# chunk opening tokens
j2 = setdiff(pipe_comment_start(block), j1 + 1)

if (length(j3 <- c(j1, j2))) {
block[j1] = paste0(p[1], gsub(rc, '\\3', block[j1]), p[2])
block[j2] = paste0(p[1], p[2], '\n', block[j2])

# close each chunk if there are multiple chunks in this block
if (any(opt > 1)) {
j = opt[opt > 1]
block[j] = paste(p[3L], block[j], sep = '\n')
if (any(j3 > 1)) {
j = j3[j3 > 1]
block[j] = paste0(p[3], '\n', block[j])
}
}
if (!grepl(p1, block[1L])) {
if (!startsWith(block[1L], p[1L])) {
block = c(paste0(p[1L], p[2L]), block)
}
c('', block, p[3L], '')
Expand Down Expand Up @@ -155,6 +159,15 @@ spin = function(
c(paste0(b, '{r'), '}', b, paste0(i, 'r \\1 ', i))
}

# find the position of the starting `#|` in a consecutive block of `#|` comments
pipe_comment_start = function(x) {
i = startsWith(x, '#| ')
r = rle(i)
l = r$lengths
j = cumsum(l) - l + 1
j[r$values]
}

#' Spin a child R script
#'
#' This function is similar to \code{\link{knit_child}()} but is used in R
Expand Down
3 changes: 1 addition & 2 deletions man/spin.Rd

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

45 changes: 30 additions & 15 deletions tests/testit/test-spin.R
Original file line number Diff line number Diff line change
@@ -1,32 +1,47 @@
library(testit)

spin_w_tempfile = function(..., format = "Rmd") {
tmp = tempfile(fileext = ".R")
writeLines(c(...), tmp)
spinned = spin(tmp, knit = FALSE, format = format)
result = readLines(spinned)
file.remove(c(tmp, spinned))
result
spin_text = function(..., format = "Rmd") {
x = spin(text = c(...), knit = FALSE, format = format)
xfun::split_lines(x)
}

assert("spin() detects lines for documentation", {
(spin_w_tempfile("#' test", "1 * 1", "#' test") %==%
(spin_text("#' test", "1 * 1", "#' test") %==%
c("test", "", "```{r}", "1 * 1", "```", "", "test"))
# a multiline string literal contains the pattern of doc or inline
(spin_w_tempfile("code <- \"", "#' test\"") %==%
(spin_text("code <- \"", "#' test\"") %==%
c("", "```{r}", "code <- \"", "#' test\"", "```", ""))
(spin_w_tempfile("code <- \"", "{{ 1 + 1 }}", "\"") %==%
(spin_text("code <- \"", "{{ 1 + 1 }}", "\"") %==%
c("", "```{r}", "code <- \"", "{{ 1 + 1 }}", "\"", "```", ""))
# a multiline symbol contains the pattern of doc or inline
(spin_w_tempfile("`", "#' test", "`") %==%
(spin_text("`", "#' test", "`") %==%
c("", "```{r}", "`", "#' test", "`", "```", ""))
(spin_w_tempfile("`", "{{ 1 + 1 }}", "`") %==%
(spin_text("`", "{{ 1 + 1 }}", "`") %==%
c("", "```{r}", "`", "{{ 1 + 1 }}", "`", "```", ""))
})

assert("spin() uses proper number of backticks", {
(spin_w_tempfile("{{ '`' }}") %==% c("``r '`' ``"))
(spin_w_tempfile("{{`x`}}") %==% c("``r `x` ``"))
(spin_w_tempfile("x <- '", "```", "'") %==%
(spin_text("{{ '`' }}") %==% c("``r '`' ``"))
(spin_text("{{`x`}}") %==% c("``r `x` ``"))
(spin_text("x <- '", "```", "'") %==%
c("", "````{r}", "x <- '", "```", "'", "````", ""))
})

assert("spin() generates code chunks with pipe comments `#|`", {
(
spin_text("", "#| echo: false", "#| message: false", "#| include: false", "1+1", "#| eval: false", "2 + 2", "", "#' Text") %==%
c('', '```{r}', '#| echo: false', '#| message: false', '#| include: false', '1+1', '```', '```{r}', '#| eval: false', '2 + 2', '```', '', 'Text')
)

# https://github.com/yihui/knitr/issues/2314
(
spin_text('#| echo: false', '1+1', '#| label: test', '1+1') %==%
c('', '```{r}', '#| echo: false', '1+1', '```', '```{r}', '#| label: test', '1+1', '```', '')
)

# Has a `# %%` already
(
spin_text('# %%', '#| echo: false', '1+1', '#| label: test', '1+1') %==%
c('', '```{r}', '#| echo: false', '1+1', '```', '```{r}', '#| label: test', '1+1', '```', '')
)
})
Loading