diff --git a/DESCRIPTION b/DESCRIPTION index 18d35cbe7b..b47bcea164 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: rmarkdown Type: Package Title: Dynamic Documents for R -Version: 0.7.1 +Version: 0.7.3 Date: 2015-06-13 Author: JJ Allaire, Jonathan McPherson, Yihui Xie, Hadley Wickham, Joe Cheng, Jeff Allen diff --git a/NAMESPACE b/NAMESPACE index fc8e38768b..832258ef78 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -11,6 +11,7 @@ export(html_vignette) export(includes) export(includes_to_pandoc_args) export(ioslides_presentation) +export(knit_params_ask) export(knitr_options) export(knitr_options_html) export(knitr_options_pdf) diff --git a/R/html_resources.R b/R/html_resources.R index 34f22e768f..12c27ebba4 100644 --- a/R/html_resources.R +++ b/R/html_resources.R @@ -286,6 +286,22 @@ discover_rmd_resources <- function(rmd_file, encoding, discover_render_resource(front_matter[[bibfile]]) } } + + # check for parameter values that look like files. + if (!is.null(front_matter$params)) { + # This is the raw parameter information and has not had any YAML tag + # processing performed. See `knitr:::resolve_params`. + lapply(front_matter$params, function(param) { + if (is.list(param)) { + if (identical(param$input, "file")) { + if (!is.null(param$value)) { + # We treat param filenames as non-web resources. + discover_single_resource(param$value, TRUE, FALSE) + } + } + } + }) + } # check for knitr child documents in R Markdown documents if (tolower(tools::file_ext(rmd_file)) == "rmd") { diff --git a/R/params.R b/R/params.R new file mode 100644 index 0000000000..6da37bf998 --- /dev/null +++ b/R/params.R @@ -0,0 +1,389 @@ + +knit_params_get <- function(input_lines, params) { + # check for recent enough knitr + if (packageVersion("knitr") < "1.10") { + stop("knitr >= 1.10 required to use rmarkdown params") + } + + # read the default parameters and extract them into a named list + knit_params <- mark_utf8(knitr::knit_params(input_lines)) + default_params <- list() + for (param in knit_params) { + default_params[[param$name]] <- param$value + } + + # validate params passed to render + if (!is.null(params)) { + + if (identical(params, "ask")) { + if (!interactive()) { + stop("render parameter configuration only allowed in an interactive environment") + } + + params <- knit_params_ask(input_lines = input_lines) + if (is.null(params)) { + stop("render parameter configuration canceled") + } + } + + # verify they are a list + if (!is.list(params) || (length(names(params)) != length(params))) { + stop("render params argument must be a named list") + } + + # verify that all parameters passed are also in the yaml + invalid_params <- setdiff(names(params), names(default_params)) + if (length(invalid_params) > 0) { + stop("render params not declared in YAML: ", + paste(invalid_params, sep = ", ")) + } + } + + # merge explicitly provided params with defaults + merge_lists(default_params, params, recursive = FALSE) +} + +params_label <- function(inputControlFn, param) { + label <- ifelse(is.null(param$label), param$name, param$label) + if (identical(inputControlFn, shiny::fileInput)) { + if (is.character(param$value)) { + label <- paste0(label, " (default: ", param$value, ")") + } + } + label +} + +params_value_to_ui <- function(inputControlFn, value) { + if (identical(inputControlFn, shiny::fileInput)) { + NULL + } else if (identical(inputControlFn, shiny::textInput)) { + ## TODO: if long input, maybe truncate textInput values for display + + classes <- class(value) + if ("POSIXct" %in% classes) { + as.character(value) + } else if ("POSIXlt" %in% classes) { + as.character(value) + } else { + value + } + } else { + ## A type/control that doesn't need special handling; just emit the value. + value + } +} + +params_value_from_ui <- function(inputControlFn, value, uivalue) { + if (identical(inputControlFn, shiny::fileInput)) { + uivalue$datapath + } else if (identical(inputControlFn, shiny::textInput)) { + classes <- class(value) + if ("POSIXct" %in% classes) { + as.POSIXct(uivalue) + } else if ("POSIXlt" %in% classes) { + as.POSIXlt(uivalue) + } else { + uivalue + } + } else { + ## A type/control that doesn't need special handling; just emit the value. + uivalue + } +} + +params_get_input <- function(param) { + # Maps between value types and input: XXX + default_inputs <- list( + logical = "checkbox", + ## BUG: dateInput does not allow the user to not specify a value. + ## https://github.com/rstudio/shiny/issues/896 + Date = "date", + ## BUG: shiny does not support datetime selectors + ## https://github.com/rstudio/shiny/issues/897 + ## we ask for string input for now. + POSIXct = "datetime", + character = "text" + ) + default_inputs$integer <- default_inputs$numeric <- { + ## If min/max are specified, use a slider. + if (is.null(param$min) || is.null(param$max)) { + "numeric" + } else { + "slider" + } + } + + input <- param$input + if (is.null(input)) { + if (!is.null(param$choices)) { + ## radio buttons for a small number of choices, select otherwise. + if (length(param$choices) <= 4) { + input <- "radio" + } else { + input <- "select" + } + } else { + ## Not choices. Look at the value type to find what input control we + ## should use. + + ## A value might have multiple classes. Try: class(Sys.time()) + ## Try to find first class listed with a named control. + for (c in class(param$value)) { + default_input <- default_inputs[[c]] + if (!is.null(default_input)) { + input <- default_input + break + } + } + } + } + input +} + +params_get_control <- function(param) { + input <- params_get_input(param) + if (is.null(input)) { + return(NULL) + } + + # Maps between input: XXX and the various Shiny input controls + input_controls <- list( + checkbox = shiny::checkboxInput, + numeric = shiny::numericInput, + slider = shiny::sliderInput, + date = shiny::dateInput, + datetime = shiny::textInput, # placeholder for future datetime picker + text = shiny::textInput, + file = shiny::fileInput, + radio = shiny::radioButtons, + select = shiny::selectInput + ) + control <- input_controls[[input]] + if (is.null(control)) { + stop(paste("could not determine what control to use for parameter", param$name, "with input:", input)) + } + control +} + +params_configurable <- function(param) { + length(param$value) == 1 && !is.null(params_get_control(param)) +} + +#' Run a shiny application asking for parameter configuration for the given document. +#' +#' @param file Path to the R Markdown document with configurable parameters. +#' @param input_lines Content of the R Markdown document. If \code{NULL}, the contents of \code{file} will be read. +#' @param params A named list of optional parameter overrides used in place of the document defaults. +#' @param shiny_args Additional arguments to \code{\link[shiny:runApp]{runApp}}. +#' @param save_caption Caption to use use for button that saves/confirms parameters. +#' @param encoding The encoding of the input file; see \code{\link{file}}. +#' +#' @return named list with overridden parameter names and value. +#' +#' @export +knit_params_ask <- function(file = NULL, + input_lines = NULL, + params = NULL, + shiny_args = NULL, + save_caption = "Save", + encoding = getOption("encoding")) { + if (packageVersion("knitr") < "1.10.18") { + stop("knitr >= 1.10.18 required to use rmarkdown::knit_params_ask") + } + + if (is.null(input_lines)) { + if (is.null(file)) { + stop("knit_params_ask must have a non-NULL file or input_lines parameter") + } + input_lines <- read_lines_utf8(file, encoding) + } + + knit_params <- mark_utf8(knitr::knit_params(input_lines)) + + ## Input validation on params (checks shared with render) + if (!is.null(params)) { + ## Must be a named list + if (!is.list(params) || (length(names(params)) != length(params))) { + stop("knit_params_ask params argument must be a named list") + } + ## We do not validate names(params) because the document may have changed + ## but we're loading parameters that were configured with an older + ## version. + } + + ## If we happen to not have any knit_params, just return an empty list and + ## don't fire up the Shiny app. + if (length(knit_params) == 0) { + return(list()) + } + + configurable <- Filter(params_configurable, knit_params) + unconfigurable <- Filter(Negate(params_configurable), knit_params) + + ## This set of published values is the raw set that came from the user. + ## It does not include those values that cannot be configured or are + ## left to use the default. + values <- list() + + server <- function(input, output) { + param.ui <- function(param) { + inputControlFn <- params_get_control(param) + inputControlFnFormals <- names(formals(inputControlFn)) + + inputId <- param$name + label <- params_label(inputControlFn, param) + + arguments = list( + inputId = inputId, + label = label + ) + lapply(names(param), function(name) { + if (name %in% c("name", "input", "expr")) { + } else if (name == "label") { + arguments$label <<- label + } else if (name == "value") { + + ## The current value for this parameter is either `params` when + ## overridden by our caller or `param$value` otherwise. + current_value <- param$value + if (!is.null(params)) { + override <- params[[param$name]] + if (!is.null(override)) { + current_value <- override + } + } + # Now, transform into something that the input control can handle. + current_value <- params_value_to_ui(inputControlFn, current_value) + + # value maps to either "value" or "selected" depending on the control. + if ("value" %in% inputControlFnFormals) { + arguments$value <<- current_value + } else if ("selected" %in% inputControlFnFormals) { + arguments$selected <<- current_value + } + } else { + ## Not a special field. Blindly promote to the input control. + arguments[[name]] <<- param[[name]] + } + }) + + ## This is based on param$value not current_value because we want to + ## understand deviation from the report default, not any (optional) + ## call-time override. + uidefault <- params_value_to_ui(inputControlFn, param$value) + hasDefaultValue <- function(value) { + identical(uidefault, value) + } + + inputControl <- do.call(inputControlFn, arguments) + + showSelectControl <- NULL + selectControl <- NULL + selectInputId <- paste0("select_", param$name) + + ## Dates and times with expressions that mean "now" or "today" are first + ## materialized as selects. If the user chooses to customize the field, + ## we then show the type-specific picker. + if (is.null(params[[param$name]])) { # prior value; implicit customization + if (identical("Sys.time()", param$expr)) { + showSelectControl <- function(current) { + (is.null(current) || identical(current, "default")) + } + hasDefaultValue <- function(value) { FALSE } + choices <- list() + choices[[paste0("now (", param$value, ")")]] <- "default" + choices[["Use a custom time"]] <- "custom" + selectControl <- shiny::selectInput(inputId = selectInputId, + label = label, + choices = choices) + } else if (identical("Sys.Date()", param$expr)) { + showSelectControl <- function(current) { + (is.null(current) || identical(current, "default")) + } + hasDefaultValue <- function(value) { FALSE } + choices <- list() + choices[[paste0("today (", param$value, ")")]] <- "default" + choices[["Use a custom date"]] <- "custom" + selectControl <- shiny::selectInput(inputId = selectInputId, + label = label, + choices = choices) + } + } + + output[[paste0("ui_", param$name)]] <- shiny::renderUI({ + # For most parameters, the selectInputId input will be NULL. + if (!is.null(showSelectControl) && showSelectControl(input[[selectInputId]])) { + selectControl + } else { + inputControl + } + }) + + shiny::observe({ + uivalue <- input[[param$name]] + if (is.null(uivalue)) { + # ignore startup NULLs + } else if (hasDefaultValue(uivalue)) { + values[[param$name]] <<- NULL + } else { + values[[param$name]] <<- params_value_from_ui(inputControlFn, param$value, uivalue) + } + }) + } + + lapply(configurable, function(param) { + param.ui(param) + }) + + shiny::observeEvent(input$save, { + shiny::stopApp(values) + }) + + shiny::observeEvent(input$cancel, { + shiny::stopApp(NULL) + }) + } + + contents <- shiny::tags$div( + shiny::fluidRow(shiny::column(12, lapply(configurable, function(param) { + shiny::uiOutput(paste0("ui_", param$name)) + }))), class = "container-fluid") + + if (length(unconfigurable) > 0) { + skipped <- shiny::tags$div(shiny::tags$strong("Note:"), + "The following parameters cannot be customized:", + paste(lapply(unconfigurable, function(param) { param$name }), collapse = ", ")) + contents <- shiny::tagAppendChildren(contents, shiny::fluidRow(shiny::column(12, skipped))) + } + footer <- shiny::tags$div( + shiny::tags$div( + shiny::fluidRow(shiny::column(12, + shiny::actionButton("save", save_caption, class = "btn-primary navbar-btn pull-right"), + shiny::actionButton("cancel","Cancel", class = "navbar-btn pull-right"))), + class = "container-fluid"), + class = "navbar navbar-default navbar-fixed-bottom") + + style <- shiny::tags$style( + # Our controls are wiiiiide. + ".container-fluid .shiny-input-container { width: auto; }", + # Prevent the save/cancel buttons from squashing together. + ".navbar button { margin-left: 10px; }", + # Style for the navbar footer. + # http://getbootstrap.com/components/#navbar-fixed-bottom + "body { padding-bottom: 70px; }" + ) + ## Escape is "cancel" and Enter is "save". + script <- shiny::tags$script(shiny::HTML("$(document).keyup(function(e) {\n", + "if (e.which == 13) { $('#save').click(); } // enter\n", + "if (e.which == 27) { $('#cancel').click(); } // esc\n", + "});" + )) + ui <- shiny::bootstrapPage( + shiny::tags$head(style, script), + contents, + footer) + + shiny_app <- shiny::shinyApp(ui = ui, server = server) + shiny_args <- merge_lists(list(appDir = shiny_app), shiny_args) + do.call(shiny::runApp, shiny_args) +} diff --git a/R/render.R b/R/render.R index d4526cc618..8af4db270d 100644 --- a/R/render.R +++ b/R/render.R @@ -258,33 +258,8 @@ render <- function(input, # (only do this if there are parameters in the front matter # so we don't require recent knitr for all users) if (!is.null(yaml_front_matter$params)) { - - # check for recent enough knitr - if (packageVersion("knitr") < "1.10") - stop("knitr >= 1.10 required to use rmarkdown params") - - # read the default parameters and extract them into a named list - knit_params <- mark_utf8(knitr::knit_params(input_lines)) - default_params <- list() - for (param in knit_params) - default_params[[param$name]] <- param$value - - # validate params passed to render - if (!is.null(params)) { - - # verify they are a list - if (!is.list(params) || (length(names(params)) != length(params))) - stop("render params argument must be a named list") - - # verify that all parameters passed are also in the yaml - invalid_params <- setdiff(names(params), names(default_params)) - if (length(invalid_params) > 0) - stop("render params not declared in yaml: ", - paste(invalid_params, sep = ", ")) - } - - # merge explicitly provided params with defaults - params <- merge_lists(default_params, params, recursive = FALSE) + + params <- knit_params_get(input_lines, params) # make the params available in the knit environment if (!exists("params", envir = envir, inherits = FALSE)) { diff --git a/man/knit_params_ask.Rd b/man/knit_params_ask.Rd new file mode 100644 index 0000000000..73af35a531 --- /dev/null +++ b/man/knit_params_ask.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2 (4.1.1): do not edit by hand +% Please edit documentation in R/params.R +\name{knit_params_ask} +\alias{knit_params_ask} +\title{Run a shiny application asking for parameter configuration for the given document.} +\usage{ +knit_params_ask(file = NULL, input_lines = NULL, params = NULL, + shiny_args = NULL, save_caption = "Save", + encoding = getOption("encoding")) +} +\arguments{ +\item{file}{Path to the R Markdown document with configurable parameters.} + +\item{input_lines}{Content of the R Markdown document. If \code{NULL}, the contents of \code{file} will be read.} + +\item{params}{A named list of optional parameter overrides used in place of the document defaults.} + +\item{shiny_args}{Additional arguments to \code{\link[shiny:runApp]{runApp}}.} + +\item{save_caption}{Caption to use use for button that saves/confirms parameters.} + +\item{encoding}{The encoding of the input file; see \code{\link{file}}.} +} +\value{ +named list with overridden parameter names and value. +} +\description{ +Run a shiny application asking for parameter configuration for the given document. +} + diff --git a/man/render.Rd b/man/render.Rd index 6b9b240c8b..1ee9b7d6f1 100644 --- a/man/render.Rd +++ b/man/render.Rd @@ -47,7 +47,8 @@ render(input, output_format = NULL, output_file = NULL, output_dir = NULL, \item{params}{List of named parameters that override custom params specified within the YAML front-matter (e.g. specifying a dataset to - read or a date range to confine output to).} + read or a date range to confine output to). Pass \code{"ask"} to start + an application that helps guide parameter configuration.} \item{envir}{The environment in which the code chunks are to be evaluated during knitting (can use