rstudio / rmarkdown

Dynamic Documents for R
https://rmarkdown.rstudio.com
GNU General Public License v3.0
2.88k stars 977 forks source link

render_site with Rmd parameters #903

Open zeehio opened 7 years ago

zeehio commented 7 years ago

I started to use the rmarkdown::render_site function. At some point I had an Rmd file with parameters, and I wanted to render it several times with different options.

To do that, I adapted a custom site generator. I would like to know if you are interested in a pull request or at least export some functions so I don't need to use :::.

render_site with rmarkdown parameters

I wrote a custom site generator named site_with_params based on default_site. The main changes with respect to the default_site are:

name: "my-website"
navbar:
  title: "My Website"
  left:
    - text: "Home"
      href: index.html
    - text: "Domestic results"
       href: domestic.html
    - text: "Foreign results"
      href: foreign.html
files:
  index.html:
    src: index.Rmd
  domestic.html:
    src: domestic_foreign.Rmd
    params:
      type_of_flight: "domestic"
  foreign.html:
    src: domestic_foreign.Rmd
    params:
      type_of_flight: "foreign"

Questions

Actual code

The code I wrote is adapted from default_site and uses several non-exported rmarkdown functions.

This could be submitted as a PR if there is interest:

#' Generate site with Rmd files with parameters
#' To use this site generator set \code{site: rmarkdown::site_with_params}
#' in the yaml header of the \code{index.Rmd} file.
#' Then in the \code{_site.yml} file, add a files section:
#'
#' \code{
#' files:
#'   index.html:
#'     src: index.Rmd
#'     params:
#'       opt1: Val1
#'   about.html:
#'     src: about.Rmd
#'     params:
#'       opt1: Val2
#' }
#'
#' @param input
#'
#' @export
site_with_params <- function(input, encoding = getOption("encoding"), ...) {

  # get the site config
  config <- rmarkdown:::site_config(input, encoding)
  if (is.null(config))
    stop("No site configuration (_site.yml) file found.")

  input_dir <- rmarkdown:::input_as_dir(input)
  output_file_options <- config[["files"]]
  # config[["files"]] has:
  # files:
  #   index.html:
  #     src: index.Rmd
  #     params:
  #       opt1: Val1
  #   about.html:
  #     src: about.Rmd
  #     params:
  #       opt1: Val2

  # define render function (use ... to gracefully handle future args)
  render <- function(input_file,
                     output_format,
                     envir,
                     quiet,
                     encoding, ...) {

    # track outputs
    outputs <- c()

    # see if this is an incremental render
    incremental <- !is.null(input_file)

    if (incremental) {
      all_input_files <- vapply(output_file_options, function(x) x[["src"]], character(1))
      output_files <- names(output_file_options[all_input_files %in% input_file])
    } else {
      output_files <- names(output_file_options)
    }
    sapply(output_files, function(x) {
      # we suppress messages so that "Output created" isn't emitted
      # (which could result in RStudio previewing the wrong file)
      output <- suppressMessages(
        rmarkdown::render(file.path(input_dir, output_file_options[[x]][["src"]]),
                          output_format = output_format,
                          output_file = x,
                          params = output_file_options[[x]]$params,
                          output_options = list(lib_dir = "site_libs",
                                                self_contained = FALSE),
                          envir = envir,
                          quiet = quiet,
                          encoding = encoding)
      )

      # add to global list of outputs
      outputs <<- c(outputs, output)

      # check for files dir and add that as well
      sidecar_files_dir <- rmarkdown:::knitr_files_dir(output)
      files_dir_info <- file.info(sidecar_files_dir)
      if (isTRUE(files_dir_info$isdir))
        outputs <<- c(outputs, sidecar_files_dir)
    })

    # do we have a relative output directory? if so then remove,
    # recreate, and copy outputs to it (we don't however remove
    # it for incremental builds)
    if (config$output_dir != '.') {

      # remove and recreate output dir if necessary
      output_dir <- file.path(input, config$output_dir)
      if (file.exists(output_dir)) {
        if (!incremental) {
          unlink(output_dir, recursive = TRUE)
          dir.create(output_dir)
        }
      } else {
        dir.create(output_dir)
      }

      # move outputs
      for (output in outputs) {

        # don't move it if it's a _files dir that has a _cache dir
        if (grepl("^.*_files$", output)) {
          cache_dir <- gsub("_files$", "_cache", output)
          if (rmarkdown:::dir_exists(cache_dir))
            next;
        }

        output_dest <- file.path(output_dir, basename(output))
        if (rmarkdown:::dir_exists(output_dest))
          unlink(output_dest, recursive = TRUE)
        file.rename(output, output_dest)
      }

      # copy lib dir a directory at a time (allows it to work with incremental)
      lib_dir <- file.path(input, "site_libs")
      output_lib_dir <- file.path(output_dir, "site_libs")
      if (!file.exists(output_lib_dir))
        dir.create(output_lib_dir)
      libs <- list.files(lib_dir)
      for (lib in libs)
        file.copy(file.path(lib_dir, lib), output_lib_dir, recursive = TRUE)
      unlink(lib_dir, recursive = TRUE)

      # copy other files
      rmarkdown:::copy_site_resources(input, encoding)
    }

    # Print output created for rstudio preview
    if (!quiet) {
      # determine output file
      output_file <- ifelse(is.null(input_file),
                            "index.html",
                            output_files)
      if (config$output_dir != ".")
        output_file <- file.path(config$output_dir, output_file)
      message("\nOutput created: ", output_file)
    }
  }

  # define clean function
  clean <- function() {

    # build list of generated files
    generated <- c()

    # get html files
    html_files <- names(config[["files"]])

    # _files peers are always removed (they could be here due to
    # output_dir == "." or due to a _cache existing for the page)
    html_supporting <- paste0(rmarkdown:::knitr_files_dir(html_files), '/')
    generated <- c(generated, html_supporting)

    # _cache peers are always removed
    html_cache <- paste0(rmarkdown:::knitr_root_cache_dir(html_files), '/')
    generated <- c(generated, html_cache)

    # for rendering in the current directory we need to eliminate
    # output files for our inputs (including _files) and the lib dir
    if (config$output_dir == ".") {

      # .html peers
      generated <- c(generated, html_files)

      # site_libs dir
      generated <- c(generated, "site_libs/")

      # for an explicit output_dir just remove the directory
    } else {
      generated <- c(generated, paste0(config$output_dir, '/'))
    }

    # filter out by existence
    generated[file.exists(file.path(input, generated))]
  }

  # return site generator
  list(
    name = config$name,
    output_dir = config$output_dir,
    render = render,
    clean = clean
  )
}
jjallaire commented 7 years ago

What if we just added support for a params argument for the render_site function? We could furthermore allow this parameter to be the path to a YAML file.

zeehio commented 7 years ago

That would be great!

My only concern is that the same .Rmd file may be used as the input file for several output html files (with different parameters) and the params argument should be able to accept that use case.

Looking at the render function in the default_site, when it wants to do an "incremental" rendering, it is based on the input_file. This is fine if there is a single output file for each input file, but it gets a bit confusing in the case where we have a single Rmd file generating several html files.

To me it would make more sense to say "render this output file" and then it would render it using the corresponding Rmd input file and parameters... but maybe I am missing something or overcomplicating things...

Thanks for the quick reply on a Sunday... I wasn't expecting that :open_mouth: :+1:

jjallaire commented 7 years ago

I get it now, you want to use a single Rmd as a template for multiple distinct output files. That makes a tremendous amount of sense. Yes, I think it would be good to have support for this built in so I would take a PR. What would you think about the YAML being "output_files" rather than "files"?

On Sun, Dec 4, 2016 at 7:04 AM, Sergio Oller notifications@github.com wrote:

That would be great!

My only concern is that the same .Rmd file may be used as the input file for several output html files (with different parameters) and the params argument should be able to accept that use case.

Looking at the render https://github.com/rstudio/rmarkdown/blob/master/R/render_site.R#L170 function in the default_site, when it wants to do an "incremental" rendering, it is based on the input_file. This is fine if there is a single output file for each input file, but it gets a bit confusing in the case where we have a single Rmd file generating several html files.

To me it would make more sense to say "render this output file" and then it would render it using the corresponding Rmd input file and parameters... but maybe I am missing something or overcomplicating things...

Thanks for the quick reply on a Sunday... I wasn't expecting that 😮 👍

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/rstudio/rmarkdown/issues/903#issuecomment-264699970, or mute the thread https://github.com/notifications/unsubscribe-auth/AAGXx-RoLIWoDOdo0DqgxLKjn63w55Eoks5rEqxagaJpZM4LDj7O .

zeehio commented 7 years ago

I thought it would be better to do a PR with the changes and if necessary do further discussion there.

We were lucky and I also fixed #892 with almost zero extra effort. Looking forward to the next release! :smiley:

jjallaire commented 7 years ago

@zeehio I'm going to take this PR back up after the next CRAN release (2 or 3 weeks out).

zeehio commented 7 years ago

@jjallaire this is just a kind reminder of the issue, given your previous comment of taking the PR back up after the 1.3 release :+1:

yihui commented 7 years ago

@zeehio Sorry for leaving this issue aside for so long. Bad news is we probably still don't have the bandwidth to review your PR. I have currently scheduled it for v1.8, the next version after 1.7 (no ETA yet; probably early next year). I'd appreciate it if you could resolve the GIT conflicts before then.

zeehio commented 7 years ago

@yihui, I fully understand the bandwidth limitations. Like you, I am a bit full until mid January, but I will do my best to find the time to resolve conflicts. I appreciate a lot your kind reply, keep up doing your great work!

jhelvy commented 2 years ago

Just came across this - any chance this was ever integrated? I could certainly use this functionality in some course sites I'm building.

zeehio commented 2 years ago

As far as I know, the option of passing parameters to Rmd files through render_site() was not implemented.

I believe my pull request had some room for improvement and I discarded it. I think there was some interest by rmarkdown maintainers to keep render_site() functionality simple (or at least not more complex) and someone suggested that for such functionality maybe another package would be a better suit.

I have no idea and I don't need this functionality anymore, but I would check other packages (blogdown maybe?)