ruaridhw
ruaridhw

Reputation: 2345

RMarkdown render to Notebook with child chunks included

I'm after a way to render an Rmd document (that contains references to various "child" files) to a self-contained R Notebook without these dependencies.

At the moment, the .Rmd code chunks are located throughout a number of .R, .py and .sql files and are referenced in the report using

```{r extraction, include=FALSE, cache=FALSE}
knitr::read_chunk("myscript.R")
```

followed by

```{r chunk_from_myscript}
```

as documented here.

I've done this to avoid code duplication and to allow for running the source files separately however these code chunks are only executable in the report via a call to knit or render (when read_chunk is run and the code chunk is available).

Is there a way to spin-off an Rmd (prior to knitting) with just these chunks populated?

This function

rmarkdown::render("report.Rmd", clean = FALSE)

almost gets there as it leaves the markdown files behind whilst removing extraction and populating chunk_from_myscript however as these files are straight markdown, the chunks are no longer executable and the chunk options are missing. It obviously also doesn't include chunks where eval=TRUE, echo=FALSE which would be needed to run the resulting notebook.

I've also looked at knitr::spin however this would mean disseminating the contents of the report to every source file and isn't terribly ideal.

Reprex

report.Rmd

---
title: 'Report'
---

```{r read_chunks, include=FALSE, cache=FALSE}
knitr::read_chunk("myscript.R")
```

Some documentation

```{r chunk_from_myscript}
```

Some more documentation

```{r chunk_two_from_myscript, eval=TRUE, echo=FALSE}
```

myscript.R

#' # MyScript
#' 
#' This is a valid R source file which is formatted
#' using the `knitr::spin` style comments and code
#' chunks.
#' The file's code can be used in large .Rmd reports by
#' extracting the various chunks using `knitr::read_chunk` or
#' it can be spun into its own small commented .Rmd report
#' using `knitr::spin`

# ---- chunk_from_myscript
sessionInfo()

#' This is the second chunk

# ---- chunk_two_from_myscript
1 + 1

Desired Output

notebook.Rmd

---
title: 'Report'
---

Some documentation

```{r chunk_from_myscript}
sessionInfo()
```

Some more documentation

```{r chunk_two_from_myscript, eval=TRUE, echo=FALSE}
1 + 1
```

Upvotes: 0

Views: 1043

Answers (2)

ruaridhw
ruaridhw

Reputation: 2345

Since neither knitr::knit nor rmarkdown::render seem suited to rendering to R markdown, I've managed to somewhat work around this by dynamically inserting the chunk text into each empty chunk and writing that to a new file:

library(magrittr)
library(stringr)

# Find the line numbers of every empty code chunk
get_empty_chunk_line_nums <- function(file_text){
  # Create an Nx2 matrix where the rows correspond
  # to code chunks and the columns are start/end line nums
  mat <- file_text %>%
    grep(pattern = "^```") %>%
    matrix(ncol = 2, byrow = TRUE)
  # Return the chunk line numbers where the end line number
  # immediately follows the starting line (ie. chunk is empty)
  empty_chunks <- mat[,1] + 1 == mat[,2]
  mat[empty_chunks, 1]
}

# Substitute each empty code chunk with the code from `read_chunk`
replace_chunk_code <- function(this_chunk_num) {
  this_chunk <- file_text[this_chunk_num]
  # Extract the chunk alias
  chunk_name <- stringr::str_match(this_chunk, "^```\\{\\w+ (\\w+)")[2]
  # Replace the closing "```" with "<chunk code>\n```"
  chunk_code <- paste0(knitr:::knit_code$get(chunk_name), collapse = "\n")
  file_text[this_chunk_num + 1] %<>% {paste(chunk_code, ., sep = "\n")}
  file_text
}

render_to_rmd <- function(input_file, output_file, source_files) {
  lapply(source_files, knitr::read_chunk)
  file_text <- readLines(input_file)
  empty_chunks <- get_empty_chunk_line_nums(file_text)
  for (chunk_num in empty_chunks){
    file_text <- replace_chunk_code(file_text, chunk_num)
  }
  writeLines(file_text, output_file)
}

source_files <- c("myscript.R")
render_to_rmd("report.Rmd", "output.Rmd", source_files)

This has the added benefits of preserving chunk options and working with Python and SQL chunks too since there is no requirement to evaluate any chunks in this step.

Upvotes: 0

Kevin Arseneau
Kevin Arseneau

Reputation: 6264

Working through your reprex I now better understand the issue you are trying to solve. You can knit into an output.Rmd to merge your report and scripts into a single markdown file.

Instead of using knitr::read_chunk, I've read in with knitr::spin to cat the asis output into another .Rmd file. Also note the params$final flag to allow rendering the final document when set as TRUE or allowing the knit to an intermediate .Rmd as FALSE by default.

report.Rmd

---
title: "Report"
params:
  final: false
---

```{r load_chunk, include=FALSE}
chunk <- knitr::spin(text = readLines("myscript.R"), report = FALSE, knit = params$final)
```


Some documentation

```{r print_chunk, results='asis', echo=FALSE}
cat(chunk, sep = "\n")
```

to produce the intermediate file:

rmarkdown::render("report.Rmd", "output.Rmd")

output.Rmd

---
title: "Report"
---

Some documentation

```{r chunk_from_myscript, echo=TRUE}
sessionInfo() 
```

With the secondary output.Rmd, you could continue with my original response below to render to html_notebook so that the document may be shared without needing to regenerate but still containing the source R markdown file.

To render the final document from report.Rmd you can use:

rmarkdown::render("report.Rmd", params = list(final = TRUE))

Original response

You need to include additional arguments to your render statement.

rmarkdown::render(
  input = "output.Rmd",
  output_format = "html_notebook",
  output_file = "output.nb.html"
)

When you open the .nb.html file in RStudio the embedded .Rmd will be viewable in the editing pane.

Upvotes: 1

Related Questions