From fecefa4d9db922af72c6eeac2e08a327040c3ded Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 15 Oct 2021 09:58:44 -0400 Subject: [PATCH 1/6] Refactor knit hooks into prep/install step `tutorial_knitr_options()` now prepares a list of knitr options with the knitr options and hooks that are used during the tutorial pre-render. This list is in the same format as the knitr options expected by `rmarkdown::output_format()` so we can set the knitr options directly in the `tutorial` output format. This ensures that the hooks are set for each `rmarkdown::render()` call. Previously, by only relying on `.onAttach()`, these hooks might be reset at the end of the first `render()` and wouldn't be re-installed for subsequent renders, resulting in the behavior seen in rstudio/learnr#598. I haven't removed the `.onAttach()` mechanism because it is still useful for catching learnr component usage outside of the `learnr::tutorial()` format. --- R/knitr-hooks.R | 31 +++++++++++++++++++++---------- R/tutorial-format.R | 3 +++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/R/knitr-hooks.R b/R/knitr-hooks.R index f8101131f..4fbfa8551 100644 --- a/R/knitr-hooks.R +++ b/R/knitr-hooks.R @@ -13,12 +13,7 @@ detect_installed_knitr_hooks <- function() { is.function(tutorial_knit_hook) } -install_knitr_hooks <- function() { - # set global tutorial option which we can use as a basis for hooks - # (this is so we don't collide with hooks set by the user or - # by other packages or Rmd output formats) - knitr::opts_chunk$set(tutorial = TRUE) - +tutorial_knitr_options <- function() { # helper to check for runtime: shiny_prerendered being active is_shiny_prerendered_active <- function() { identical(knitr::opts_knit$get("rmarkdown.runtime"),"shiny_prerendered") @@ -186,7 +181,7 @@ install_knitr_hooks <- function() { } # hook to turn off evaluation/highlighting for exercise related chunks - knitr::opts_hooks$set(tutorial = function(options) { + tutorial_opts_hook <- function(options) { # check for chunk type exercise_chunk <- is_exercise_chunk(options) @@ -285,10 +280,10 @@ install_knitr_hooks <- function() { # return modified options options - }) + } # hook to amend output for exercise related chunks - knitr::knit_hooks$set(tutorial = function(before, options, envir) { + tutorial_knit_hook <- function(before, options, envir) { # helper to produce an exercise wrapper div w/ the specified class exercise_wrapper_div <- function(suffix = NULL, extra_html = NULL) { @@ -465,7 +460,23 @@ install_knitr_hooks <- function() { } } - }) + } + + list( + # set global tutorial option which we can use as a basis for hooks + # (this is so we don't collide with hooks set by the user or + # by other packages or Rmd output formats) + opts_chunk = list(tutorial = TRUE), + opts_hooks = list(tutorial = tutorial_opts_hook), + knit_hooks = list(tutorial = tutorial_knit_hook) + ) +} + +install_knitr_hooks <- function() { + knit_opts <- tutorial_knitr_options() + knitr::opts_chunk$set(tutorial = knit_opts$opts_chunk$tutorial) + knitr::opts_hooks$set(tutorial = knit_opts$opts_hooks$tutorial) + knitr::knit_hooks$set(tutorial = knit_opts$knit_hooks$tutorial) } remove_knitr_hooks <- function() { diff --git a/R/tutorial-format.R b/R/tutorial-format.R index f0df3c96e..8d922556b 100644 --- a/R/tutorial-format.R +++ b/R/tutorial-format.R @@ -133,6 +133,9 @@ tutorial <- function(fig_width = 6.5, args = args, ext = ".html") + tutorial_opts <- tutorial_knitr_options() + knitr_options <- utils::modifyList(knitr_options, tutorial_opts) + # set 1000 as the default maximum number of rows in paged tables knitr_options$opts_chunk$max.print <- 1000 From ea072ac9f65a196850a4a10081dd1f3eea59ecdd Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 15 Oct 2021 11:44:26 -0400 Subject: [PATCH 2/6] Use package docs and move package event hooks to `R/zzz.R` --- R/learnr-package.R | 35 +++++++++++++++++++++++++++++++++++ R/package.R | 23 ----------------------- R/{learnr.R => zzz.R} | 12 ++++++++++++ man/learnr-package.Rd | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 23 deletions(-) create mode 100644 R/learnr-package.R delete mode 100644 R/package.R rename R/{learnr.R => zzz.R} (68%) create mode 100644 man/learnr-package.Rd diff --git a/R/learnr-package.R b/R/learnr-package.R new file mode 100644 index 000000000..b554c6a14 --- /dev/null +++ b/R/learnr-package.R @@ -0,0 +1,35 @@ +#' @keywords internal +#' @import rmarkdown +"_PACKAGE" + +## usethis namespace: start +#' @importFrom evaluate evaluate +#' @importFrom htmltools attachDependencies +#' @importFrom htmltools div +#' @importFrom htmltools HTML +#' @importFrom htmltools htmlDependency +#' @importFrom htmltools tags +#' @importFrom htmlwidgets createWidget +#' @importFrom jsonlite base64_dec +#' @importFrom jsonlite base64_enc +#' @importFrom knitr all_labels +#' @importFrom knitr knit_hooks +#' @importFrom knitr knit_meta_add +#' @importFrom knitr opts_chunk +#' @importFrom knitr opts_hooks +#' @importFrom knitr opts_knit +#' @importFrom knitr spin +#' @importFrom markdown markdownExtensions +#' @importFrom markdown markdownToHTML +#' @importFrom rprojroot find_root +#' @importFrom rprojroot is_r_package +#' @importFrom shiny invalidateLater +#' @importFrom shiny isolate +#' @importFrom shiny observe +#' @importFrom shiny observeEvent +#' @importFrom shiny reactive +#' @importFrom shiny reactiveValues +#' @importFrom shiny req +#' @importFrom withr with_envvar +## usethis namespace: end +NULL diff --git a/R/package.R b/R/package.R deleted file mode 100644 index a0320e6b6..000000000 --- a/R/package.R +++ /dev/null @@ -1,23 +0,0 @@ - -#' @import rmarkdown -#' @importFrom htmltools htmlDependency attachDependencies HTML div tags -#' @importFrom knitr opts_chunk opts_knit opts_hooks knit_hooks knit_meta_add all_labels spin -#' @importFrom jsonlite base64_dec base64_enc -#' @importFrom htmlwidgets createWidget -#' @importFrom markdown markdownToHTML markdownExtensions -#' @importFrom evaluate evaluate -#' @importFrom withr with_envvar -#' @importFrom rprojroot find_root is_r_package -#' @importFrom shiny reactiveValues observeEvent req isolate invalidateLater isolate observe reactive -NULL - -# install knitr hooks when package is attached to search path -.onAttach <- function(libname, pkgname) { - install_knitr_hooks() - initialize_tutorial() -} - -# remove knitr hooks when package is detached from search path -.onDetach <- function(libpath) { - remove_knitr_hooks() -} diff --git a/R/learnr.R b/R/zzz.R similarity index 68% rename from R/learnr.R rename to R/zzz.R index c65bd5b09..19e324658 100644 --- a/R/learnr.R +++ b/R/zzz.R @@ -1,3 +1,15 @@ + +# install knitr hooks when package is attached to search path +.onAttach <- function(libname, pkgname) { + install_knitr_hooks() + initialize_tutorial() +} + +# remove knitr hooks when package is detached from search path +.onDetach <- function(libpath) { + remove_knitr_hooks() +} + .onLoad <- function(libname, pkgname) { register_default_event_handlers() diff --git a/man/learnr-package.Rd b/man/learnr-package.Rd new file mode 100644 index 000000000..dc21654ab --- /dev/null +++ b/man/learnr-package.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/learnr-package.R +\docType{package} +\name{learnr-package} +\alias{learnr} +\alias{learnr-package} +\title{learnr: Interactive Tutorials for R} +\description{ +Create interactive tutorials using R Markdown. Use a combination of narrative, figures, videos, exercises, and quizzes to create self-paced tutorials for learning about R and R packages. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://rstudio.github.io/learnr/} + \item \url{https://github.com/rstudio/learnr} + \item Report bugs at \url{https://github.com/rstudio/learnr/issues} +} + +} +\author{ +\strong{Maintainer}: Garrick Aden-Buie \email{garrick@rstudio.com} (\href{https://orcid.org/0000-0002-7111-0077}{ORCID}) + +Authors: +\itemize{ + \item Barret Schloerke \email{barret@rstudio.com} (\href{https://orcid.org/0000-0001-9986-114X}{ORCID}) + \item JJ Allaire \email{jj@rstudio.com} [conceptor] +} + +Other contributors: +\itemize{ + \item Alexander Rossell Hayes \email{alex.rossellhayes@rstudio.com} (\href{https://orcid.org/0000-0001-9412-0457}{ORCID}) [contributor] + \item Nischal Shrestha \email{nischal@rstudio.com} (\href{https://orcid.org/0000-0003-3321-1712}{ORCID}) [contributor] + \item Angela Li \email{angelali921@gmail.com} (vignette) [contributor] + \item RStudio [copyright holder, funder] + \item Ajax.org B.V. (Ace library) [contributor, copyright holder] + \item Zeno Rocha (clipboard.js library) [contributor, copyright holder] + \item Nick Payne (Bootbox library) [contributor, copyright holder] + \item Jake Archibald (idb-keyval library) [contributor, copyright holder] +} + +} +\keyword{internal} From d320120652fa9abce625508ff14f06995160366e Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 15 Oct 2021 12:08:00 -0400 Subject: [PATCH 3/6] Don't import {rmarkdown} --- NAMESPACE | 1 - R/learnr-package.R | 1 - R/tutorial-format.R | 14 +++++++------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index e7571aeea..af48705aa 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -67,7 +67,6 @@ export(tutorial_html_dependency) export(tutorial_options) export(tutorial_package_dependencies) import(curl) -import(rmarkdown) import(shiny) importFrom(evaluate,evaluate) importFrom(htmltools,HTML) diff --git a/R/learnr-package.R b/R/learnr-package.R index b554c6a14..648184db2 100644 --- a/R/learnr-package.R +++ b/R/learnr-package.R @@ -1,5 +1,4 @@ #' @keywords internal -#' @import rmarkdown "_PACKAGE" ## usethis namespace: start diff --git a/R/tutorial-format.R b/R/tutorial-format.R index 8d922556b..4260bb5b9 100644 --- a/R/tutorial-format.R +++ b/R/tutorial-format.R @@ -62,13 +62,13 @@ tutorial <- function(fig_width = 6.5, args <- c(args, "--section-divs") # template - args <- c(args, "--template", pandoc_path_arg( + args <- c(args, "--template", rmarkdown::pandoc_path_arg( system.file("rmarkdown/templates/tutorial/resources/tutorial-format.htm", package = "learnr") )) # content includes - args <- c(args, includes_to_pandoc_args(includes)) + args <- c(args, rmarkdown::includes_to_pandoc_args(includes)) # pagedtables if (identical(df_print, "paged")) { @@ -94,7 +94,7 @@ tutorial <- function(fig_width = 6.5, # additional css for (css_file in css) - args <- c(args, "--css", pandoc_path_arg(css_file)) + args <- c(args, "--css", rmarkdown::pandoc_path_arg(css_file)) # resolve theme (ammend base stylesheet for "rstudio" theme stylesheets <- "tutorial-format.css" @@ -123,12 +123,12 @@ tutorial <- function(fig_width = 6.5, # additional pandoc variables jsbool <- function(value) ifelse(value, "true", "false") - args <- c(args, pandoc_variable_arg("progressive", jsbool(progressive))) - args <- c(args, pandoc_variable_arg("allow-skip", jsbool(allow_skip))) + args <- c(args, rmarkdown::pandoc_variable_arg("progressive", jsbool(progressive))) + args <- c(args, rmarkdown::pandoc_variable_arg("allow-skip", jsbool(allow_skip))) # knitr and pandoc options - knitr_options <- knitr_options_html(fig_width, fig_height, fig_retina, keep_md = FALSE , dev) - pandoc_options <- pandoc_options(to = "html4", + knitr_options <- rmarkdown::knitr_options_html(fig_width, fig_height, fig_retina, keep_md = FALSE , dev) + pandoc_options <- rmarkdown::pandoc_options(to = "html4", from = rmarkdown::from_rmarkdown(fig_caption, md_extensions), args = args, ext = ".html") From 31d3e7ef15d440ee9a7ad24bb432eb41d6efbabe Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 15 Oct 2021 12:34:24 -0400 Subject: [PATCH 4/6] rewrite comment --- R/knitr-hooks.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/R/knitr-hooks.R b/R/knitr-hooks.R index 4fbfa8551..d8b59c072 100644 --- a/R/knitr-hooks.R +++ b/R/knitr-hooks.R @@ -463,9 +463,10 @@ tutorial_knitr_options <- function() { } list( - # set global tutorial option which we can use as a basis for hooks - # (this is so we don't collide with hooks set by the user or - # by other packages or Rmd output formats) + # learnr uses `tutorial` for options and hooks, and we also globally set the + # chunk option `tutorial = TRUE`. This allows the learnr tutorial hooks to + # visit every chunk without colliding with hooks or options set by other + # packages or Rmd formats. opts_chunk = list(tutorial = TRUE), opts_hooks = list(tutorial = tutorial_opts_hook), knit_hooks = list(tutorial = tutorial_knit_hook) From 159fc27b31395cca79dac37f1a43fe04a1b723c3 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 15 Oct 2021 12:43:25 -0400 Subject: [PATCH 5/6] Ensure knitr hooks are installed in `run_tutorial()` This is now redundant for `learnr::tutorial` documents, but required for tutorials that don't use the tutorial format --- R/run.R | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/R/run.R b/R/run.R index e8c7f2723..4a5ecc3f5 100644 --- a/R/run.R +++ b/R/run.R @@ -84,6 +84,12 @@ run_tutorial <- function(name = NULL, package = NULL, shiny_args = NULL) { ) }) + # ensure hooks are available for a tutorial and clean up after run_tutorial() + if (!detect_installed_knitr_hooks()) { + withr::defer(remove_knitr_hooks()) + } + install_knitr_hooks() + # run within tutorial wd withr::with_dir(tutorial_path, { if (!identical(Sys.getenv("SHINY_PORT", ""), "")) { From 3e0fa2ccbbc5f4877fafa07a56bd674208791f4c Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 15 Oct 2021 12:56:52 -0400 Subject: [PATCH 6/6] Add NEWS item for #599 --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 697ac3b40..5ecfcfbe5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -57,6 +57,7 @@ learnr (development version) ## Bug fixes +* learnr's knitr hooks are now included by default in the `learnr::tutorial` R Markdown format. They are also registered for any tutorials run by `run_tutorial()`. [thanks @czucca, #599](https://github.com/rstudio/learnr/pull/599) * Support the updated Bootstrap 4+ popover `dispose` method name, previously `destroy`. ([#560](https://github.com/rstudio/learnr/pull/560)) * Properly enforce time limits and measure exercise execution times that exceed 60 seconds ([#366](https://github.com/rstudio/learnr/pull/366), [#368](https://github.com/rstudio/learnr/pull/368)) * Fixed unexpected behavior for `question_is_correct.learnr_text()` where `trim = FALSE`. Comparisons will now happen with the original input value, not the `HTML()` formatted answer value. ([#376](https://github.com/rstudio/learnr/pull/376))