Reputation: 33
Take for example this Rmd file - https://github.com/rstudio/learnr/blob/master/inst/tutorials/ex-setup-r/ex-setup-r.Rmd
The YAML header has this -
output:
learnr::tutorial:
progressive: true
allow_skip: true
I would like to change this to -
output:
ioslides_presentation:
widescreen: true
Is there a way to make this edit programmatically i.e. Can I write some function that takes an Rmd file as input, edits the YAML header, and produces a new Rmd file?
Thanks!
Upvotes: 3
Views: 1937
Reputation: 1041
I modified it slightly.
With the version given, if an existing yaml element is changed, it is moved to the end of the yaml header.
With my modification, existing elements with changed values will keep their position in the header.
change_yaml_matter <- function(input_file, ..., output_file) {
input_lines <- readLines(input_file)
delimiters <- grep("^---\\s*$", input_lines)
if (!length(delimiters)) {
stop("unable to find yaml delimiters")
} else if (length(delimiters) == 1L) {
if (delimiters[1] == 1L) {
stop("cannot find second delimiter, first is on line 1")
} else {
# found just one set, assume it is *closing* the yaml matter;
# fake a preceding line of delimiter
delimiters <- c(0L, delimiters[1])
}
}
delimiters <- delimiters[1:2]
yaml_list <- yaml::yaml.load(
input_lines[ (delimiters[1]+1):(delimiters[2]-1) ])
dots <- list(...)
for (element_name in names(dots)){
if(element_name %in% names(yaml_list)) {
yaml_list[element_name] <- dots[element_name]
} else {
yaml_list <- c(yaml_list,dots[element_name])
}
}
output_lines <- c(
if (delimiters[1] > 0) input_lines[1:(delimiters[1])],
strsplit(yaml::as.yaml(yaml_list), "\n")[[1]],
input_lines[ -(1:(delimiters[2]-1)) ]
)
if (missing(output_file)) {
return(output_lines)
} else {
writeLines(output_lines, con = output_file)
return(invisible(output_lines))
}
}
Upvotes: 1
Reputation: 160742
I think a quick function could do this.
change_yaml_matter <- function(input_file, ..., output_file) {
input_lines <- readLines(input_file)
delimiters <- grep("^---\\s*$", input_lines)
if (!length(delimiters)) {
stop("unable to find yaml delimiters")
} else if (length(delimiters) == 1L) {
if (delimiters[1] == 1L) {
stop("cannot find second delimiter, first is on line 1")
} else {
# found just one set, assume it is *closing* the yaml matter;
# fake a preceding line of delimiter
delimiters <- c(0L, delimiters[1])
}
}
delimiters <- delimiters[1:2]
yaml_list <- yaml::yaml.load(input_lines[ (delimiters[1]+1):(delimiters[2]-1) ])
dots <- list(...)
yaml_list <- c(yaml_list[ setdiff(names(yaml_list), names(dots)) ], dots)
output_lines <- c(
if (delimiters[1] > 0) input_lines[1:(delimiters[1])],
strsplit(yaml::as.yaml(yaml_list), "\n")[[1]],
input_lines[ -(1:(delimiters[2]-1)) ]
)
if (missing(output_file)) {
return(output_lines)
} else {
writeLines(output_lines, con = output_file)
return(invisible(output_lines))
}
}
Where ...
is whatever you want it to be. Meaning: if you want to replace the output:
component of the yaml matter, then you give a named list as output=list(...)
.
If I use the rmarkdown document I used in a previous answer, then unchanged, it looks like this:
readLines("~/StackOverflow/1883604/62095186.Rmd")
# [1] "---"
# [2] "title: Hello"
# [3] "output: html_document"
# [4] "params:"
# [5] " intab: TRUE"
# [6] "---"
# [7] ""
# [8] "# Headline 1"
# [9] ""
# [10] "## Headline 2 `r if (params$intab) \"{.tabset}\"`"
# [11] ""
# [12] "### Headline 3 in a tab"
# [13] ""
# [14] "### Headline 4 in a tab"
# [15] ""
# [16] "### Headline 5 in a tab"
# [17] ""
# [18] ""
And to change the output
portion, I add a nested named list as:
change_yaml_matter("~/StackOverflow/1883604/62095186.Rmd",
output=list(ioslides_presentation=list(widescreen=TRUE)))
# [1] "---"
# [2] "title: Hello"
# [3] "params:"
# [4] " intab: yes"
# [5] "output:"
# [6] " ioslides_presentation:"
# [7] " widescreen: yes"
# [8] "---"
# [9] ""
# [10] "# Headline 1"
# [11] ""
# [12] "## Headline 2 `r if (params$intab) \"{.tabset}\"`"
# [13] ""
# [14] "### Headline 3 in a tab"
# [15] ""
# [16] "### Headline 4 in a tab"
# [17] ""
# [18] "### Headline 5 in a tab"
# [19] ""
# [20] ""
You can change just about any portion of the yaml matter. (The only things you cannot change, I suspect, are if you happen to have yaml parameters named input_file
or output_file
. If you actually have Rmd files with those yaml top-level parameters, then you can easily rename the named arguments here to be something else, such as Mxyzptlk
and something else ... you're unlikely to see those in production.)
Notes:
output_file="path/to/new.RMd"
to your call, and it will write a new file.output_file=
in the arguments, if you choose to not catch the return value, it will appear to return nothing. This is due to invisible
in my return; if you really want to see and save, either capture to a variable and look at that, or wrap the function call in parens, as in (change_yaml_matter(...))
.The trick for YAML is to know that yaml::
will treat every top-level as the named element of a list, and its contents are recursively lists in the same manner. For instance,
str(yaml::yaml.load("
---
top1:
level2a:
level3a: 123
level3b: 456
level2b: 789
top2: quux
---"))
# List of 2
# $ top1:List of 2
# ..$ level2a:List of 2
# .. ..$ level3a: int 123
# .. ..$ level3b: int 456
# ..$ level2b: int 789
# $ top2: chr "quux"
To assign new values, just provide nested named lists.
Upvotes: 4