3

I was interested in making an engine for knitr that would pre-process code chunks before sending them for evaluation with the following steps:

  1. pre-process options$code
  2. evaluate code with evaluate::evaluate() with the parent environment
  3. format the output with knitr::engine_output()

What I found was that any variables created in these custom chunks would not be available in the rest of the document. After some tinkering, I found that if I walked the call stack and found the last place knitr was called from, I could take the value of the envir argument and use that as the environment to evaluate::evaluate(). This feels hacky, though. Is there a better way to match the environment of the document in a custom chunk engine?

Example

An engine that appends a flippy onto all comments

knitr::knit_engines$set(flip =
  function(options) {
    # pre-process code
    code <- gsub("(#+?)", "\\1 (╯°□°)╯︵", options$code)

    # Find environment <--------------- IS THERE A BETTER WAY?
    #
    # grabbing the call stack
    cstack <- vapply(sys.calls(), function(i) paste(as.character(deparse(i)), collapse = "\n"), character(1))
    fstack <- sys.frames()
    # Finding the last instance of the knit function and grabbing the envir variable
    knitting <- rev(grep("knit(", cstack, fixed = TRUE))[1]
    e <- get("envir", fstack[[knitting]])

    OUT <- evaluate::evaluate(code, envir = e)
    knitr::engine_output(options, out = OUT)
  }
)

tmp <- tempfile(fileext = ".Rmd")
tmpout <- tempfile(fileext = ".md")
txt <- "---\noutput: md_document\n---\n\n```{r}\na <- 'A'\na\n```\n\n```{flip}\nb <- paste(a, 'and B') # FLIPPIN\nb\n```\n\nSponsored by the letters `r try(b)`\n"
cat(txt, file = tmp)

rmarkdown::render(tmp, output_file = tmpout, envir = new.env())
#> processing file: file3230dc4500b.Rmd
#> output file: file3230dc4500b.knit.md
#> /usr/bin/pandoc +RTS -K512m -RTS file3230dc4500b.utf8.md --to markdown_strict --from markdown+autolink_bare_uris+tex_math_single_backslash --output /tmp/Rtmpzc5qWO/file32306aeaf291.md --standalone
#> 
#> Output created: /tmp/Rtmpzc5qWO/file32306aeaf291.md
cat(readLines(tmp), sep = "\n")
#> ---
#> output: md_document
#> ---
#> 
#> ```{r}
#> a <- 'A'
#> a
#> ```
#> 
#> ```{flip}
#> b <- paste(a, 'and B') # FLIPPIN
#> b
#> ```
#> 
#> Sponsored by the letters `r try(b)`
cat(readLines(tmpout), sep = "\n")
#>     a <- 'A'
#>     a
#>     #> [1] "A"
#> 
#>     b <- paste(a, 'and B') # (╯°□°)╯︵ FLIPPIN
#>     b
#>     #> [1] "A and B"
#> 
#> Sponsored by the letters A and B

Created on 2020-06-16 by the reprex package (v0.3.0)

ZNK
  • 2,196
  • 1
  • 19
  • 25
  • 2
    Maybe the evaluate hook (undocumented) is useful for you: https://github.com/yihui/knitr/blob/0daf31be36eed8d8ec0ba51eedee909283afc45d/R/hooks.R#L10-L17 If you add your own `evaluate()` function then maybe you don't even need to create an engine? – Gabor Csardi Jun 16 '20 at 19:50
  • Wouldn't the evaluate hook work on all R chunks indiscriminately? – ZNK Jun 16 '20 at 20:35
  • But THANK YOU for pointing me to this portion of the code because I just found `knitr::knit_global()`, which solves my issue! – ZNK Jun 16 '20 at 20:39

1 Answers1

3

It turns out that there is a function in {knitr} precisely for this situation: knitr::knit_global(), which happens to be the environment when evaluating inline code: https://github.com/yihui/knitr/blob/0daf31be36eed8d8ec0ba51eedee909283afc45d/R/hooks.R#L13

knitr::knit_engines$set(flip = 
  function(options) {
    # pre-process code
    code <- gsub("(#+?)", "\\1 (╯°□°)╯︵", options$code)

    e <- knitr::knit_global() # <----- SOLUTION ᕕ( ᐛ )ᕗ

    OUT <- evaluate::evaluate(code, envir = e)
    knitr::engine_output(options, out = OUT)
  }
)

tmp <- tempfile(fileext = ".Rmd")
tmpout <- tempfile(fileext = ".md")
txt <- "---\noutput: md_document\n---\n\n```{r}\na <- 'A'\na\n```\n\n```{flip}\nb <- paste(a, 'and B') # FLIPPIN\nb\n```\n\nSponsored by the letters `r try(b)`\n"
cat(txt, file = tmp)

rmarkdown::render(tmp, output_file = tmpout, envir = new.env())
#> processing file: file3b0117b9cb1f.Rmd
#> output file: file3b0117b9cb1f.knit.md
#> /usr/bin/pandoc +RTS -K512m -RTS file3b0117b9cb1f.utf8.md --to markdown_strict --from markdown+autolink_bare_uris+tex_math_single_backslash --output /tmp/Rtmp1KgoUj/file3b01702d22f0.md --standalone
#> 
#> Output created: /tmp/Rtmp1KgoUj/file3b01702d22f0.md
cat(readLines(tmp), sep = "\n")
#> ---
#> output: md_document
#> ---
#> 
#> ```{r}
#> a <- 'A'
#> a
#> ```
#> 
#> ```{flip}
#> b <- paste(a, 'and B') # FLIPPIN
#> b
#> ```
#> 
#> Sponsored by the letters `r try(b)`
cat(readLines(tmpout), sep = "\n")
#>     a <- 'A'
#>     a
#>     #> [1] "A"
#> 
#>     b <- paste(a, 'and B') # (╯°□°)╯︵ FLIPPIN
#>     b
#>     #> [1] "A and B"
#> 
#> Sponsored by the letters A and B

Created on 2020-06-16 by the reprex package (v0.3.0)

ZNK
  • 2,196
  • 1
  • 19
  • 25
  • I opened this post in my browser in the morning, and meant to type out `knitr::knit_global()` but was too busy. Now I'm back to your post, and I'm glad that you have found it by yourself :) – Yihui Xie Jun 17 '20 at 03:37
  • I'm working on a custom knitr function. I read that `knit_global()` by default returns `R_GlobalEnv`. In this case, after knitting the document, does it mean that `a` and `b` are available inside the R Session? I want my engine, which works by creating an object of my interface class when called the first time, i.e. the first chunk and when `knit()` is finished I want that object deleted by leaving `knit()`'s scope. Is this/ your way how to go about it? – rcst Mar 27 '21 at 13:06