Skip to content

Commit

Permalink
make spin() recognize #| comments as code chunks (#2320)
Browse files Browse the repository at this point in the history
Co-authored-by: Yihui Xie <xie@yihui.name>
  • Loading branch information
kylebutts and yihui authored Mar 28, 2024
1 parent 9ecb451 commit ed80400
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 30 deletions.
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', '```', '')
)
})

0 comments on commit ed80400

Please sign in to comment.