diff --git a/.Rbuildignore b/.Rbuildignore index ed3083fa0..6d5e232d6 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -17,3 +17,7 @@ ^doc$ ^Meta$ + +^\.vscode$ +^dev$ +^\.renovaterc\.json5$ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..82e1af541 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,10 @@ +{ + "name": "${localWorkspaceFolderBasename}", + "image": "ghcr.io/rocker-org/devcontainer/r-ver:4.3", + "mounts": [ + "source=${localEnv:HOME}/.gitignore_global,target=/home/rstudio/.gitignore_global,readonly", + "source=${localEnv:HOME}/.gitconfig,target=/home/rstudio/.gitconfig,readonly", + "source=${localEnv:HOME}/.ssh,target=/home/rstudio/.ssh,readonly" + ], + "postCreateCommand": "Rscript -e 'pak::local_install_dev_deps()'" +} diff --git a/.github/pull.yml b/.github/pull.yml new file mode 100644 index 000000000..85058ab9e --- /dev/null +++ b/.github/pull.yml @@ -0,0 +1,8 @@ +## Automatically sync fork with upstream +version: "1" +rules: + - base: carpentries-main + upstream: carpentries:main + mergeUnstable: false +label: ":arrow_heading_down: pull" +conflictLabel: "merge-conflict" diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 7430620cf..2e6e02434 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -6,9 +6,11 @@ # usethis::use_github_action("check-standard") will install it. on: push: - branches: [main, master] + branches: + - main + - l2d pull_request: - branches: [main, master] + branches: [main, master, l2d] schedule: - cron: '0 0 * * 2' workflow_dispatch: @@ -25,53 +27,21 @@ jobs: fail-fast: false matrix: config: - - {os: macOS-latest, cache: '~/Library/Application Support/renv', r: 'release'} - - - {os: windows-latest, r: 'release'} - - # Use 3.6 to trigger usage of RTools35 - # As of 2024-06, downlit now requires R >4.0.0 so this should be deprecated. - # - {os: windows-latest, cache: '~\AppData\Local\renv', r: '3.6'} - - # use 4.1 to check with rtools40's older compiler - - {os: windows-latest, r: '4.1'} - - - {os: ubuntu-latest, cache: '~/.local/share/renv', r: 'devel', http-user-agent: 'release'} - - {os: ubuntu-latest, cache: '~/.local/share/renv', r: 'release', cov: 'true'} - - {os: ubuntu-latest, cache: '~/.local/share/renv', r: 'oldrel-1'} - - {os: ubuntu-latest, cache: '~/.local/share/renv', r: 'oldrel-2'} - - {os: ubuntu-latest, cache: '~/.local/share/renv', r: 'oldrel-3'} - - {os: ubuntu-latest, cache: '~/.local/share/renv', r: 'oldrel-4'} + ## Run CI only on ubuntu-latest as this is the os used to build lessons + - {os: ubuntu-latest, r: 'release', cov: 'true'} env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} R_KEEP_PKG_SOURCE: yes R_REMOTES_NO_ERRORS_FROM_WARNINGS: true - RENV_PATHS_ROOT: ${{ matrix.config.cache }} steps: - - name: Record Linux Version - if: runner.os == 'Linux' - run: | - echo "OS_VERSION=`lsb_release -sr`" >> $GITHUB_ENV - mkdir -p "${{ runner.temp }}/sandbox/" - echo "RENV_PATHS_SANDBOX=${{ runner.temp }}/sandbox/" >> $GITHUB_ENV - - - name: "Windows: prevent autocrlf" - if: runner.os == 'Windows' - run: git config --global core.autocrlf false - shell: bash - - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: r-lib/actions/setup-pandoc@v2 - - name: "Windows: setup TMPDIR" - if: runner.os == 'Windows' - run: | - git config --global core.autocrlf true # reset setting to prevent errors downstream - echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV - shell: bash + - name: "Set up Chrome" # for PDF rendering + uses: browser-actions/setup-chrome@v1.6.0 - name: "Setup R" uses: r-lib/actions/setup-r@v2 @@ -86,25 +56,6 @@ jobs: extra-packages: any::rcmdcheck needs: check - - name: "Restore {renv} cache" - if: runner.os != 'Windows' - uses: actions/cache@v3 - with: - path: ${{ env.RENV_PATHS_ROOT }} - key: ${{ runner.os }}-${{ env.OS_VERSION }}-renv-${{ runner.r }}-${{ hashFiles('.github/workflows/R-CMD-check.yaml') }} - restore-keys: | - ${{ runner.os }}-${{ env.OS_VERSION }}-renv-${{ runner.r }}- - - - name: "Prime {renv} Cache" - if: runner.os != 'Windows' - run: | - renv::settings$snapshot.type("explicit") - renv::init() - system('sudo rm -rf renv.lock renv .Rprofile') - system('git clean -fd -e .github') - system('git restore .') - shell: Rscript {0} - - name: "Session info" run: | options(width = 100) @@ -114,17 +65,5 @@ jobs: - name: "Check" uses: r-lib/actions/check-r-package@v2 - if: runner.os != 'Windows' - with: - upload-snapshots: true - - - name: "Check" - uses: r-lib/actions/check-r-package@v2 - if: runner.os == 'Windows' - env: - RENV_PATHS_ROOT: ~ with: upload-snapshots: true - - - diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 8630b5628..548703cfa 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -20,7 +20,7 @@ jobs: env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: r-lib/actions/setup-pandoc@v2 diff --git a/.github/workflows/pr-commands.yaml b/.github/workflows/pr-commands.yaml index 71f335b3e..f3267b874 100644 --- a/.github/workflows/pr-commands.yaml +++ b/.github/workflows/pr-commands.yaml @@ -14,7 +14,7 @@ jobs: env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: r-lib/actions/pr-fetch@v2 with: diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml index 610b58750..c910c6608 100644 --- a/.github/workflows/test-coverage.yaml +++ b/.github/workflows/test-coverage.yaml @@ -2,9 +2,9 @@ # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help on: push: - branches: [main, master] + branches: [main, master, l2d] pull_request: - branches: [main, master] + branches: [main, master, l2d] workflow_dispatch: name: test-coverage @@ -14,11 +14,10 @@ jobs: runs-on: ubuntu-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - RENV_PATHS_ROOT: ~/.local/share/renv OS_VERSION: 1 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: r-lib/actions/setup-r@v2 with: @@ -27,6 +26,9 @@ jobs: - name: "Set up pandoc" uses: r-lib/actions/setup-pandoc@v2 + - name: "Set up Chrome" # for PDF rendering + uses: browser-actions/setup-chrome@v1.2.0 + - name: "Install sysreqs" id: run-apt shell: bash @@ -57,23 +59,6 @@ jobs: extra-packages: any::covr needs: coverage - - name: Restore {renv} cache - uses: actions/cache@v3 - with: - path: ${{ env.RENV_PATHS_ROOT }} - key: ${{ runner.os }}-${{ env.OS_VERSION }}-renv-${{ runner.r }}-${{ hashFiles('.github/workflows/R-CMD-check.yaml') }} - restore-keys: | - ${{ runner.os }}-${{ env.OS_VERSION }}-renv-${{ runner.r }}- - - - name: Prime {renv} Cache - run: | - renv::settings$snapshot.type("explicit") - renv::init() - system('rm -rf renv .Rprofile') - system('git clean -fd -e .github') - system('git restore .') - shell: Rscript {0} - - name: Test coverage run: | covr::codecov( diff --git a/.github/workflows/update-dots.yaml b/.github/workflows/update-dots.yaml index a5f25cffa..3ad4c53b7 100644 --- a/.github/workflows/update-dots.yaml +++ b/.github/workflows/update-dots.yaml @@ -2,7 +2,7 @@ name: Render Dotfiles on: push: - paths: + paths: - vignettes/articles/img/*dot - .github/workflows/update-docs.yaml @@ -11,8 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.5.0 - + - uses: actions/checkout@v4 + - name: Setup Graphviz uses: ts-graphviz/setup-graphviz@v1 diff --git a/.renovaterc.json5 b/.renovaterc.json5 new file mode 100644 index 000000000..b92a80d63 --- /dev/null +++ b/.renovaterc.json5 @@ -0,0 +1,5 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>UCL-ARC/.github//renovate/default-config.json"], + "reviewers": ["milanmlft"] +} diff --git a/DESCRIPTION b/DESCRIPTION index 11a300a29..261e3ca6c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: sandpaper Title: Create and Curate Carpentries Lessons -Version: 0.16.5 +Version: 0.16.5.90000-1 Authors@R: c( person(given = "Robert", family = "Davey", @@ -61,6 +61,7 @@ Description: We provide tools to build a Carpentries-themed lesson repository those designed to be used in a continuous integration context so that all the lesson author needs to focus on is writing the content of the actual lesson. License: MIT + file LICENSE +Depends: R (>= 4.0) Imports: pkgdown (>= 1.6.0), pegboard (>= 0.7.0), @@ -85,7 +86,8 @@ Imports: callr, servr, utils, - tools + tools, + pagedown, Suggests: testthat (>= 3.0.0), covr, @@ -96,11 +98,15 @@ Suggests: jsonlite, sessioninfo, mockr, - varnish (>= 0.3.0) + varnish (>= 0.3.0), + reticulate, + jupytextR (>= 0.1.0), + BiocManager Additional_repositories: https://carpentries.r-universe.dev/ Remotes: - carpentries/pegboard, - carpentries/varnish + carpentries/pegboard, + LearnToDiscover/varnish, + milanmlft/jupytextR SystemRequirements: pandoc (>= 2.11.4) - https://pandoc.org Encoding: UTF-8 LazyData: true @@ -109,7 +115,7 @@ Config/testthat/parallel: false Config/Needs/check: rstudio/renv Config/potools/style: explicit Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.1 URL: https://carpentries.github.io/sandpaper/, https://github.com/carpentries/sandpaper/, https://carpentries.github.io/workbench/ BugReports: https://github.com/carpentries/sandpaper/issues/ VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index b97363e27..e0ade642d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -25,6 +25,7 @@ export(move_episode) export(no_package_cache) export(package_cache_trigger) export(pin_version) +export(py_install) export(renv_diagnostics) export(reset_episodes) export(reset_site) @@ -37,6 +38,7 @@ export(set_instructors) export(set_learners) export(set_profiles) export(strip_prefix) +export(template_citation) export(template_conduct) export(template_config) export(template_contributing) @@ -55,6 +57,7 @@ export(update_cache) export(update_github_workflows) export(update_varnish) export(use_package_cache) +export(use_python) export(validate_lesson) export(work_with_cache) importFrom(assertthat,validate_that) diff --git a/R/build_episode.R b/R/build_episode.R index 6e722f127..aa82a6195 100644 --- a/R/build_episode.R +++ b/R/build_episode.R @@ -144,6 +144,7 @@ get_nav_data <- function(path_md, path_src = NULL, home = NULL, minutes = as.integer(yaml$teaching) + as.integer(yaml$exercises), file_source = fs::path_rel(path_src, start = home), this_page = fs::path_file(this_page), + this_page_pdf = fs::path_file(as_pdf(this_page)), page_back = page_back, back_title = pb_title, page_forward = page_forward, @@ -206,6 +207,7 @@ build_episode_md <- function(path, hash = NULL, outdir = path_built(path), md <- fs::path_ext_set(fs::path_file(path), "md") outpath <- fs::path(outdir, md) + # Set up the arguments # shortcut if we have a markdown file if (file_ext(path) == "md") { file.copy(path, outpath, overwrite = TRUE) @@ -233,7 +235,9 @@ build_episode_md <- function(path, hash = NULL, outdir = path_built(path), # ========================================================== # # Note that this process can NOT use any internal functions + sho <- !(quiet || identical(Sys.getenv("TESTTHAT"), "true")) + callr::r( func = callr_build_episode_md, args = args, @@ -246,3 +250,41 @@ build_episode_md <- function(path, hash = NULL, outdir = path_built(path), invisible(outpath) } + + +#' Convert a single episode to a Jupyter notebook +#' +#' @param path path to the RMarkdown file. +#' @param outdir the directory to write to/ +#' @param quiet if `TRUE`, output is suppressed, default is `TRUE` so we can use better +#' formatting elsewhere. +#' +#' @return the path to the output, invisibly +#' @keywords internal +build_episode_ipynb <- function(path, outdir = path_built(path), + profile = "lesson-requirements", quiet = TRUE) { + + if (!rlang::is_installed("jupytextR", version = "0.1.0")) { + rlang::inform("jupytextR is not installed, attempting to install it.") + BiocManager::install("milanmlft/jupytextR") + } + + outfile <- fs::path_ext_set(fs::path_file(path), "ipynb") + outpath <- fs::path(outdir, outfile) + + args <- list(input = path, output = outpath, quiet = quiet) + sho <- !(quiet || identical(Sys.getenv("TESTTHAT"), "true")) + + callr::r( + func = function(input, output, quiet) { + jupytextR::jupytext(input = input, to = "ipynb", output = output, quiet = quiet) + }, + args = args, + show = !quiet, + spinner = sho, + env = c(callr::rcmd_safe_env(), + "RENV_PROFILE" = profile, + "RENV_CONFIG_CACHE_SYMLINKS" = renv_cache_available()) + ) + invisible(outpath) +} diff --git a/R/build_ipynb.R b/R/build_ipynb.R new file mode 100644 index 000000000..bf9294747 --- /dev/null +++ b/R/build_ipynb.R @@ -0,0 +1,68 @@ +#' Build ipynb notebooks from the RMarkdown episodes +#' +#' Convert the RMarkdown episodes to Jupyter notebooks using `jupytext`. +#' Only the actual episodes are converted, not the additional pages. +#' +#' @param path the path to your repository (defaults to your current working +#' directory) +#' @param rebuild if `TRUE`, everything will be built from scratch as if there +#' was no cache. Defaults to `FALSE`, which will only build ipynb files that +#' haven't been built before. +#' @param quiet when `TRUE`, output is supressed. +#' +#' @keywords internal +#' @seealso [build_episode_ipynb()] +build_ipynb <- function(path = ".", rebuild = FALSE, quiet = FALSE) { + if (rebuild) { + reset_ipynb(path) + } else { + create_site(path) + } + + outdir <- path_built(path) + + ## We only want to build ipynb for the actual episodes + episodes <- get_episodes(path) + episode_paths <- fs::path(path_episodes(path), episodes) + + ## Determine episodes to rebuild + db_path <- fs::path(outdir, "md5sum-ipynb.txt") + db <- build_status(episode_paths, db_path, rebuild, write = FALSE, format = "ipynb") + + update <- FALSE + on.exit({if (update) write_build_db(db$new, db_path)}, add = TRUE) + + cli::cli_div(theme = sandpaper_cli_theme()) + + needs_building <- fs::path_ext(db$build) %in% c("md", "Rmd") + if (any(needs_building)) { + build_me <- db$build[needs_building] + + for (i in seq_along(build_me)) { + if (!quiet) cli::cli_alert_info("Converting {episodes[i]} to ipynb") + build_episode_ipynb(path = episode_paths[i], outdir = outdir) + } + } else { + if (!quiet) { + cli::cli_alert_success("All files up-to-date; nothing to rebuild!") + } + } + + cli::cli_end() + + # We've made it this far, so the database can be updated + update <- TRUE + invisible(db$build) +} + +## Clear existing ipynb files +reset_ipynb <- function(path) { + check_lesson(path) + to_delete <- get_ipynb_files(path) + fs::file_delete(to_delete) +} + +get_ipynb_files <- function(path) { + path <- path_built(path) + fs::dir_ls(path, glob = "*.ipynb") +} diff --git a/R/build_lesson.R b/R/build_lesson.R index be5c12452..d6d28342e 100644 --- a/R/build_lesson.R +++ b/R/build_lesson.R @@ -3,6 +3,7 @@ #' This function orchestrates rendering generated lesson content and applying #' the theme for the HTML site. #' +#' #' @param path the path to your repository (defaults to your current working #' directory) #' @param rebuild if `TRUE`, everything will be built from scratch as if there @@ -99,7 +100,6 @@ build_lesson <- function(path = ".", rebuild = FALSE, quiet = !interactive(), pr # This step uses the contents of `site/built` to build the website in # `site/docs` with {whisker} and {pkgdown} build_site(path = path, quiet = quiet, preview = preview, override = override, slug = slug, built = built) - } # Determine the build slug for lessons with child documents. diff --git a/R/build_markdown.R b/R/build_markdown.R index 45014a7d1..3ca13ee85 100644 --- a/R/build_markdown.R +++ b/R/build_markdown.R @@ -72,6 +72,8 @@ build_markdown <- function(path = ".", rebuild = FALSE, quiet = FALSE, slug = NU cli::cli_div(theme = sandpaper_cli_theme()) # Only build if there are markdown sources to be built. needs_building <- fs::path_ext(db$build) %in% c("md", "Rmd") + + if (any(needs_building)) { # Render the episode files to the built directory -------------------------- renv_check_consent(path, quiet, sources) @@ -107,11 +109,13 @@ build_markdown <- function(path = ".", rebuild = FALSE, quiet = FALSE, slug = NU if (should_build_handout) { build_handout(path, out = handout) } + } else { if (!quiet) { cli::cli_alert_success("All files up-to-date; nothing to rebuild!") } } + cli::cli_end() # Update hash of {renv} file if it exists ------------------------------------ @@ -137,6 +141,7 @@ build_markdown <- function(path = ".", rebuild = FALSE, quiet = FALSE, slug = NU } } + # We've made it this far, so the database can be updated update <- TRUE invisible(db$build) @@ -184,6 +189,10 @@ get_build_sources <- function(path, outdir, slug = NULL, quiet) { # filter out the assets (e.g. child files) from the source list no_asset <- names(source_list) %nin% c("files", "data", "fig") sources <- unlist(source_list[no_asset], use.names = FALSE) + + # filter out unreleased episodes + sources <- filter_out_unreleased(sources, get_config(path)) + names(sources) <- get_slug(sources) if (is.null(slug)) { copy_maybe(sources[["config"]], fs::path(outdir, "config.yaml")) diff --git a/R/build_site.R b/R/build_site.R index b0e90de5e..4c83ba42a 100644 --- a/R/build_site.R +++ b/R/build_site.R @@ -32,6 +32,8 @@ build_site <- function(path = ".", quiet = !interactive(), preview = TRUE, overr # does not exist or update the CSS, HTML, and JS if it does exist. pkg <- pkgdown::as_pkgdown(path_site(path), override = override) built_path <- fs::path(pkg$src_path, "built") + + # NOTE: This is a kludge to prevent pkgdown from displaying a bunch of noise # if the user asks for quiet. if (quiet) { @@ -47,6 +49,14 @@ build_site <- function(path = ".", quiet = !interactive(), preview = TRUE, overr # NOTE: future plans to reduce build times rebuild_template <- TRUE || !template_check$valid() + # Building jupyter notebooks, if required ------------------------------------ + + cfg <- get_config(path) + if (cfg$ipynb) { + describe_progress("Building jupyter notebooks", quiet = quiet) + built_ipynb <- build_ipynb(path = path, quiet = quiet) + } + # Determining what to rebuild ------------------------------------------------ describe_progress("Scanning episodes to rebuild", quiet = quiet) # @@ -55,6 +65,7 @@ build_site <- function(path = ".", quiet = !interactive(), preview = TRUE, overr db <- get_built_db(fs::path(built_path, "md5sum.txt")) # filter out files that we will combine to generate db <- reserved_db(db) + # Get absolute paths for pandoc to understand abs_md <- fs::path(path, db$built) abs_src <- fs::path(path, db$file) @@ -91,6 +102,7 @@ build_site <- function(path = ".", quiet = !interactive(), preview = TRUE, overr # restore the original functions on exit on.exit(eval(when_done), add = TRUE) } + # ------------------------ end downlit shim ---------------------------- for (i in files_to_render) { location <- page_location(i, abs_md, er) @@ -113,6 +125,16 @@ build_site <- function(path = ".", quiet = !interactive(), preview = TRUE, overr describe_progress("Creating 404 page", quiet = quiet) build_404(pkg, quiet = quiet) + + # Episode PDFs --------------------------------------------------------------- + if (cfg$pdf) { + describe_progress("Creating PDFs", quiet = quiet) + html_paths <- fs::path(pkg$dst_path, as_html(abs_md)) + for (i in files_to_render) { + html_to_pdf(html_paths[i], quiet = quiet) + } + } + # Combined pages ------------------------------------------------------------- # # There are two pages that are the result of source file combinations: @@ -148,6 +170,7 @@ build_site <- function(path = ".", quiet = !interactive(), preview = TRUE, overr # Once we have the pre-processed templates and HTML content, we can pass these # to our aggregator functions: + if (not_overview) { describe_progress("Creating keypoints summary", quiet = quiet) build_keypoints(pkg, pages = html_pages, quiet = quiet) @@ -155,6 +178,12 @@ build_site <- function(path = ".", quiet = !interactive(), preview = TRUE, overr describe_progress("Creating All-in-one page", quiet = quiet) build_aio(pkg, pages = html_pages, quiet = quiet) + if (cfg$pdf) { + describe_progress("Creating All-in-one PDF", quiet = quiet) + aio_html <- fs::path(pkg$dst_path, "aio.html") + html_to_pdf(input = aio_html, output = fs::path(pkg$dst_path, "aio.pdf"), quiet = quiet) + } + describe_progress("Creating Images page", quiet = quiet) build_images(pkg, pages = html_pages, quiet = quiet) } diff --git a/R/create_lesson.R b/R/create_lesson.R index b62d7e776..e26e97c4c 100644 --- a/R/create_lesson.R +++ b/R/create_lesson.R @@ -11,6 +11,18 @@ #' file extension in the lesson. #' @param rstudio create an RStudio project (defaults to if RStudio exits) #' @param open if interactive, the lesson will open in a new editor window. +#' @param add_python if set to `TRUE`, will add Python as a dependency for the +#' lesson. See [use_python()] for details. Defaults to `FALSE`. +#' @param python the path to the version of Python to be used. The default, +#' `NULL`, will prompt the user to select an appropriate version of Python in +#' interactive sessions. In non-interactive sessions, \pkg{renv} will attempt +#' to automatically select an appropriate version. See [renv::use_python()] +#' for more details. +#' @param type the type of Python environment to use. When `"auto"`, the +#' default, virtual environments will be used. See [renv::use_python()] for +#' more details. +#' @param pdf if `TRUE`, a PDF version of each episode will be built. +#' @param ipynb if `TRUE`, a Jupyter Notebook version of each episode will be built. #' #' @export #' @return the path to the new lesson @@ -19,7 +31,9 @@ #' on.exit(unlink(tmp)) #' lsn <- create_lesson(tmp, name = "This Lesson", open = FALSE) #' lsn -create_lesson <- function(path, name = fs::path_file(path), rmd = TRUE, rstudio = rstudioapi::isAvailable(), open = rlang::is_interactive()) { +create_lesson <- function(path, name = fs::path_file(path), rmd = TRUE, rstudio = rstudioapi::isAvailable(), open = rlang::is_interactive(), + add_python = FALSE, python = NULL, type = c("auto", "virtualenv", "conda", "system"), + pdf = FALSE, ipynb = FALSE) { path <- fs::path_abs(path) id <- cli::cli_status("{cli::symbol$arrow_right} Creating Lesson in {.file {path}}...") @@ -67,6 +81,8 @@ create_lesson <- function(path, name = fs::path_file(path), rmd = TRUE, rstudio source = glue::glue("https://github.com/{account}/{basename(path)}"), branch = get_default_branch(), contact = "team@carpentries.org", + pdf = if (pdf) "yes" else "no", + ipynb = if (ipynb) "yes" else "no", NULL ) ) @@ -93,6 +109,11 @@ create_lesson <- function(path, name = fs::path_file(path), rmd = TRUE, rstudio if (has_consent) { cli::cli_status_update("{cli::symbol$arrow_right} Managing Dependencies ...") manage_deps(path, snapshot = TRUE) + + if (add_python) { + cli::cli_status_update("{cli::symbol$arrow_right} Setting up Python ...") + use_python(path = path, python = python, type = type, open = FALSE) + } } cli::cli_status_update("{cli::symbol$arrow_right} Committing ...") diff --git a/R/manage_deps.R b/R/manage_deps.R index 8a65efcbf..b53a736b7 100644 --- a/R/manage_deps.R +++ b/R/manage_deps.R @@ -119,7 +119,7 @@ update_cache <- function(path = ".", profile = "lesson-requirements", prompt = i if (isTRUE(updates)) { return(invisible()) } - if (packageVersion("renv") < "0.17.1") { + if (utils::packageVersion("renv") < "0.17.1") { wanna_update <- "Do you want to update the following packages?" cli::cli_alert(wanna_update) ud <- utils::capture.output(print(updates)) diff --git a/R/test-fixtures.R b/R/test-fixtures.R index c6aab8785..838239eaf 100644 --- a/R/test-fixtures.R +++ b/R/test-fixtures.R @@ -9,6 +9,9 @@ #' scope section of testthat article on Test #' Fixtures](https://testthat.r-lib.org/articles/test-fixtures.html#package). #' +#' @param pdf logical; whether to create PDFs for the lesson's episodes +#' @param ipynb logical; whether to create Jupyter notebooks for the lesson's episodes +#' #' @details #' #' ## `create_test_lesson()` @@ -16,10 +19,10 @@ #' This creates the test lesson and calls `generate_restore_fixture()` with the #' path of the new test lesson. #' -#' @note These are implemented in tests/testthat/setup.md +#' @note These are implemented in tests/testthat/setup.R #' @keywords internal #' @rdname fixtures -create_test_lesson <- function() { +create_test_lesson <- function(pdf = FALSE, ipynb = FALSE) { noise <- interactive() || Sys.getenv("CI") == "true" if (noise) { t1 <- Sys.time() @@ -42,7 +45,7 @@ create_test_lesson <- function() { suppressMessages({ withr::with_envvar(list(RENV_CONFIG_CACHE_SYMLINKS = FALSE), { renv_output <- utils::capture.output( - create_lesson(repo, open = FALSE) + create_lesson(repo, open = FALSE, pdf = pdf, ipynb = ipynb) ) }) }) diff --git a/R/use_python.R b/R/use_python.R new file mode 100644 index 000000000..d84e3bb2f --- /dev/null +++ b/R/use_python.R @@ -0,0 +1,147 @@ +#' Add Python as a lesson dependency +#' +#' Associate a version of Python with your lesson. This is essentially a wrapper +#' around [renv::use_python()]. +#' +#' @param path path to the current project +#' @inheritParams renv::use_python +#' @param open if interactive, the lesson will open in a new editor window. +#' @param ... Further arguments to be passed on to [renv::use_python()] +#' +#' @details +#' This helper function adds Python as a dependency to the \pkg{renv} lockfile +#' and installs a Python environment of the specified `type`. This ensures any +#' Python packages used for this lesson are installed separately from the user's +#' main library, much like the R packages (see [manage_deps()]). +#' +#' Note that \pkg{renv} is not (yet) able to automatically detect Python package +#' dependencies (e.g. from `import` statements). So any required Python packages +#' still need to be installed manually. To facilitate this, the [py_install()] +#' helper is provided. This will install Python packages in the correct +#' environment and record them in a `requirements.txt` file, which will be +#' tracked by \pkg{renv}. Subsequent calls of [manage_deps()] will then +#' correctly restore the required Python packages if needed. +#' +#' @export +#' @rdname use_python +#' @seealso [renv::use_python()], [py_install()] +#' @return The path to the Python executable. Note that this function is mainly +#' called for its side effects. +#' @examples +#' \dontrun{ +#' tmp <- tempfile() +#' on.exit(unlink(tmp)) +#' +#' ## Create lesson with Python support +#' lsn <- create_lesson(tmp, name = "This Lesson", open = FALSE, add_python = TRUE) +#' lsn +#' +#' ## Add Python as a dependency to an existing lesson +#' setwd(lsn) +#' use_python() +#' +#' ## Install Python packages and record them as dependencies +#' py_install("numpy") +#' } +use_python <- function(path = ".", python = NULL, + type = c("auto", "virtualenv", "conda", "system"), + open = rlang::is_interactive(), ...) { + ## Make sure reticulate is installed + install_reticulate(path = path) + + ## Generate function to run in separate R process + use_python_with_renv <- function(path, python, type, ...) { + prof <- Sys.getenv("RENV_PROFILE") + renv::use_python(project = path, python = python, type = type, ...) + + ## NOTE: use_python() deactivates the default profile, + ## see https://github.com/rstudio/renv/issues/1217 + ## Workaround: re-activate the profile + renv::activate(project = path, profile = prof) + } + callr_use_python <- with_renv_factory(use_python_with_renv, + renv_path = path, renv_profile = "lesson-requirements" + ) + + ## Run in separate R process + callr::r( + func = function(f, path, python, type, ...) f(path = path, python = python, type = type, ...), + args = list(f = callr_use_python, path = path, python = python, type = type, ...), + show = TRUE + ) + + if (open) { + if (usethis::proj_activate(path)) { + on.exit() + } + } + invisible(path) +} + + +#' Install Python packages and add them to the cache +#' +#' To add Python packages, `py_install()` is provided, which installs Python +#' packages with [reticulate::py_install()] and then records them in the renv +#' environment. This ensures [manage_deps()] keeps track of the Python packages +#' as well. +#' +#' @param packages Python packages to be installed as a character vecto. +#' @param path path to your lesson. Defaults to the current working directory. +#' @param ... Further arguments to be passed to [reticulate::py_install()] +#' +#' @export +#' @rdname use_python +py_install <- function(packages, path = ".", ...) { + ## Ensure reticulate is installed + install_reticulate(path = path) + + py_install_with_renv <- function(packages, ...) { + reticulate::py_install(packages = packages, ...) + cli::cli_alert("Updating the package cache") + renv::snapshot(prompt = FALSE) + } + callr_py_install <- with_renv_factory(py_install_with_renv, + renv_path = path, renv_profile = "lesson-requirements" + ) + + ## Run in separate R process + callr::r( + func = function(f, packages) f(packages = packages), + args = list(f = callr_py_install, packages = packages), + show = TRUE + ) + + invisible(TRUE) +} + + +## Helper to install reticulate in the lesson's renv environment and record it as a dependency +install_reticulate <- function(path, quiet = FALSE) { + if (!check_reticulate_installable()) { + cli::cli_alert("`reticulate` can not be installed on this system. Skipping installation.") + return(invisible(FALSE)) + } + + ## Record reticulate as a dependency for renv + dep_file <- fs::path(path, "dependencies.R") + fs::file_create(dep_file) + line <- "library(reticulate)" + if (!(line %in% readLines(dep_file))) { + write(line, file = dep_file, append = TRUE) + } + + ## Install reticulate through manage_deps() + manage_deps(path = path, quiet = quiet) + + invisible(TRUE) +} + +check_reticulate_installable <- function(minimal_major = 4) { + minimal_major <- 4 + r_compatible <- R.version$major >= minimal_major + if (!r_compatible) { + cli::cli_warn("R version {minimal_major}.0 or higher is required for reticulate") + } + r_compatible +} diff --git a/R/utils-aggregate.R b/R/utils-aggregate.R index b21b44315..d5ffb642b 100644 --- a/R/utils-aggregate.R +++ b/R/utils-aggregate.R @@ -245,6 +245,8 @@ build_agg_page <- function(pkg, pages, title = NULL, slug = NULL, aggregate = "s xml2::xml_remove(xml2::xml_children(instruct_parent)) the_episodes <- .resources$get()[["episodes"]] + the_episodes <- filter_out_unreleased(the_episodes, get_config(path)) + the_slugs <- get_slug(the_episodes) the_slugs <- if (prefix) paste0(slug, "-", the_slugs) else the_slugs diff --git a/R/utils-built-db.R b/R/utils-built-db.R index 8c864eb8d..dc618be0d 100644 --- a/R/utils-built-db.R +++ b/R/utils-built-db.R @@ -213,6 +213,8 @@ get_lineages <- function(lsn) { #' FALSE) #' @param write if TRUE, the database will be updated, Defaults to FALSE, #' meaning that the database will remain the same. +#' @param format the format of the built files. Either `"md"` (the default) for Markdown, or +#' `"ipynb"` for Jupyter notebooks. #' @return a list of the following elements #' - *build* absolute paths of files to build #' - *new* a new data frame with three columns: @@ -280,7 +282,10 @@ get_lineages <- function(lsn) { #' cat("Goodbye!\n", append = TRUE, #' file = fs::path(tmp, "episodes", "files", "hi.md")) #' sp$build_status(resources, db, write = TRUE) -build_status <- function(sources, db = "site/built/md5sum.txt", rebuild = FALSE, write = FALSE) { +build_status <- function(sources, db = "site/built/md5sum.txt", rebuild = FALSE, write = FALSE, + format = c("md", "ipynb")) { + format <- match.arg(format) + # Modified on 2021-03-10 from blogdown::filter_md5sum version 1.2 # Original author: Yihui Xie # My additional commands use arrows. @@ -301,7 +306,7 @@ build_status <- function(sources, db = "site/built/md5sum.txt", rebuild = FALSE, built <- fs::path(built_path, fs::path_file(sources)) built <- ifelse( fs::path_ext(built) %in% c("Rmd", "rmd"), - fs::path_ext_set(built, "md"), built + fs::path_ext_set(built, format), built ) date <- format(Sys.Date(), "%F") # calculate checksums ------------------------------------------------------- @@ -400,5 +405,3 @@ build_status <- function(sources, db = "site/built/md5sum.txt", rebuild = FALSE, old = old ) } - - diff --git a/R/utils-callr.R b/R/utils-callr.R index 4304da835..f54071af5 100644 --- a/R/utils-callr.R +++ b/R/utils-callr.R @@ -71,4 +71,5 @@ callr_build_episode_md <- function(path, hash, workenv, outpath, workdir, root, quiet = quiet, encoding = "UTF-8" ) + } diff --git a/R/utils-html_to_pdf.R b/R/utils-html_to_pdf.R new file mode 100644 index 000000000..f78283014 --- /dev/null +++ b/R/utils-html_to_pdf.R @@ -0,0 +1,44 @@ +html_to_pdf <- function(input, output = fs::path_ext_set(input, "pdf"), quiet = FALSE, ...) { + rlang::check_installed("pagedown") + + chrome_available <- check_chrome_available() + if (!chrome_available) { + return(invisible(FALSE)) + } else { + if (!quiet) cli::cli_text("Converting {.file {basename(input)}} to {.file {basename(output)}}") + try_chrome_print(input = input, output = output, ...) + } + invisible(file.exists(output)) +} + +try_chrome_print <- function(input, output, ...) { + tryCatch({ + pagedown::chrome_print(input = input, output = output, format = "pdf", ...) + }, error = function(e) { + cli::cli_warn(c( + "x" = "chrome_print failed to write {.file {basename(output)}} with the following error:", + " " = "{.emph '{e$message}'}" + )) + }) +} + +check_chrome_available <- function() { + browser <- find_browser() + chrome_available <- utils::file_test("-x", browser) + if (!chrome_available) { + cli::cli_warn(c( + "x" = "Chrome is not available on your system.", + " " = "Please install Chrome and try again.", + "i" = "See https://www.google.com/chrome/ for more information." + )) + } + chrome_available +} + +find_browser <- function() { + ## Adapted from https://github.com/rstudio/pagedown/blob/466c1c1e8fc4a679aeff25bdd19fd834c0b78bbd/R/chrome.R#LL78C3-L82C4 + if (is.na(browser <- Sys.getenv("PAGEDOWN_CHROME", NA))) { + browser <- pagedown::find_chrome() + } + browser +} diff --git a/R/utils-paths-source.R b/R/utils-paths-source.R index 9ad426734..4f0c9d3a4 100644 --- a/R/utils-paths-source.R +++ b/R/utils-paths-source.R @@ -147,6 +147,7 @@ get_resource_list <- function(path, trim = FALSE, subfolder = NULL, warn = FALSE res[[i]] <- parse_file_matches(reality = res[[i]], hopes = cfg[[i]], warn = warn, subfolder = i) } + if (use_subfolder) res[[subfolder]] else res[names(res) != "site"] } diff --git a/R/utils-releases.R b/R/utils-releases.R new file mode 100644 index 000000000..1d327fdfb --- /dev/null +++ b/R/utils-releases.R @@ -0,0 +1,35 @@ +#' Filter out unreleased episodes +#' +#' @param episodes A character vector of file paths +#' @param lesson_config A list of lesson configuration, as returned by [`get_config()`] +#' +#' @details +#' The `lesson_config` list is expected to have a `releases` component, which is a named list of +#' episodes, with the names being the release dates in the format `YYYY-MM-DD`. This function +#' filters out any episodes that are scheduled to be released in the future. +#' +#' The lessons `config.yaml` file is expected to have a `releases` component, with the following structure: +#' +#' ```yaml +#' releases: +#' "2024-05-01": introduction.Rmd +#' "2025-05-01": +#' - not-yet-released.Rmd +#' - not-yet-released-2.Rmd +#' ``` +#' +#' @return A character vector of file paths, excluding unreleased episodes +#' @keywords internal +filter_out_unreleased <- function(episodes, lesson_config) { + + releases <- lesson_config$releases + if (length(releases) == 0) { + return(episodes) + } + + today <- Sys.Date() + release_dates <- as.Date(names(releases)) + unreleased <- unlist(releases[release_dates > today], use.names = FALSE) + + episodes[!fs::path_file(episodes) %in% unreleased] +} diff --git a/R/utils-renv.R b/R/utils-renv.R index a9c23c3bf..c423d03a9 100644 --- a/R/utils-renv.R +++ b/R/utils-renv.R @@ -258,7 +258,7 @@ callr_manage_deps <- function(path, repos, snapshot, lockfile_exists) { #nocov end if (needs_hydration) { #nocov start - if (packageVersion("renv") == "0.17.2") { + if (utils::packageVersion("renv") == "0.17.2") { # 2023-03-24 ---- renv cannot find the right packages # # @@ -291,8 +291,12 @@ callr_manage_deps <- function(path, repos, snapshot, lockfile_exists) { # recorded. if (lockfile_exists) { cli::cli_alert("Restoring any dependency versions") - res <- renv::restore(project = path, library = renv_lib, - lockfile = renv_lock, prompt = FALSE) + # Load profile, this ensures Python dependencies also get restored + renv::load(project = path) + on.exit({ + invisible(utils::capture.output(renv::deactivate(project = path), type = "message")) + }, add = TRUE) + res <- renv::restore(project = path, prompt = FALSE) } if (snapshot) { # 3. Load the current profile, unloading it when we exit @@ -309,3 +313,32 @@ callr_manage_deps <- function(path, repos, snapshot, lockfile_exists) { } return(NULL) } + +#' Generate a function to run in a renv profile +#' +#' This is a [Function operator](https://adv-r.hadley.nz/function-operators.html) which will +#' generate a function that will run in a renv profile. This is useful for running code in a +#' separate R subprocess with [`callr::r()`], to avoid *renv* side effects related to interactive +#' sessions. +#' +#' @param func The function to be evaluated after loading the renv environment. +#' @param renv_path The path to the renv environment to load. Usually a directory created by +#' [`create_lesson()`] +#' @param renv_profile Optional profile to load. Defaults to "lesson-requirements". +#' @param ... Additional arguments to be passed to `func`. +#' +#' @return The result of evaluating `func(...)` after loading the renv environment. +#' +#' @keywords internal +with_renv_factory <- function(func, renv_path, renv_profile = "lesson-requirements") { + force(func); force(renv_path); force(renv_profile) + + function(...) { + renv_path <- normalizePath(renv_path) + withr::local_dir(renv_path) + withr::local_envvar(c("RENV_PROFILE" = renv_profile)) + renv::load(renv_path) + + func(...) + } +} diff --git a/R/utils-sidebar.R b/R/utils-sidebar.R index 6a5d93f33..90d053015 100644 --- a/R/utils-sidebar.R +++ b/R/utils-sidebar.R @@ -110,26 +110,54 @@ create_sidebar_headings <- function(nodes) { if (inherits(nodes, "character")) { nodes <- xml2::read_html(nodes) } - # find all the div items that are purely section level 2 - h2 <- xml2::xml_find_all(nodes, ".//section/h2[@class='section-heading']") - have_children <- xml2::xml_length(h2) > 0 - txt <- xml2::xml_text(h2) - ids <- xml2::xml_attr(xml2::xml_parent(h2), "id") + # find all sections, each section corresponds to a single h2 heading + sections <- xml2::xml_find_all(nodes, ".//section") + txt <- character(length(sections)) + + for (i in seq_along(sections)) { + # find all the div items that are purely section level 2 + h2 <- xml2::xml_find_all(sections[[i]], ".//h2[@class='section-heading']") + # find all the div items that are purely section level 3 + # FIXME: the h3 IDs are being lost here + h3 <- xml2::xml_find_all(sections[[i]], ".//div[@class='section level3']/h3") + + h2_txt <- .handle_embedded_html(h2) + h2_id <- xml2::xml_attr(xml2::xml_parent(h2), "id") + + txt[[i]] <- .add_href(h2_txt, h2_id) + + if (length(h3)) { + h3_txt <- .handle_embedded_html(h3) + h3_ids <- xml2::xml_attr(xml2::xml_parent(h3), "id") + h3_txt <- paste0("
  • ", .add_href(h3_txt, h3_ids), "
  • ", collapse = "\n") + txt[[i]] <- paste0(txt[[i]], "\n", "") + } + } + + if (length(txt)) { + paste0("
  • ", txt, "
  • ", collapse = "\n") + } else { + NULL + } +} + +.handle_embedded_html <- function(heading_node) { + have_children <- xml2::xml_length(heading_node) > 0 + txt <- xml2::xml_text(heading_node) if (any(have_children)) { for (child in which(have_children)) { # Headings that have embedded HTML will need this - child_html <- xml2::xml_contents(h2[[child]]) + child_html <- xml2::xml_contents(heading_node[[child]]) no_anchor <- !xml2::xml_attr(child_html, "class") %in% "anchor" txt[child] <- paste(child_html[no_anchor], collapse = "") } } - if (length(ids) && length(txt)) { - paste0("
  • ", txt, "
  • ", - collapse = "\n" - ) - } else { - NULL - } + txt +} + +.add_href <- function(txt, id) { + stopifnot("The number of txt and id elements should be the same." = length(txt) == length(id)) + paste0("", txt, "") } #' Create the sidebar for varnish @@ -253,7 +281,7 @@ update_sidebar <- function( #' #' # Add an anchor to the links #' snd$fix_sidebar_href(my_links, scheme = "https", fragment = "anchor") -#' +#' #' # NOTE: this will _always_ return a character vector, even if the input is #' # incorrect #' snd$fix_sidebar_href(list(), server = "example.com") @@ -316,4 +344,3 @@ prepend <- function(first, sep = "#", last, trim = TRUE) { } return(ifelse(first == "", last, paste0(first, sep, last))) } - diff --git a/R/utils-varnish.R b/R/utils-varnish.R index e699261d7..00470cc62 100644 --- a/R/utils-varnish.R +++ b/R/utils-varnish.R @@ -73,6 +73,10 @@ set_globals <- function(path) { on.exit(.resources$set(key = NULL, old)) set_resource_list(path) these_resources <- .resources$get() + these_resources[["episodes"]] <- filter_out_unreleased( + these_resources[["episodes"]], + get_config(path) + ) # Sidebar information is largely duplicated across the views. The only thing # that is different is the name of the index node. @@ -108,9 +112,11 @@ set_globals <- function(path) { instructor$resources <- c(instructor$resources, "
    ", learner$extras) pkg_versions <- varnish_vars() + cfg <- get_config(path) learner_globals$set(key = NULL, c(list( aio = TRUE, + aio_pdf = cfg$pdf, instructor = FALSE, sidebar = learner_sidebar, more = paste(learner$extras, collapse = ""), @@ -121,6 +127,7 @@ set_globals <- function(path) { instructor_globals$set(key = NULL, c(list( aio = TRUE, + aio_pdf = cfg$pdf, instructor = TRUE, sidebar = instructor_sidebar, more = paste(instructor$extras, collapse = ""), diff --git a/R/utils-yaml.R b/R/utils-yaml.R index a160a85bb..45816370e 100644 --- a/R/utils-yaml.R +++ b/R/utils-yaml.R @@ -237,7 +237,9 @@ known_yaml_items <- c( "episodes", "instructors", "learners", - "profiles" + "profiles", + "pdf", + "ipynb" ) diff --git a/R/utils.R b/R/utils.R index 954ec28f7..35761ca9f 100644 --- a/R/utils.R +++ b/R/utils.R @@ -9,6 +9,12 @@ as_html <- function(i, instructor = FALSE) { if (instructor) fs::path("instructor", res) else res } +as_pdf <- function(i, instructor = FALSE) { + if (length(i) == 0) return(i) + res <- fs::path_ext_set(fs::path_file(i), "pdf") + if (instructor) fs::path("instructor", res) else res +} + example_can_run <- function(need_git = FALSE, skip_cran = TRUE) { no_need_git <- !need_git run_ok <- (no_need_git || has_git()) && diff --git a/README.Rmd b/README.Rmd index 24352b945..3db1b90d3 100644 --- a/README.Rmd +++ b/README.Rmd @@ -15,24 +15,32 @@ knitr::opts_chunk$set( ``` -[![R Universe](https://carpentries.r-universe.dev/badges/sandpaper)](https://carpentries.r-universe.dev/ui#builds) [![Codecov test coverage](https://codecov.io/gh/carpentries/sandpaper/branch/main/graph/badge.svg)](https://codecov.io/gh/carpentries/sandpaper?branch=main) [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://www.tidyverse.org/lifecycle/#experimental) -[![CRAN status](https://www.r-pkg.org/badges/version/sandpaper)](https://CRAN.R-project.org/package=sandpaper) -[![R-CMD-check](https://github.com/carpentries/sandpaper/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/carpentries/sandpaper/actions/workflows/R-CMD-check.yaml) +[![R-CMD-check](https://github.com/LearnToDiscover/sandpaper/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/LearnToDiscover/sandpaper/actions/workflows/R-CMD-check.yaml) The {sandpaper} package was created by [The Carpentries] to re-imagine our method -of creating lesson websites for our workshops. This package will take a series -of [Markdown] or [RMarkdown] files and generate a static website with the +of creating lesson websites for our workshops. This package will take a series +of [Markdown] or [RMarkdown] files and generate a static website with the features and styling of The Carpentries lessons including customized layouts and callout blocks. Much of the functionality in this package is inspired by [Jenny Bryan's](https://jennybryan.org/) work with the [{usethis}] package. +This fork contains a modified version of {sandpaper} specifically created for the [Learn to Discover](https://github.com/LearnToDiscover) project. +The main changes are: + +- Use L2D styling and logo for the lesson websites +- Add Jupyter notebooks for each episode +- Add PDF output for the entire lesson ([#27](https://github.com/LearnToDiscover/sandpaper/pull/27)) +- Add support for setting up Python in the package cache ([#15](https://github.com/LearnToDiscover/sandpaper/pull/15)) + ## Documentation -**Want to know how this works in a lesson format? Head over to -.** +**Want to know how this works in a lesson format? Head over to +.** + +For a quick overview, see If, instead, you already know how a lesson is built and are interested in understanding how the functions in {sandpaper} work, you can visit this package @@ -40,16 +48,11 @@ documentation site at . ## Installation -{sandpaper} is not currently on CRAN, but it can be installed from our -[Carpentries Universe](https://carpentries.r-universe.dev/ui#builds) (updated -every hour) with the following commands: +To download the **L2D** version of *{sandpaper}*, use the following command: -``` r -options(repos = c( - carpentries = "https://carpentries.r-universe.dev/", - CRAN = "https://cran.rstudio.com/" -)) -install.packages("sandpaper", dep = TRUE) +```r +# install.packages("devtools") +devtools::install_github("LearnToDiscover/sandpaper", dependencies = TRUE) ``` Note that this will also install development versions of the following packages: @@ -64,7 +67,7 @@ Note that this will also install development versions of the following packages: ## Design This package is designed to make the life of the lesson contributors and -maintainers easier by separating the tools needed to build the site from the +maintainers easier by separating the tools needed to build the site from the user-defined content of the site itself. It will no longer rely on Jekyll or any of the other [>450 static site generators](https://staticsitegenerators.net), but instead rely on R, RStudio, and [{pkgdown}] to generate a site with the @@ -98,14 +101,14 @@ issues surrounding out-of-date artefacts and directory structure confusion. The website is generated in two steps: 1. markdown files from the source files are rendered containing a hash for the - source file so that these need only be re-rendered when they change. + source file so that these need only be re-rendered when they change. 2. html files are generated from the rendered markdown files and the CSS and JS sources in the [{varnish}] package for the preview. To ensure there are no clashes between minor differences in the user setup, no artifacts are committed to the main branch of the repository. Because of the -caching mechanism between the website and the rendered markdown files, -long-running lessons can be updated and previewed quickly. +caching mechanism between the website and the rendered markdown files, +long-running lessons can be updated and previewed quickly. ### Rendering on continuous integration @@ -123,21 +126,21 @@ to be: - CI agnostic (but currently set up with GitHub) - easy to set up - auditable (e.g. I can see changes between the content of two commits) - - versionable (e.g. I can instruct learners to go to `/1.1`. This + - versionable (e.g. I can instruct learners to go to `/1.1`. This is inspired from the python documentation style) To acheive this, there will be two branches created: `md-outputs` and `gh-pages` that will inerit like so main -> `md-outputs` -> `gh-pages`. Because the build time from main to `md-outputs` can be time intensive, this will default to -updating only files that were changed. +updating only files that were changed. - `md-outputs`: this branch will contain the files and artifacts generated from - rmarkdown in the vignettes directory of a thin package skeleton. + rmarkdown in the vignettes directory of a thin package skeleton. - `gh-pages`: this branch is generated via `md-outputs` and bundles the html, css, and js for the website. This will contain a single `index.html` file with several subfolders with different versions of the site. The `index.html` file will redirect to the `current/` directory, which contains the up-to-date - site. + site. #### Scheduled builds @@ -145,11 +148,11 @@ updating only files that were changed. separated from the styling, we will set up the CI to generate the webpage from the pre-built sources on a weekly basis, which will check if there has been an update to the styles (which I have in the [{varnish}] package) and - then rebuild the site without rebuilding the content. + then rebuild the site without rebuilding the content. - `md-outputs` branch: This will be rerun every month from scratch with the most recent version of R and R packages. If there is a change, a pull request can be generated to update the `renv.lock` file with a link to the changed - markdown files in this branch. + markdown files in this branch. ### Function syntax @@ -193,7 +196,7 @@ Accessors **Continuous Integration Utilities** - `ci_deploy()` builds and deploys the lesson on CI from the source files - - `ci_build_markdown()` builds the markdown files on CI from the source and deploys them to the markdown branch. + - `ci_build_markdown()` builds the markdown files on CI from the source and deploys them to the markdown branch. - `ci_build_site()` deploys the lesson on CI from pre-rendered markdown files - `ci_release()` builds and deploys the lesson on CI from the source files and adds a release tag - `update_github_workflows()` updates GitHub workflows @@ -201,7 +204,7 @@ Accessors Cleanup - `reset_episodes()` removes the schedule from the config.yaml file - - `reset_site()` clears the website and cache + - `reset_site()` clears the website and cache ## Usage @@ -234,7 +237,7 @@ the following structure: |-- episodes/ # - PUT YOUR MARKDOWN FILES IN THIS FOLDER | |-- data/ # - Data for your lesson goes here | |-- figures/ # - All static figures and diagrams are here -| |-- files/ # - Additional files (e.g. handouts) +| |-- files/ # - Additional files (e.g. handouts) | `-- 00-introducition.Rmd # - Lessons start with a two-digit number |-- instructors/ # - Information for Instructors |-- learners/ # - Information for Learners @@ -265,10 +268,10 @@ sandpaper::build_lesson() ``` > #### Working in RStudio? -> +> > If you are using RStudio, you can preview the lesson site using the keyboard > shortcut ctrl + shift + B (which corresponds to the "Build Website" button in the "Build" tab. To preview individual files, you can use -> ctrl + shift + K (This corresponds to the "Knit" button in the editor pane) +> ctrl + shift + K (This corresponds to the "Knit" button in the editor pane) This will create the website structure inside of the the `site/` folder, render the RMarkdown files to markdown (for inspection and quick rendering), render the @@ -279,14 +282,14 @@ markdown files to HTML, and then enable a preview within your browser window. To contribute to a lesson, you can either fork the lesson to your own repository and clone it to your computer manually from GitHub, or you can use the {usethis} -package to automate it. For example, This is how you can create a copy of +package to automate it. For example, This is how you can create a copy of [**Programming With R**](http://swcarpentry.github.io/r-novice-inflammation/) to your computer's Desktop. ```{r} usethis::create_from_github( - repo = "swcarpentry/r-novice-gapminder", + repo = "swcarpentry/r-novice-gapminder", destdir = "~/Desktop/r-novice-gampinder", fork = TRUE ) @@ -303,9 +306,9 @@ already have a copy on your machine. If not, follow the instructions in the The typical workflow will look like this: -1. open the sandpaper project in RStudio and make edits to files in the +1. open the sandpaper project in RStudio and make edits to files in the `episodes/` folder -2. in the R console run the following +2. in the R console run the following ```{r} @@ -325,8 +328,8 @@ sandpaper::build_portable_lesson(version = "current") ``` This will render a fully portable lesson site as a zip file in the `site/` -folder. You can distribute this lesson to learners who do not have reliable -internet access for use offline without sacrificing any of the styling. +folder. You can distribute this lesson to learners who do not have reliable +internet access for use offline without sacrificing any of the styling. ### Rendering with GitHub actions @@ -359,7 +362,7 @@ sandpaper::ci_release(tag = "0.1", md_branch = "md-outputs", site_branch = "gh-p [The Carpentries]: https://carpentries.org -[Markdown]: https://www.markdownguide.org/getting-started/ +[Markdown]: https://www.markdownguide.org/getting-started/ [RMarkdown]: https://rmarkdown.rstudio.com/ [{learnr}]: https://rstudio.github.io/learnr/index.html [{remotes}]: https://remotes.r-lib.org/ diff --git a/README.md b/README.md index ea57c7b01..3dca07951 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,11 @@ -[![R -Universe](https://carpentries.r-universe.dev/badges/sandpaper)](https://carpentries.r-universe.dev/ui#builds) [![Codecov test -coverage](https://codecov.io/gh/carpentries/sandpaper/branch/main/graph/badge.svg)](https://codecov.io/gh/carpentries/sandpaper?branch=main) +coverage](https://codecov.io/gh/LearnToDiscover/sandpaper/branch/l2d/graph/badge.svg)](https://codecov.io/gh/LearnToDiscover/sandpaper?branch=l2d) [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://www.tidyverse.org/lifecycle/#experimental) -[![CRAN -status](https://www.r-pkg.org/badges/version/sandpaper)](https://CRAN.R-project.org/package=sandpaper) -[![R-CMD-check](https://github.com/carpentries/sandpaper/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/carpentries/sandpaper/actions/workflows/R-CMD-check.yaml) +[![R-CMD-check](https://github.com/LearnToDiscover/sandpaper/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/LearnToDiscover/sandpaper/actions/workflows/R-CMD-check.yaml) The {sandpaper} package was created by [The @@ -26,11 +22,25 @@ functionality in this package is inspired by [Jenny Bryan’s](https://jennybryan.org/) work with the [{usethis}](https://usethis.r-lib.org/) package. +This fork contains a modified version of {sandpaper} specifically +created for the [Learn to Discover](https://github.com/LearnToDiscover) +project. The main changes are: + +- Use L2D styling and logo for the lesson websites +- Add Jupyter notebooks for each episode +- Add PDF output for the entire lesson + ([\#27](https://github.com/LearnToDiscover/sandpaper/pull/27)) +- Add support for setting up Python in the package cache + ([\#15](https://github.com/LearnToDiscover/sandpaper/pull/15)) + ## Documentation **Want to know how this works in a lesson format? Head over to .** +For a quick overview, see + + If, instead, you already know how a lesson is built and are interested in understanding how the functions in {sandpaper} work, you can visit this package documentation site at @@ -38,16 +48,12 @@ this package documentation site at ## Installation -{sandpaper} is not currently on CRAN, but it can be installed from our -[Carpentries Universe](https://carpentries.r-universe.dev/ui#builds) -(updated every hour) with the following commands: +To download the **L2D** version of *{sandpaper}*, use the following +command: ``` r -options(repos = c( - carpentries = "https://carpentries.r-universe.dev/", - CRAN = "https://cran.rstudio.com/" -)) -install.packages("sandpaper", dep = TRUE) +# install.packages("devtools") +devtools::install_github("LearnToDiscover/sandpaper", dependencies = TRUE) ``` Note that this will also install development versions of the following @@ -64,31 +70,33 @@ packages: This package is designed to make the life of the lesson contributors and maintainers easier by separating the tools needed to build the site from the user-defined content of the site itself. It will no longer rely on -Jekyll or any of the other [>450 static site +Jekyll or any of the other [\>450 static site generators](https://staticsitegenerators.net), but instead rely on R, RStudio, and [{pkgdown}](https://github.com/r-lib/pkgdown#readme) to generate a site with the following features: -- [x] optional offline use -- [x] filename-agnostic episode arrangements -- [x] clear definitions of package versions needed to build the lesson -- [ ] lesson versioning (e.g. I can navigate to - for the - current version and - for - the release in 2020-11) -- [x] seamless updates to the Carpentries’ style -- [x] caching of rendered content for rapid deployment -- [ ] packaging of - [{learnr}](https://rstudio.github.io/learnr/index.html) materials -- [x] validation of lesson structure -- [x] git aware, but does not require contributors to have git - installed +- [x] optional offline use +- [x] filename-agnostic episode arrangements +- [x] clear definitions of package versions needed to build the lesson +- [ ] lesson versioning (e.g. I can navigate to + for the + current version and + for + the release in 2020-11) +- [x] seamless updates to the Carpentries’ style +- [x] caching of rendered content for rapid deployment +- [ ] packaging of + [{learnr}](https://rstudio.github.io/learnr/index.html) materials +- [x] validation of lesson structure +- [x] git aware, but does not require contributors to have git installed ### Rendering locally
    -diagram of three folders. The first folder, "episodes/", labelled as RMarkdown, has an arrow (labelled as hash episodes) pointing to "site/built/", labelled as Markdown. The Markdown folder has an arrow (labelled as "apply template") pointing to "site/docs/", labelled as "HTML". The first folder is labelled in pale yellow, indicating that it is the only one tracked by git. + +
    The local two-step model of deployment into local +folders
    In a repository generated via {sandpaper}, only the source is committed @@ -114,111 +122,111 @@ quickly. ### Rendering on continuous integration
    -Diagrammatic representation of the GitHub deployment cycle showing four branches, gh-pages, md-outputs, main, and my-edit. The my-edit branch is a direct descendent of the main branch, while the gh-pages and md-outputs branches are orphans. Each commit of the main branch has a process represented by a dashed arrow that builds a commit of the subsequent orphan branches + +
    Two-step deployment model on continuous +integration
    Continuous integration will act as the single source-of-truth for how the outputs of the lessons are rendered. For this, we want the resulting website to be: -- CI agnostic (but currently set up with GitHub) -- easy to set up -- auditable (e.g. I can see changes between the content of two - commits) -- versionable (e.g. I can instruct learners to go to `/1.1`. - This is inspired from the python documentation style) +- CI agnostic (but currently set up with GitHub) +- easy to set up +- auditable (e.g. I can see changes between the content of two commits) +- versionable (e.g. I can instruct learners to go to `/1.1`. + This is inspired from the python documentation style) To acheive this, there will be two branches created: `md-outputs` and -`gh-pages` that will inerit like so main -> `md-outputs` -> +`gh-pages` that will inerit like so main -\> `md-outputs` -\> `gh-pages`. Because the build time from main to `md-outputs` can be time intensive, this will default to updating only files that were changed. -- `md-outputs`: this branch will contain the files and artifacts - generated from rmarkdown in the vignettes directory of a thin - package skeleton. -- `gh-pages`: this branch is generated via `md-outputs` and bundles - the html, css, and js for the website. This will contain a single - `index.html` file with several subfolders with different versions of - the site. The `index.html` file will redirect to the `current/` - directory, which contains the up-to-date site. +- `md-outputs`: this branch will contain the files and artifacts + generated from rmarkdown in the vignettes directory of a thin package + skeleton. +- `gh-pages`: this branch is generated via `md-outputs` and bundles the + html, css, and js for the website. This will contain a single + `index.html` file with several subfolders with different versions of + the site. The `index.html` file will redirect to the `current/` + directory, which contains the up-to-date site. #### Scheduled builds -- `gh-pages` website: Because we are designing the lessons to have - content separated from the styling, we will set up the CI to - generate the webpage from the pre-built sources on a weekly basis, - which will check if there has been an update to the styles (which I - have in the - [{varnish}](https://github.com/carpentries/varnish#readme) package) - and then rebuild the site without rebuilding the content. -- `md-outputs` branch: This will be rerun every month from scratch - with the most recent version of R and R packages. If there is a - change, a pull request can be generated to update the `renv.lock` - file with a link to the changed markdown files in this branch. +- `gh-pages` website: Because we are designing the lessons to have + content separated from the styling, we will set up the CI to generate + the webpage from the pre-built sources on a weekly basis, which will + check if there has been an update to the styles (which I have in the + [{varnish}](https://github.com/carpentries/varnish#readme) package) + and then rebuild the site without rebuilding the content. +- `md-outputs` branch: This will be rerun every month from scratch with + the most recent version of R and R packages. If there is a change, a + pull request can be generated to update the `renv.lock` file with a + link to the changed markdown files in this branch. ### Function syntax The functions in {sandpaper} have the following prefixes: -- `create_` will create/amend files or folders in your workspace -- `update_` will update build resources in the lesson -- `build_` will build files from your source -- `check_` validates either the elements of the lesson and/or episodes -- `fetch_` will download files or resources from the internet -- `reset_` removes files or information -- `get_` will retrieve information from your source files as an R - object -- `set_` will update information in files. -- `ci_` interacts with continous integration to build the website +- `create_` will create/amend files or folders in your workspace +- `update_` will update build resources in the lesson +- `build_` will build files from your source +- `validate_` will check the validity of either the elements of the + lesson and/or episodes +- `fetch_` will download files or resources from the internet +- `reset_` removes files or information +- `get_` will retrieve information from your source files as an R object +- `set_` will update information in files. +- `ci_` interacts with continous integration to build the website Here is a working list of user-facing functions: **Lesson and Episode Creation** -- `create_lesson()` creates a lesson from scratch -- `create_episode()` creates a new episode with the correct number - prefix -- `create_dataset()` creates a csv or text data set from an R object -- `set_episodes()` arranges the episodes in a user-specified order +- `create_lesson()` creates a lesson from scratch +- `create_episode()` creates a new episode with the correct number + prefix +- `create_dataset()` creates a csv or text data set from an R object +- `set_episodes()` arranges the episodes in a user-specified order Accessors -- `get_config()` reads the contents of `config.yaml` as a list -- `get_drafts()` reports files that are not listed in `config.yaml` -- `get_episodes()` returns the episode filenames as a vector -- `get_syllabus()` returns the syllabus with timings, titles, and - questions +- `get_config()` reads the contents of `config.yaml` as a list +- `get_drafts()` reports files that are not listed in `config.yaml` +- `get_episodes()` returns the episode filenames as a vector +- `get_syllabus()` returns the syllabus with timings, titles, and + questions **Website Creation and Validation** -- `check_lesson()` checks and validates the source files and lesson - structure -- `build_episode_md()` renders an individual file to markdown - (internal use) -- `build_episode_html()` renders a built markdown file to html - (internal use) -- `build_lesson()` builds the lesson into a static website -- `build_portable_lesson()` builds the lesson into a portable static - website -- `fetch_lesson()` fetches the static website from the lesson - repository +- `validate_lesson()` checks and validates the source files and lesson + structure +- `build_episode_md()` renders an individual file to markdown (internal + use) +- `build_episode_html()` renders a built markdown file to html (internal + use) +- `build_lesson()` builds the lesson into a static website +- `build_portable_lesson()` builds the lesson into a portable static + website +- `fetch_lesson()` fetches the static website from the lesson repository **Continuous Integration Utilities** -- `ci_deploy()` builds and deploys the lesson on CI from the source - files -- `ci_build_markdown()` builds the markdown files on CI from the - source and deploys them to the markdown branch. -- `ci_build_site()` deploys the lesson on CI from pre-rendered - markdown files -- `ci_release()` builds and deploys the lesson on CI from the source - files and adds a release tag -- `update_github_workflows()` updates GitHub workflows +- `ci_deploy()` builds and deploys the lesson on CI from the source + files +- `ci_build_markdown()` builds the markdown files on CI from the source + and deploys them to the markdown branch. +- `ci_build_site()` deploys the lesson on CI from pre-rendered markdown + files +- `ci_release()` builds and deploys the lesson on CI from the source + files and adds a release tag +- `update_github_workflows()` updates GitHub workflows Cleanup -- `reset_episodes()` removes the schedule from the config.yaml file -- `reset_site()` clears the website and cache +- `reset_episodes()` removes the schedule from the config.yaml file +- `reset_site()` clears the website and cache ## Usage @@ -250,7 +258,7 @@ with the following structure: |-- episodes/ # - PUT YOUR MARKDOWN FILES IN THIS FOLDER | |-- data/ # - Data for your lesson goes here | |-- figures/ # - All static figures and diagrams are here - | |-- files/ # - Additional files (e.g. handouts) + | |-- files/ # - Additional files (e.g. handouts) | `-- 00-introducition.Rmd # - Lessons start with a two-digit number |-- instructors/ # - Information for Instructors |-- learners/ # - Information for Learners @@ -304,7 +312,7 @@ computer’s Desktop. ``` r usethis::create_from_github( - repo = "swcarpentry/r-novice-gapminder", + repo = "swcarpentry/r-novice-gapminder", destdir = "~/Desktop/r-novice-gampinder", fork = TRUE ) @@ -341,7 +349,7 @@ The typical workflow will look like this: 2. in the R console run the following ``` r -sandpaper::check_lesson() # validates the structure of the input files +sandpaper::validate_lesson() # validates the structure of the input files sandpaper::build_lesson() # builds and validates lesson ``` diff --git a/_pkgdown.yml b/_pkgdown.yml index 87c5c4cd9..75c10b253 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -61,6 +61,7 @@ reference: - contents: - use_package_cache - manage_deps + - use_python - title: "Updating Lesson Tools" desc: > Lesson updates will happen automatically on a regular schedule on GitHub. @@ -138,6 +139,7 @@ articles: - title: "How to..." navbar: ~ contents: + - instructor-guide - building-with-renv - automated-pull-requests - include-child-documents @@ -151,4 +153,3 @@ articles: - title: "In Progress" contents: - articles/internationalisation - diff --git a/inst/pandoc/eisvogel.latex b/inst/pandoc/eisvogel.latex new file mode 100644 index 000000000..23a1b0a4b --- /dev/null +++ b/inst/pandoc/eisvogel.latex @@ -0,0 +1,1088 @@ +%% +% Copyright (c) 2017 - 2023, Pascal Wagler; +% Copyright (c) 2014 - 2023, John MacFarlane +% +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions +% are met: +% +% - Redistributions of source code must retain the above copyright +% notice, this list of conditions and the following disclaimer. +% +% - Redistributions in binary form must reproduce the above copyright +% notice, this list of conditions and the following disclaimer in the +% documentation and/or other materials provided with the distribution. +% +% - Neither the name of John MacFarlane nor the names of other +% contributors may be used to endorse or promote products derived +% from this software without specific prior written permission. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +% "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +% LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +% FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +% COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +% INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +% BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +% CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +% LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +% ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +%% + +%% +% This is the Eisvogel pandoc LaTeX template. +% +% For usage information and examples visit the official GitHub page: +% https://github.com/Wandmalfarbe/pandoc-latex-template +%% + +% Options for packages loaded elsewhere +\PassOptionsToPackage{unicode$for(hyperrefoptions)$,$hyperrefoptions$$endfor$}{hyperref} +\PassOptionsToPackage{hyphens}{url} +\PassOptionsToPackage{dvipsnames,svgnames,x11names,table}{xcolor} +$if(CJKmainfont)$ +\PassOptionsToPackage{space}{xeCJK} +$endif$ +% +\documentclass[ +$if(fontsize)$ + $fontsize$, +$endif$ +$if(papersize)$ + $papersize$paper, +$else$ + paper=a4, +$endif$ +$if(beamer)$ + ignorenonframetext, +$if(handout)$ + handout, +$endif$ +$if(aspectratio)$ + aspectratio=$aspectratio$, +$endif$ +$endif$ +$for(classoption)$ + $classoption$$sep$, +$endfor$ + ,captions=tableheading +]{$if(beamer)$$documentclass$$else$$if(book)$scrbook$else$scrartcl$endif$$endif$} +$if(beamer)$ +$if(background-image)$ +\usebackgroundtemplate{% + \includegraphics[width=\paperwidth]{$background-image$}% +} +% In beamer background-image does not work well when other images are used, so this is the workaround +\pgfdeclareimage[width=\paperwidth,height=\paperheight]{background}{$background-image$} +\usebackgroundtemplate{\pgfuseimage{background}} +$endif$ +\usepackage{pgfpages} +\setbeamertemplate{caption}[numbered] +\setbeamertemplate{caption label separator}{: } +\setbeamercolor{caption name}{fg=normal text.fg} +\beamertemplatenavigationsymbols$if(navigation)$$navigation$$else$empty$endif$ +$for(beameroption)$ +\setbeameroption{$beameroption$} +$endfor$ +% Prevent slide breaks in the middle of a paragraph +\widowpenalties 1 10000 +\raggedbottom +$if(section-titles)$ +\setbeamertemplate{part page}{ + \centering + \begin{beamercolorbox}[sep=16pt,center]{part title} + \usebeamerfont{part title}\insertpart\par + \end{beamercolorbox} +} +\setbeamertemplate{section page}{ + \centering + \begin{beamercolorbox}[sep=12pt,center]{part title} + \usebeamerfont{section title}\insertsection\par + \end{beamercolorbox} +} +\setbeamertemplate{subsection page}{ + \centering + \begin{beamercolorbox}[sep=8pt,center]{part title} + \usebeamerfont{subsection title}\insertsubsection\par + \end{beamercolorbox} +} +\AtBeginPart{ + \frame{\partpage} +} +\AtBeginSection{ + \ifbibliography + \else + \frame{\sectionpage} + \fi +} +\AtBeginSubsection{ + \frame{\subsectionpage} +} +$endif$ +$endif$ +$if(beamerarticle)$ +\usepackage{beamerarticle} % needs to be loaded first +$endif$ +\usepackage{amsmath,amssymb} +$if(linestretch)$ +\usepackage{setspace} +$else$ +% Use setspace anyway because we change the default line spacing. +% The spacing is changed early to affect the titlepage and the TOC. +\usepackage{setspace} +\setstretch{1.2} +$endif$ +\usepackage{iftex} +\ifPDFTeX + \usepackage[$if(fontenc)$$fontenc$$else$T1$endif$]{fontenc} + \usepackage[utf8]{inputenc} + \usepackage{textcomp} % provide euro and other symbols +\else % if luatex or xetex +$if(mathspec)$ + \ifXeTeX + \usepackage{mathspec} % this also loads fontspec + \else + \usepackage{unicode-math} % this also loads fontspec + \fi +$else$ + \usepackage{unicode-math} % this also loads fontspec +$endif$ + \defaultfontfeatures{Scale=MatchLowercase}$-- must come before Beamer theme + \defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1} +\fi +$if(fontfamily)$ +$else$ +$-- Set default font before Beamer theme so the theme can override it +\usepackage{lmodern} +$endif$ +$-- Set Beamer theme before user font settings so they can override theme +$if(beamer)$ +$if(theme)$ +\usetheme[$for(themeoptions)$$themeoptions$$sep$,$endfor$]{$theme$} +$endif$ +$if(colortheme)$ +\usecolortheme{$colortheme$} +$endif$ +$if(fonttheme)$ +\usefonttheme{$fonttheme$} +$endif$ +$if(mainfont)$ +\usefonttheme{serif} % use mainfont rather than sansfont for slide text +$endif$ +$if(innertheme)$ +\useinnertheme{$innertheme$} +$endif$ +$if(outertheme)$ +\useoutertheme{$outertheme$} +$endif$ +$endif$ +$-- User font settings (must come after default font and Beamer theme) +$if(fontfamily)$ +\usepackage[$for(fontfamilyoptions)$$fontfamilyoptions$$sep$,$endfor$]{$fontfamily$} +$endif$ +\ifPDFTeX\else + % xetex/luatex font selection +$if(mainfont)$ + \setmainfont[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$} +$endif$ +$if(sansfont)$ + \setsansfont[$for(sansfontoptions)$$sansfontoptions$$sep$,$endfor$]{$sansfont$} +$endif$ +$if(monofont)$ + \setmonofont[$for(monofontoptions)$$monofontoptions$$sep$,$endfor$]{$monofont$} +$endif$ +$for(fontfamilies)$ + \newfontfamily{$fontfamilies.name$}[$for(fontfamilies.options)$$fontfamilies.options$$sep$,$endfor$]{$fontfamilies.font$} +$endfor$ +$if(mathfont)$ +$if(mathspec)$ + \ifXeTeX + \setmathfont(Digits,Latin,Greek)[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$} + \else + \setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$} + \fi +$else$ + \setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$} +$endif$ +$endif$ +$if(CJKmainfont)$ + \ifXeTeX + \usepackage{xeCJK} + \setCJKmainfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$} + $if(CJKsansfont)$ + \setCJKsansfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKsansfont$} + $endif$ + $if(CJKmonofont)$ + \setCJKmonofont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmonofont$} + $endif$ + \fi +$endif$ +$if(luatexjapresetoptions)$ + \ifLuaTeX + \usepackage[$for(luatexjapresetoptions)$$luatexjapresetoptions$$sep$,$endfor$]{luatexja-preset} + \fi +$endif$ +$if(CJKmainfont)$ + \ifLuaTeX + \usepackage[$for(luatexjafontspecoptions)$$luatexjafontspecoptions$$sep$,$endfor$]{luatexja-fontspec} + \setmainjfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$} + \fi +$endif$ +\fi +$if(zero-width-non-joiner)$ +%% Support for zero-width non-joiner characters. +\makeatletter +\def\zerowidthnonjoiner{% + % Prevent ligatures and adjust kerning, but still support hyphenating. + \texorpdfstring{% + \TextOrMath{\nobreak\discretionary{-}{}{\kern.03em}% + \ifvmode\else\nobreak\hskip\z@skip\fi}{}% + }{}% +} +\makeatother +\ifPDFTeX + \DeclareUnicodeCharacter{200C}{\zerowidthnonjoiner} +\else + \catcode`^^^^200c=\active + \protected\def ^^^^200c{\zerowidthnonjoiner} +\fi +%% End of ZWNJ support +$endif$ +% Use upquote if available, for straight quotes in verbatim environments +\IfFileExists{upquote.sty}{\usepackage{upquote}}{} +\IfFileExists{microtype.sty}{% use microtype if available + \usepackage[$for(microtypeoptions)$$microtypeoptions$$sep$,$endfor$]{microtype} + \UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts +}{} +$if(indent)$ +$else$ +\makeatletter +\@ifundefined{KOMAClassName}{% if non-KOMA class + \IfFileExists{parskip.sty}{% + \usepackage{parskip} + }{% else + \setlength{\parindent}{0pt} + \setlength{\parskip}{6pt plus 2pt minus 1pt}} +}{% if KOMA class + \KOMAoptions{parskip=half}} +\makeatother +$endif$ +$if(verbatim-in-note)$ +\usepackage{fancyvrb} +$endif$ +\usepackage{xcolor} +\definecolor{default-linkcolor}{HTML}{A50000} +\definecolor{default-filecolor}{HTML}{A50000} +\definecolor{default-citecolor}{HTML}{4077C0} +\definecolor{default-urlcolor}{HTML}{4077C0} +$if(footnotes-pretty)$ +% load footmisc in order to customize footnotes (footmisc has to be loaded before hyperref, cf. https://tex.stackexchange.com/a/169124/144087) +\usepackage[hang,flushmargin,bottom,multiple]{footmisc} +\setlength{\footnotemargin}{0.8em} % set space between footnote nr and text +\setlength{\footnotesep}{\baselineskip} % set space between multiple footnotes +\setlength{\skip\footins}{0.3cm} % set space between page content and footnote +\setlength{\footskip}{0.9cm} % set space between footnote and page bottom +$endif$ +$if(geometry)$ +$if(beamer)$ +\geometry{$for(geometry)$$geometry$$sep$,$endfor$} +$else$ +\usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry} +$endif$ +$else$ +$if(beamer)$ +$else$ +\usepackage[margin=2.5cm,includehead=true,includefoot=true,centering,$for(geometry)$$geometry$$sep$,$endfor$]{geometry} +$endif$ +$endif$ +$if(titlepage-logo)$ +\usepackage[export]{adjustbox} +\usepackage{graphicx} +$endif$ +$if(beamer)$ +\newif\ifbibliography +$endif$ +$if(listings)$ +\usepackage{listings} +\newcommand{\passthrough}[1]{#1} +\lstset{defaultdialect=[5.3]Lua} +\lstset{defaultdialect=[x86masm]Assembler} +$endif$ +$if(listings-no-page-break)$ +\usepackage{etoolbox} +\BeforeBeginEnvironment{lstlisting}{\par\noindent\begin{minipage}{\linewidth}} +\AfterEndEnvironment{lstlisting}{\end{minipage}\par\addvspace{\topskip}} +$endif$ +$if(lhs)$ +\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{} +$endif$ +$if(highlighting-macros)$ +$highlighting-macros$ + +% Workaround/bugfix from jannick0. +% See https://github.com/jgm/pandoc/issues/4302#issuecomment-360669013) +% or https://github.com/Wandmalfarbe/pandoc-latex-template/issues/2 +% +% Redefine the verbatim environment 'Highlighting' to break long lines (with +% the help of fvextra). Redefinition is necessary because it is unlikely that +% pandoc includes fvextra in the default template. +\usepackage{fvextra} +\DefineVerbatimEnvironment{Highlighting}{Verbatim}{breaklines,fontsize=$if(code-block-font-size)$$code-block-font-size$$else$\small$endif$,commandchars=\\\{\}} + +$endif$ +$if(tables)$ +\usepackage{longtable,booktabs,array} +$if(multirow)$ +\usepackage{multirow} +$endif$ +\usepackage{calc} % for calculating minipage widths +$if(beamer)$ +\usepackage{caption} +% Make caption package work with longtable +\makeatletter +\def\fnum@table{\tablename~\thetable} +\makeatother +$else$ +% Correct order of tables after \paragraph or \subparagraph +\usepackage{etoolbox} +\makeatletter +\patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{} +\makeatother +% Allow footnotes in longtable head/foot +\IfFileExists{footnotehyper.sty}{\usepackage{footnotehyper}}{\usepackage{footnote}} +\makesavenoteenv{longtable} +$endif$ +$endif$ +% add backlinks to footnote references, cf. https://tex.stackexchange.com/questions/302266/make-footnote-clickable-both-ways +$if(footnotes-disable-backlinks)$ +$else$ +\usepackage{footnotebackref} +$endif$ +$if(graphics)$ +\usepackage{graphicx} +\makeatletter +\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi} +\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi} +\makeatother +% Scale images if necessary, so that they will not overflow the page +% margins by default, and it is still possible to overwrite the defaults +% using explicit options in \includegraphics[width, height, ...]{} +\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio} +% Set default figure placement to htbp +\makeatletter +% Make use of float-package and set default placement for figures to H. +% The option H means 'PUT IT HERE' (as opposed to the standard h option which means 'You may put it here if you like'). +\usepackage{float} +\floatplacement{figure}{$if(float-placement-figure)$$float-placement-figure$$else$H$endif$} +\makeatother +$endif$ +$if(svg)$ +\usepackage{svg} +$endif$ +$if(strikeout)$ +$-- also used for underline +\ifLuaTeX + \usepackage{luacolor} + \usepackage[soul]{lua-ul} +\else +\usepackage{soul} +\fi +$endif$ +\setlength{\emergencystretch}{3em} % prevent overfull lines +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} +$if(numbersections)$ +\setcounter{secnumdepth}{$if(secnumdepth)$$secnumdepth$$else$5$endif$} +$else$ +\setcounter{secnumdepth}{-\maxdimen} % remove section numbering +$endif$ +$if(subfigure)$ +\usepackage{subcaption} +$endif$ +$if(beamer)$ +$else$ +$if(block-headings)$ +% Make \paragraph and \subparagraph free-standing +\ifx\paragraph\undefined\else + \let\oldparagraph\paragraph + \renewcommand{\paragraph}[1]{\oldparagraph{#1}\mbox{}} +\fi +\ifx\subparagraph\undefined\else + \let\oldsubparagraph\subparagraph + \renewcommand{\subparagraph}[1]{\oldsubparagraph{#1}\mbox{}} +\fi +$endif$ +$endif$ +$if(pagestyle)$ +\pagestyle{$pagestyle$} +$endif$ +$if(csl-refs)$ +\newlength{\cslhangindent} +\setlength{\cslhangindent}{1.5em} +\newlength{\csllabelwidth} +\setlength{\csllabelwidth}{3em} +\newlength{\cslentryspacingunit} % times entry-spacing +\setlength{\cslentryspacingunit}{\parskip} +\newenvironment{CSLReferences}[2] % #1 hanging-ident, #2 entry spacing + {% don't indent paragraphs + \setlength{\parindent}{0pt} + % turn on hanging indent if param 1 is 1 + \ifodd #1 + \let\oldpar\par + \def\par{\hangindent=\cslhangindent\oldpar} + \fi + % set entry spacing + \setlength{\parskip}{#2\cslentryspacingunit} + }% + {} +\usepackage{calc} +\newcommand{\CSLBlock}[1]{#1\hfill\break} +\newcommand{\CSLLeftMargin}[1]{\parbox[t]{\csllabelwidth}{#1}} +\newcommand{\CSLRightInline}[1]{\parbox[t]{\linewidth - \csllabelwidth}{#1}\break} +\newcommand{\CSLIndent}[1]{\hspace{\cslhangindent}#1} +$endif$ +$if(lang)$ +\ifLuaTeX +\usepackage[bidi=basic]{babel} +\else +\usepackage[bidi=default]{babel} +\fi +$if(babel-lang)$ +\babelprovide[main,import]{$babel-lang$} +$if(mainfont)$ +\ifPDFTeX +\else +\babelfont{rm}[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$} +\fi +$endif$ +$endif$ +$for(babel-otherlangs)$ +\babelprovide[import]{$babel-otherlangs$} +$endfor$ +$for(babelfonts/pairs)$ +\babelfont[$babelfonts.key$]{rm}{$babelfonts.value$} +$endfor$ +% get rid of language-specific shorthands (see #6817): +\let\LanguageShortHands\languageshorthands +\def\languageshorthands#1{} +$endif$ +$for(header-includes)$ +$header-includes$ +$endfor$ +\ifLuaTeX + \usepackage{selnolig} % disable illegal ligatures +\fi +$if(dir)$ +\ifPDFTeX + \TeXXeTstate=1 + \newcommand{\RL}[1]{\beginR #1\endR} + \newcommand{\LR}[1]{\beginL #1\endL} + \newenvironment{RTL}{\beginR}{\endR} + \newenvironment{LTR}{\beginL}{\endL} +\fi +$endif$ +$if(natbib)$ +\usepackage[$natbiboptions$]{natbib} +\bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$} +$endif$ +$if(biblatex)$ +\usepackage[$if(biblio-style)$style=$biblio-style$,$endif$$for(biblatexoptions)$$biblatexoptions$$sep$,$endfor$]{biblatex} +$for(bibliography)$ +\addbibresource{$bibliography$} +$endfor$ +$endif$ +$if(nocite-ids)$ +\nocite{$for(nocite-ids)$$it$$sep$, $endfor$} +$endif$ +$if(csquotes)$ +\usepackage{csquotes} +$endif$ +\IfFileExists{bookmark.sty}{\usepackage{bookmark}}{\usepackage{hyperref}} +\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available +\urlstyle{$if(urlstyle)$$urlstyle$$else$same$endif$} +$if(links-as-notes)$ +% Make links footnotes instead of hotlinks: +\DeclareRobustCommand{\href}[2]{#2\footnote{\url{#1}}} +$endif$ +$if(verbatim-in-note)$ +\VerbatimFootnotes % allow verbatim text in footnotes +$endif$ +\hypersetup{ +$if(title-meta)$ + pdftitle={$title-meta$}, +$endif$ +$if(author-meta)$ + pdfauthor={$author-meta$}, +$endif$ +$if(lang)$ + pdflang={$lang$}, +$endif$ +$if(subject)$ + pdfsubject={$subject$}, +$endif$ +$if(keywords)$ + pdfkeywords={$for(keywords)$$keywords$$sep$, $endfor$}, +$endif$ +$if(colorlinks)$ + colorlinks=true, + linkcolor={$if(linkcolor)$$linkcolor$$else$default-linkcolor$endif$}, + filecolor={$if(filecolor)$$filecolor$$else$default-filecolor$endif$}, + citecolor={$if(citecolor)$$citecolor$$else$default-citecolor$endif$}, + urlcolor={$if(urlcolor)$$urlcolor$$else$default-urlcolor$endif$}, +$else$ + hidelinks, +$endif$ + breaklinks=true, + pdfcreator={LaTeX via pandoc with the Eisvogel template}} +$if(title)$ +\title{$title$$if(thanks)$\thanks{$thanks$}$endif$} +$endif$ +$if(subtitle)$ +$if(beamer)$ +$else$ +\usepackage{etoolbox} +\makeatletter +\providecommand{\subtitle}[1]{% add subtitle to \maketitle + \apptocmd{\@title}{\par {\large #1 \par}}{}{} +} +\makeatother +$endif$ +\subtitle{$subtitle$} +$endif$ +\author{$for(author)$$author$$sep$ \and $endfor$} +\date{$date$} +$if(beamer)$ +$if(institute)$ +\institute{$for(institute)$$institute$$sep$ \and $endfor$} +$endif$ +$if(titlegraphic)$ +\titlegraphic{\includegraphics{$titlegraphic$}} +$endif$ +$if(logo)$ +\logo{\includegraphics{$logo$}} +$endif$ +$endif$ + + + +%% +%% added +%% + +$if(page-background)$ +\usepackage[pages=all]{background} +$endif$ + +% +% for the background color of the title page +% +$if(titlepage)$ +\usepackage{pagecolor} +\usepackage{afterpage} +$if(titlepage-background)$ +\usepackage{tikz} +$endif$ +$if(geometry)$ +$else$ +\usepackage[margin=2.5cm,includehead=true,includefoot=true,centering]{geometry} +$endif$ +$endif$ + +% +% break urls +% +\PassOptionsToPackage{hyphens}{url} + +% +% When using babel or polyglossia with biblatex, loading csquotes is recommended +% to ensure that quoted texts are typeset according to the rules of your main language. +% +\usepackage{csquotes} + +% +% captions +% +\definecolor{caption-color}{HTML}{777777} +$if(beamer)$ +$else$ +\usepackage[font={stretch=1.2}, textfont={color=caption-color}, position=top, skip=4mm, labelfont=bf, singlelinecheck=false, justification=$if(caption-justification)$$caption-justification$$else$raggedright$endif$]{caption} +\setcapindent{0em} +$endif$ + +% +% blockquote +% +\definecolor{blockquote-border}{RGB}{221,221,221} +\definecolor{blockquote-text}{RGB}{119,119,119} +\usepackage{mdframed} +\newmdenv[rightline=false,bottomline=false,topline=false,linewidth=3pt,linecolor=blockquote-border,skipabove=\parskip]{customblockquote} +\renewenvironment{quote}{\begin{customblockquote}\list{}{\rightmargin=0em\leftmargin=0em}% +\item\relax\color{blockquote-text}\ignorespaces}{\unskip\unskip\endlist\end{customblockquote}} + +% +% Source Sans Pro as the de­fault font fam­ily +% Source Code Pro for monospace text +% +% 'default' option sets the default +% font family to Source Sans Pro, not \sfdefault. +% +\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex + $if(fontfamily)$ + $else$ + \usepackage[default]{sourcesanspro} + \usepackage{sourcecodepro} + $endif$ +\else % if not pdftex + $if(mainfont)$ + $else$ + \usepackage[default]{sourcesanspro} + \usepackage{sourcecodepro} + + % XeLaTeX specific adjustments for straight quotes: https://tex.stackexchange.com/a/354887 + % This issue is already fixed (see https://github.com/silkeh/latex-sourcecodepro/pull/5) but the + % fix is still unreleased. + % TODO: Remove this workaround when the new version of sourcecodepro is released on CTAN. + \ifxetex + \makeatletter + \defaultfontfeatures[\ttfamily] + { Numbers = \sourcecodepro@figurestyle, + Scale = \SourceCodePro@scale, + Extension = .otf } + \setmonofont + [ UprightFont = *-\sourcecodepro@regstyle, + ItalicFont = *-\sourcecodepro@regstyle It, + BoldFont = *-\sourcecodepro@boldstyle, + BoldItalicFont = *-\sourcecodepro@boldstyle It ] + {SourceCodePro} + \makeatother + \fi + $endif$ +\fi + +% +% heading color +% +\definecolor{heading-color}{RGB}{40,40,40} +$if(beamer)$ +$else$ +\addtokomafont{section}{\color{heading-color}} +$endif$ +% When using the classes report, scrreprt, book, +% scrbook or memoir, uncomment the following line. +%\addtokomafont{chapter}{\color{heading-color}} + +% +% variables for title, author and date +% +$if(beamer)$ +$else$ +\usepackage{titling} +\title{$title$} +\author{$for(author)$$author$$sep$, $endfor$} +\date{$date$} +$endif$ + +% +% tables +% +$if(tables)$ + +\definecolor{table-row-color}{HTML}{F5F5F5} +\definecolor{table-rule-color}{HTML}{999999} + +%\arrayrulecolor{black!40} +\arrayrulecolor{table-rule-color} % color of \toprule, \midrule, \bottomrule +\setlength\heavyrulewidth{0.3ex} % thickness of \toprule, \bottomrule +\renewcommand{\arraystretch}{1.3} % spacing (padding) + +$if(table-use-row-colors)$ +% TODO: This doesn't work anymore. I don't know why. +% Reset rownum counter so that each table +% starts with the same row colors. +% https://tex.stackexchange.com/questions/170637/restarting-rowcolors +% +% Unfortunately the colored cells extend beyond the edge of the +% table because pandoc uses @-expressions (@{}) like so: +% +% \begin{longtable}[]{@{}ll@{}} +% \end{longtable} +% +% https://en.wikibooks.org/wiki/LaTeX/Tables#.40-expressions +\let\oldlongtable\longtable +\let\endoldlongtable\endlongtable +\renewenvironment{longtable}{ +\rowcolors{3}{}{table-row-color!100} % row color +\oldlongtable} { +\endoldlongtable +\global\rownum=0\relax} +$endif$ +$endif$ + +% +% remove paragraph indention +% +\setlength{\parindent}{0pt} +\setlength{\parskip}{6pt plus 2pt minus 1pt} +\setlength{\emergencystretch}{3em} % prevent overfull lines + +% +% +% Listings +% +% + +$if(listings)$ + +% +% general listing colors +% +\definecolor{listing-background}{HTML}{F7F7F7} +\definecolor{listing-rule}{HTML}{B3B2B3} +\definecolor{listing-numbers}{HTML}{B3B2B3} +\definecolor{listing-text-color}{HTML}{000000} +\definecolor{listing-keyword}{HTML}{435489} +\definecolor{listing-keyword-2}{HTML}{1284CA} % additional keywords +\definecolor{listing-keyword-3}{HTML}{9137CB} % additional keywords +\definecolor{listing-identifier}{HTML}{435489} +\definecolor{listing-string}{HTML}{00999A} +\definecolor{listing-comment}{HTML}{8E8E8E} + +\lstdefinestyle{eisvogel_listing_style}{ + language = java, +$if(listings-disable-line-numbers)$ + xleftmargin = 0.6em, + framexleftmargin = 0.4em, +$else$ + numbers = left, + xleftmargin = 2.7em, + framexleftmargin = 2.5em, +$endif$ + backgroundcolor = \color{listing-background}, + basicstyle = \color{listing-text-color}\linespread{1.0}% + \lst@ifdisplaystyle% + $if(code-block-font-size)$$code-block-font-size$$else$\small$endif$% + \fi\ttfamily{}, + breaklines = true, + frame = single, + framesep = 0.19em, + rulecolor = \color{listing-rule}, + frameround = ffff, + tabsize = 4, + numberstyle = \color{listing-numbers}, + aboveskip = 1.0em, + belowskip = 0.1em, + abovecaptionskip = 0em, + belowcaptionskip = 1.0em, + keywordstyle = {\color{listing-keyword}\bfseries}, + keywordstyle = {[2]\color{listing-keyword-2}\bfseries}, + keywordstyle = {[3]\color{listing-keyword-3}\bfseries\itshape}, + sensitive = true, + identifierstyle = \color{listing-identifier}, + commentstyle = \color{listing-comment}, + stringstyle = \color{listing-string}, + showstringspaces = false, + escapeinside = {/*@}{@*/}, % Allow LaTeX inside these special comments + literate = + {á}{{\'a}}1 {é}{{\'e}}1 {í}{{\'i}}1 {ó}{{\'o}}1 {ú}{{\'u}}1 + {Á}{{\'A}}1 {É}{{\'E}}1 {Í}{{\'I}}1 {Ó}{{\'O}}1 {Ú}{{\'U}}1 + {à}{{\`a}}1 {è}{{\`e}}1 {ì}{{\`i}}1 {ò}{{\`o}}1 {ù}{{\`u}}1 + {À}{{\`A}}1 {È}{{\`E}}1 {Ì}{{\`I}}1 {Ò}{{\`O}}1 {Ù}{{\`U}}1 + {ä}{{\"a}}1 {ë}{{\"e}}1 {ï}{{\"i}}1 {ö}{{\"o}}1 {ü}{{\"u}}1 + {Ä}{{\"A}}1 {Ë}{{\"E}}1 {Ï}{{\"I}}1 {Ö}{{\"O}}1 {Ü}{{\"U}}1 + {â}{{\^a}}1 {ê}{{\^e}}1 {î}{{\^i}}1 {ô}{{\^o}}1 {û}{{\^u}}1 + {Â}{{\^A}}1 {Ê}{{\^E}}1 {Î}{{\^I}}1 {Ô}{{\^O}}1 {Û}{{\^U}}1 + {œ}{{\oe}}1 {Œ}{{\OE}}1 {æ}{{\ae}}1 {Æ}{{\AE}}1 {ß}{{\ss}}1 + {ç}{{\c c}}1 {Ç}{{\c C}}1 {ø}{{\o}}1 {å}{{\r a}}1 {Å}{{\r A}}1 + {€}{{\EUR}}1 {£}{{\pounds}}1 {«}{{\guillemotleft}}1 + {»}{{\guillemotright}}1 {ñ}{{\~n}}1 {Ñ}{{\~N}}1 {¿}{{?`}}1 + {…}{{\ldots}}1 {≥}{{>=}}1 {≤}{{<=}}1 {„}{{\glqq}}1 {“}{{\grqq}}1 + {”}{{''}}1 +} +\lstset{style=eisvogel_listing_style} + +% +% Java (Java SE 12, 2019-06-22) +% +\lstdefinelanguage{Java}{ + morekeywords={ + % normal keywords (without data types) + abstract,assert,break,case,catch,class,continue,default, + do,else,enum,exports,extends,final,finally,for,if,implements, + import,instanceof,interface,module,native,new,package,private, + protected,public,requires,return,static,strictfp,super,switch, + synchronized,this,throw,throws,transient,try,volatile,while, + % var is an identifier + var + }, + morekeywords={[2] % data types + % primitive data types + boolean,byte,char,double,float,int,long,short, + % String + String, + % primitive wrapper types + Boolean,Byte,Character,Double,Float,Integer,Long,Short + % number types + Number,AtomicInteger,AtomicLong,BigDecimal,BigInteger,DoubleAccumulator,DoubleAdder,LongAccumulator,LongAdder,Short, + % other + Object,Void,void + }, + morekeywords={[3] % literals + % reserved words for literal values + null,true,false, + }, + sensitive, + morecomment = [l]//, + morecomment = [s]{/*}{*/}, + morecomment = [s]{/**}{*/}, + morestring = [b]", + morestring = [b]', +} + +\lstdefinelanguage{XML}{ + morestring = [b]", + moredelim = [s][\bfseries\color{listing-keyword}]{<}{\ }, + moredelim = [s][\bfseries\color{listing-keyword}]{}, + moredelim = [l][\bfseries\color{listing-keyword}]{/>}, + moredelim = [l][\bfseries\color{listing-keyword}]{>}, + morecomment = [s]{}, + morecomment = [s]{}, + commentstyle = \color{listing-comment}, + stringstyle = \color{listing-string}, + identifierstyle = \color{listing-identifier} +} +$endif$ + +% +% header and footer +% +$if(beamer)$ +$else$ +$if(disable-header-and-footer)$ +$else$ +\usepackage[headsepline,footsepline]{scrlayer-scrpage} + +\newpairofpagestyles{eisvogel-header-footer}{ + \clearpairofpagestyles + \ihead*{$if(header-left)$$header-left$$else$$title$$endif$} + \chead*{$if(header-center)$$header-center$$else$$endif$} + \ohead*{$if(header-right)$$header-right$$else$$date$$endif$} + \ifoot*{$if(footer-left)$$footer-left$$else$$for(author)$$author$$sep$, $endfor$$endif$} + \cfoot*{$if(footer-center)$$footer-center$$else$$endif$} + \ofoot*{$if(footer-right)$$footer-right$$else$\thepage$endif$} + \addtokomafont{pageheadfoot}{\upshape} +} +\pagestyle{eisvogel-header-footer} + +$if(book)$ +\deftripstyle{ChapterStyle}{}{}{}{}{\pagemark}{} +\renewcommand*{\chapterpagestyle}{ChapterStyle} +$endif$ + +$if(page-background)$ +\backgroundsetup{ +scale=1, +color=black, +opacity=$if(page-background-opacity)$$page-background-opacity$$else$0.2$endif$, +angle=0, +contents={% + \includegraphics[width=\paperwidth,height=\paperheight]{$page-background$} + }% +} +$endif$ +$endif$ +$endif$ + +%% +%% end added +%% + +\begin{document} + +%% +%% begin titlepage +%% +$if(beamer)$ +$else$ +$if(titlepage)$ +\begin{titlepage} +$if(titlepage-background)$ +\newgeometry{top=2cm, right=4cm, bottom=3cm, left=4cm} +$else$ +\newgeometry{left=6cm} +$endif$ +$if(titlepage-color)$ +\definecolor{titlepage-color}{HTML}{$titlepage-color$} +\newpagecolor{titlepage-color}\afterpage{\restorepagecolor} +$endif$ +$if(titlepage-background)$ +\tikz[remember picture,overlay] \node[inner sep=0pt] at (current page.center){\includegraphics[width=\paperwidth,height=\paperheight]{$titlepage-background$}}; +$endif$ +\newcommand{\colorRule}[3][black]{\textcolor[HTML]{#1}{\rule{#2}{#3}}} +\begin{flushleft} +\noindent +\\[-1em] +\color[HTML]{$if(titlepage-text-color)$$titlepage-text-color$$else$5F5F5F$endif$} +\makebox[0pt][l]{\colorRule[$if(titlepage-rule-color)$$titlepage-rule-color$$else$435488$endif$]{1.3\textwidth}{$if(titlepage-rule-height)$$titlepage-rule-height$$else$4$endif$pt}} +\par +\noindent + +$if(titlepage-background)$ +% The titlepage with a background image has other text spacing and text size +{ + \setstretch{2} + \vfill + \vskip -8em + \noindent {\huge \textbf{\textsf{$title$}}} + $if(subtitle)$ + \vskip 1em + {\Large \textsf{$subtitle$}} + $endif$ + \vskip 2em + \noindent {\Large \textsf{$for(author)$$author$$sep$, $endfor$} \vskip 0.6em \textsf{$date$}} + \vfill +} +$else$ +{ + \setstretch{1.4} + \vfill + \noindent {\huge \textbf{\textsf{$title$}}} + $if(subtitle)$ + \vskip 1em + {\Large \textsf{$subtitle$}} + $endif$ + \vskip 2em + \noindent {\Large \textsf{$for(author)$$author$$sep$, $endfor$}} + \vfill +} +$endif$ + +$if(titlepage-logo)$ +\noindent +\includegraphics[width=$if(logo-width)$$logo-width$$else$35mm$endif$, left]{$titlepage-logo$} +$endif$ + +$if(titlepage-background)$ +$else$ +\textsf{$date$} +$endif$ +\end{flushleft} +\end{titlepage} +\restoregeometry +\pagenumbering{arabic} +$endif$ +$endif$ + +%% +%% end titlepage +%% + +$if(has-frontmatter)$ +\frontmatter +$endif$ +$if(title)$ +$if(beamer)$ +\frame{\titlepage} +% don't generate the default title +% $else$ +% \maketitle +$endif$ +$if(abstract)$ +\begin{abstract} +$abstract$ +\end{abstract} +$endif$ +$endif$ + +$if(first-chapter)$ +\setcounter{chapter}{$first-chapter$} +\addtocounter{chapter}{-1} +$endif$ + +$for(include-before)$ +$include-before$ + +$endfor$ +$if(toc)$ +$if(toc-title)$ +\renewcommand*\contentsname{$toc-title$} +$endif$ +$if(beamer)$ +\begin{frame}[allowframebreaks] +$if(toc-title)$ + \frametitle{$toc-title$} +$endif$ + \tableofcontents[hideallsubsections] +\end{frame} +$if(toc-own-page)$ +\newpage +$endif$ +$else$ +{ +$if(colorlinks)$ +\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$$endif$} +$endif$ +\setcounter{tocdepth}{$toc-depth$} +\tableofcontents +$if(toc-own-page)$ +\newpage +$endif$ +} +$endif$ +$endif$ +$if(lof)$ +\listoffigures +$endif$ +$if(lot)$ +\listoftables +$endif$ +$if(linestretch)$ +\setstretch{$linestretch$} +$endif$ +$if(has-frontmatter)$ +\mainmatter +$endif$ +$body$ + +$if(has-frontmatter)$ +\backmatter +$endif$ +$if(natbib)$ +$if(bibliography)$ +$if(biblio-title)$ +$if(has-chapters)$ +\renewcommand\bibname{$biblio-title$} +$else$ +\renewcommand\refname{$biblio-title$} +$endif$ +$endif$ +$if(beamer)$ +\begin{frame}[allowframebreaks]{$biblio-title$} + \bibliographytrue +$endif$ + \bibliography{$for(bibliography)$$bibliography$$sep$,$endfor$} +$if(beamer)$ +\end{frame} +$endif$ + +$endif$ +$endif$ +$if(biblatex)$ +$if(beamer)$ +\begin{frame}[allowframebreaks]{$biblio-title$} + \bibliographytrue + \printbibliography[heading=none] +\end{frame} +$else$ +\printbibliography$if(biblio-title)$[title=$biblio-title$]$endif$ +$endif$ + +$endif$ +$for(include-after)$ +$include-after$ + +$endfor$ +\end{document} diff --git a/inst/rmarkdown/lua/lesson.lua b/inst/rmarkdown/lua/lesson.lua index dbcca6a8a..9c70879b4 100644 --- a/inst/rmarkdown/lua/lesson.lua +++ b/inst/rmarkdown/lua/lesson.lua @@ -69,8 +69,8 @@ function overview_card() local objectives_div = pandoc.Div({}, {class='inner bordered'}); local qbody = pandoc.Div({}, {class="card-body"}) local obody = pandoc.Div({}, {class="card-body"}) - local qcol = pandoc.Div({}, {class="col-md-4"}) - local ocol = pandoc.Div({}, {class="col-md-8"}) + local qcol = pandoc.Div({}, {class="col-md-5"}) + local ocol = pandoc.Div({}, {class="col-md-7"}) local row = pandoc.Div({}, {class="row g-0"}) local overview = pandoc.Div({}, {class="overview card"}) -- create headers. Note because of --section-divs, we have to insert raw diff --git a/inst/templates/citation-template.txt b/inst/templates/citation-template.txt index 8664c63bb..bfe5fa105 100644 --- a/inst/templates/citation-template.txt +++ b/inst/templates/citation-template.txt @@ -1,7 +1,7 @@ # This template CITATION.cff file was generated with cffinit. # Visit https://bit.ly/cffinit to replace its contents # with information about your lesson. -# Remember to update this file periodically, +# Remember to update this file periodically, # ensuring that the author list and other fields remain accurate. cff-version: 1.2.0 @@ -19,4 +19,4 @@ abstract: >- FIXME Replace this with a short abstract describing the lesson, e.g. its target audience and main intended learning objectives. -license: CC-BY-4.0 \ No newline at end of file +license: CC-BY-4.0 diff --git a/inst/templates/config-template.txt b/inst/templates/config-template.txt index 5612a6797..ea1b8a552 100644 --- a/inst/templates/config-template.txt +++ b/inst/templates/config-template.txt @@ -41,6 +41,12 @@ branch: {{ branch }}{{ ^branch }}main{{ /branch }} # Who to contact if there are any issues contact: {{ contact }} +# Generate PDFs of episodes? +pdf: {{ pdf }} + +# Generate ipynb notebooks of episodes? +ipynb: {{ ipynb }} + # Navigation ------------------------------------------------ # # Use the following menu items to specify the order of diff --git a/inst/workflows/sandpaper-main.yaml b/inst/workflows/sandpaper-main.yaml index a4f8dc40c..d1453808a 100644 --- a/inst/workflows/sandpaper-main.yaml +++ b/inst/workflows/sandpaper-main.yaml @@ -43,13 +43,16 @@ jobs: - name: "Set up Pandoc" uses: r-lib/actions/setup-pandoc@v2 + - name: "Set up Chrome" # for PDF rendering + uses: browser-actions/setup-chrome@v1.2.0 + - name: "Setup Lesson Engine" - uses: carpentries/actions/setup-sandpaper@main + uses: LearnToDiscover/actions/setup-sandpaper@main with: cache-version: ${{ secrets.CACHE_VERSION }} - name: "Setup Package Cache" - uses: carpentries/actions/setup-lesson-deps@main + uses: LearnToDiscover/actions/setup-lesson-deps@main with: cache-version: ${{ secrets.CACHE_VERSION }} diff --git a/man/build_episode_ipynb.Rd b/man/build_episode_ipynb.Rd new file mode 100644 index 000000000..368e3f438 --- /dev/null +++ b/man/build_episode_ipynb.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/build_episode.R +\name{build_episode_ipynb} +\alias{build_episode_ipynb} +\title{Convert a single episode to a Jupyter notebook} +\usage{ +build_episode_ipynb( + path, + outdir = path_built(path), + profile = "lesson-requirements", + quiet = TRUE +) +} +\arguments{ +\item{path}{path to the RMarkdown file.} + +\item{outdir}{the directory to write to/} + +\item{quiet}{if \code{TRUE}, output is suppressed, default is \code{TRUE} so we can use better +formatting elsewhere.} +} +\value{ +the path to the output, invisibly +} +\description{ +Convert a single episode to a Jupyter notebook +} +\keyword{internal} diff --git a/man/build_ipynb.Rd b/man/build_ipynb.Rd new file mode 100644 index 000000000..81ca8c18f --- /dev/null +++ b/man/build_ipynb.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/build_ipynb.R +\name{build_ipynb} +\alias{build_ipynb} +\title{Build ipynb notebooks from the RMarkdown episodes} +\usage{ +build_ipynb(path = ".", rebuild = FALSE, quiet = FALSE) +} +\arguments{ +\item{path}{the path to your repository (defaults to your current working +directory)} + +\item{rebuild}{if \code{TRUE}, everything will be built from scratch as if there +was no cache. Defaults to \code{FALSE}, which will only build ipynb files that +haven't been built before.} + +\item{quiet}{when \code{TRUE}, output is supressed.} +} +\description{ +Convert the RMarkdown episodes to Jupyter notebooks using \code{jupytext}. +Only the actual episodes are converted, not the additional pages. +} +\seealso{ +\code{\link[=build_episode_ipynb]{build_episode_ipynb()}} +} +\keyword{internal} diff --git a/man/build_status.Rd b/man/build_status.Rd index 9eec97b6b..669495c24 100644 --- a/man/build_status.Rd +++ b/man/build_status.Rd @@ -14,7 +14,8 @@ build_status( sources, db = "site/built/md5sum.txt", rebuild = FALSE, - write = FALSE + write = FALSE, + format = c("md", "ipynb") ) } \arguments{ @@ -32,6 +33,9 @@ FALSE)} \item{write}{if TRUE, the database will be updated, Defaults to FALSE, meaning that the database will remain the same.} + +\item{format}{the format of the built files. Either \code{"md"} (the default) for Markdown, or +\code{"ipynb"} for Jupyter notebooks.} } \value{ a list of the following elements diff --git a/man/create_lesson.Rd b/man/create_lesson.Rd index 2afaa0187..ab09ee6c3 100644 --- a/man/create_lesson.Rd +++ b/man/create_lesson.Rd @@ -9,7 +9,12 @@ create_lesson( name = fs::path_file(path), rmd = TRUE, rstudio = rstudioapi::isAvailable(), - open = rlang::is_interactive() + open = rlang::is_interactive(), + add_python = FALSE, + python = NULL, + type = c("auto", "virtualenv", "conda", "system"), + pdf = FALSE, + ipynb = FALSE ) } \arguments{ @@ -25,6 +30,23 @@ file extension in the lesson.} \item{rstudio}{create an RStudio project (defaults to if RStudio exits)} \item{open}{if interactive, the lesson will open in a new editor window.} + +\item{add_python}{if set to \code{TRUE}, will add Python as a dependency for the +lesson. See \code{\link[=use_python]{use_python()}} for details. Defaults to \code{FALSE}.} + +\item{python}{the path to the version of Python to be used. The default, +\code{NULL}, will prompt the user to select an appropriate version of Python in +interactive sessions. In non-interactive sessions, \pkg{renv} will attempt +to automatically select an appropriate version. See \code{\link[renv:use_python]{renv::use_python()}} +for more details.} + +\item{type}{the type of Python environment to use. When \code{"auto"}, the +default, virtual environments will be used. See \code{\link[renv:use_python]{renv::use_python()}} for +more details.} + +\item{pdf}{if \code{TRUE}, a PDF version of each episode will be built.} + +\item{ipynb}{if \code{TRUE}, a Jupyter Notebook version of each episode will be built.} } \value{ the path to the new lesson diff --git a/man/filter_out_unreleased.Rd b/man/filter_out_unreleased.Rd new file mode 100644 index 000000000..78c6b444f --- /dev/null +++ b/man/filter_out_unreleased.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-releases.R +\name{filter_out_unreleased} +\alias{filter_out_unreleased} +\title{Filter out unreleased episodes} +\usage{ +filter_out_unreleased(episodes, lesson_config) +} +\arguments{ +\item{episodes}{A character vector of file paths} + +\item{lesson_config}{A list of lesson configuration, as returned by \code{\link[=get_config]{get_config()}}} +} +\value{ +A character vector of file paths, excluding unreleased episodes +} +\description{ +Filter out unreleased episodes +} +\details{ +The \code{lesson_config} list is expected to have a \code{releases} component, which is a named list of +episodes, with the names being the release dates in the format \code{YYYY-MM-DD}. This function +filters out any episodes that are scheduled to be released in the future. + +The lessons \code{config.yaml} file is expected to have a \code{releases} component, with the following structure: + +\if{html}{\out{
    }}\preformatted{releases: + "2024-05-01": introduction.Rmd + "2025-05-01": + - not-yet-released.Rmd + - not-yet-released-2.Rmd +}\if{html}{\out{
    }} +} +\keyword{internal} diff --git a/man/fixtures.Rd b/man/fixtures.Rd index d20768ddd..b1f97ed01 100644 --- a/man/fixtures.Rd +++ b/man/fixtures.Rd @@ -9,7 +9,7 @@ \alias{remove_local_remote} \title{Test fixture functions for sandpaper} \usage{ -create_test_lesson() +create_test_lesson(pdf = FALSE, ipynb = FALSE) generate_restore_fixture(repo) @@ -27,6 +27,10 @@ clean_branch(repo, branch = NULL, name = "sandpaper-local", verbose = FALSE) remove_local_remote(repo, name = "sandpaper-local") } \arguments{ +\item{pdf}{logical; whether to create PDFs for the lesson's episodes} + +\item{ipynb}{logical; whether to create Jupyter notebooks for the lesson's episodes} + \item{repo}{path to a git repository} \item{remote}{path to an empty or uninitialized directory. Defaults to a @@ -87,6 +91,6 @@ Destorys the local remote repository and removes it from the fixture lesson } } \note{ -These are implemented in tests/testthat/setup.md +These are implemented in tests/testthat/setup.R } \keyword{internal} diff --git a/man/known_languages.Rd b/man/known_languages.Rd index 4ee2b95b7..0fcef5554 100644 --- a/man/known_languages.Rd +++ b/man/known_languages.Rd @@ -22,6 +22,7 @@ details of how to do so in the source code for {sandpaper}. \if{html}{\out{
    }}\preformatted{#> - en #> - es +#> - fr #> - ja #> - uk }\if{html}{\out{
    }} diff --git a/man/template.Rd b/man/template.Rd index 72f4105d7..1f1f23a2b 100644 --- a/man/template.Rd +++ b/man/template.Rd @@ -4,6 +4,7 @@ \alias{template_gitignore} \alias{template_episode} \alias{template_links} +\alias{template_citation} \alias{template_config} \alias{template_conduct} \alias{template_index} @@ -23,6 +24,8 @@ template_episode() template_links() +template_citation() + template_config() template_conduct() diff --git a/man/use_python.Rd b/man/use_python.Rd new file mode 100644 index 000000000..898da81bd --- /dev/null +++ b/man/use_python.Rd @@ -0,0 +1,79 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/use_python.R +\name{use_python} +\alias{use_python} +\alias{py_install} +\title{Add Python as a lesson dependency} +\usage{ +use_python( + path = ".", + python = NULL, + type = c("auto", "virtualenv", "conda", "system"), + open = rlang::is_interactive(), + ... +) + +py_install(packages, path = ".", ...) +} +\arguments{ +\item{path}{path to your lesson. Defaults to the current working directory.} + +\item{python}{The path to the version of Python to be used with this project. See +\strong{Finding Python} for more details.} + +\item{type}{The type of Python environment to use. When \code{"auto"} (the default), +virtual environments will be used.} + +\item{open}{if interactive, the lesson will open in a new editor window.} + +\item{...}{Further arguments to be passed to \code{\link[reticulate:py_install]{reticulate::py_install()}}} + +\item{packages}{Python packages to be installed as a character vecto.} +} +\value{ +The path to the Python executable. Note that this function is mainly +called for its side effects. +} +\description{ +Associate a version of Python with your lesson. This is essentially a wrapper +around \code{\link[renv:use_python]{renv::use_python()}}. + +To add Python packages, \code{py_install()} is provided, which installs Python +packages with \code{\link[reticulate:py_install]{reticulate::py_install()}} and then records them in the renv +environment. This ensures \code{\link[=manage_deps]{manage_deps()}} keeps track of the Python packages +as well. +} +\details{ +This helper function adds Python as a dependency to the \pkg{renv} lockfile +and installs a Python environment of the specified \code{type}. This ensures any +Python packages used for this lesson are installed separately from the user's +main library, much like the R packages (see \code{\link[=manage_deps]{manage_deps()}}). + +Note that \pkg{renv} is not (yet) able to automatically detect Python package +dependencies (e.g. from \code{import} statements). So any required Python packages +still need to be installed manually. To facilitate this, the \code{\link[=py_install]{py_install()}} +helper is provided. This will install Python packages in the correct +environment and record them in a \code{requirements.txt} file, which will be +tracked by \pkg{renv}. Subsequent calls of \code{\link[=manage_deps]{manage_deps()}} will then +correctly restore the required Python packages if needed. +} +\examples{ +\dontrun{ +tmp <- tempfile() +on.exit(unlink(tmp)) + +## Create lesson with Python support +lsn <- create_lesson(tmp, name = "This Lesson", open = FALSE, add_python = TRUE) +lsn + +## Add Python as a dependency to an existing lesson +setwd(lsn) +use_python() + +## Install Python packages and record them as dependencies +py_install("numpy") +} +} +\seealso{ +\code{\link[renv:use_python]{renv::use_python()}}, \code{\link[=py_install]{py_install()}} +} diff --git a/man/with_renv_factory.Rd b/man/with_renv_factory.Rd new file mode 100644 index 000000000..2faa358ec --- /dev/null +++ b/man/with_renv_factory.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-renv.R +\name{with_renv_factory} +\alias{with_renv_factory} +\title{Generate a function to run in a renv profile} +\usage{ +with_renv_factory(func, renv_path, renv_profile = "lesson-requirements") +} +\arguments{ +\item{func}{The function to be evaluated after loading the renv environment.} + +\item{renv_path}{The path to the renv environment to load. Usually a directory created by +\code{\link[=create_lesson]{create_lesson()}}} + +\item{renv_profile}{Optional profile to load. Defaults to "lesson-requirements".} + +\item{...}{Additional arguments to be passed to \code{func}.} +} +\value{ +The result of evaluating \code{func(...)} after loading the renv environment. +} +\description{ +This is a \href{https://adv-r.hadley.nz/function-operators.html}{Function operator} which will +generate a function that will run in a renv profile. This is useful for running code in a +separate R subprocess with \code{\link[callr:r]{callr::r()}}, to avoid \emph{renv} side effects related to interactive +sessions. +} +\keyword{internal} diff --git a/man/yaml_list.Rd b/man/yaml_list.Rd index aea4ad033..c6edd157c 100644 --- a/man/yaml_list.Rd +++ b/man/yaml_list.Rd @@ -25,7 +25,9 @@ cat(yaml::as.yaml(hx)) # representation in yaml #> - a #> - b #> - c -cat(whisker::whisker.render("hello: \{\{hello\}\}", hx)) # messed up whisker +}\if{html}{\out{}} + +\if{html}{\out{
    }}\preformatted{cat(whisker::whisker.render("hello: \{\{hello\}\}", hx)) # messed up whisker #> hello: a,b,c }\if{html}{\out{
    }} diff --git a/tests/testthat/_snaps/3.2/render_html.md b/tests/testthat/_snaps/3.2/render_html.md new file mode 100644 index 000000000..c957e0d31 --- /dev/null +++ b/tests/testthat/_snaps/3.2/render_html.md @@ -0,0 +1,460 @@ +# pandoc structure is rendered correctly + + Code + cat(readLines(out), sep = "\n") + Output + [ RawBlock (Format "text") "" + , Div + ( "" , [ "overview" , "card" ] , [] ) + [ RawBlock + (Format "html") "

    Overview

    " + , Div + ( "" , [ "row" , "g-0" ] , [] ) + [ Div + ( "" , [ "col-md-4" ] , [] ) + [ Div + ( "" , [ "card-body" ] , [] ) + [ Div + ( "" , [ "inner" ] , [] ) + [ RawBlock + (Format "html") + "

    Questions

    " + , BulletList + [ [ Plain + [ Str "What\8217s" + , Space + , Str "the" + , Space + , Str "point?" + ] + ] + ] + ] + ] + ] + , Div + ( "" , [ "col-md-8" ] , [] ) + [ Div + ( "" , [ "card-body" ] , [] ) + [ Div + ( "" , [ "inner" , "bordered" ] , [] ) + [ RawBlock + (Format "html") + "

    Objectives

    " + , BulletList + [ [ Plain + [ Str "Bake" + , Space + , Str "him" + , Space + , Str "away," + , Space + , Str "toys" + ] + ] + ] + ] + ] + ] + ] + ] + , Header 1 ( "markdown" , [] , [] ) [ Str "Markdown" ] + , Div + ( "challenge1" , [ "callout" , "challenge" ] , [] ) + [ Div + ( "" , [ "callout-square" ] , [] ) + [ RawBlock + (Format "html") + "" + ] + , Div + ( "" , [ "callout-inner" ] , [] ) + [ Header + 3 ( "" , [ "callout-title" ] , [] ) [ Str "Challenge" ] + , Div + ( "" , [ "callout-content" ] , [] ) + [ Para + [ Str "How" + , Space + , Str "do" + , Space + , Str "you" + , Space + , Str "write" + , Space + , Str "markdown" + , Space + , Str "divs?" + ] + , Para + [ Str "This" + , Space + , Link + ( "" , [] , [] ) + [ Str "link" + , Space + , Str "should" + , Space + , Str "be" + , Space + , Str "transformed" + ] + ( "Setup.html" , "" ) + ] + , Para + [ Str "This" + , Space + , Link + ( "" , [] , [] ) + [ Str "rmd" + , Space + , Str "link" + , Space + , Str "also" + ] + ( "01-Introduction.html" , "" ) + ] + , Para + [ Str "This" + , Space + , Link + ( "" , [ "newclass" ] , [] ) + [ Str "rmd" + , Space + , Str "is" + , Space + , Str "safe" + ] + ( "https://example.com/01-Introduction.Rmd" , "" ) + ] + , Para + [ Str "This" + , Space + , Link + ( "" , [] , [] ) + [ Str "too" ] + ( "Setup.html#windows-setup" , "windows setup" ) + ] + , Figure + ( "fig-first" , [] , [] ) + (Caption + Nothing + [ Plain + [ Str "link" + , Space + , Str "should" + , Space + , Str "be" + , Space + , Str "transformed" + ] + ]) + [ Plain + [ Image + ( "" , [ "imgclass" ] , [] ) + [ Str "alt" , Space , Str "text" ] + ( "fig/Setup.png" , "" ) + ] + ] + ] + ] + ] + , Div + ( "accordionSolution1" + , [ "accordion" + , "challenge-accordion" + , "accordion-flush" + ] + , [] + ) + [ Div + ( "" , [ "accordion-item" ] , [] ) + [ RawBlock + (Format "html") + "" + , Div + ( "collapseSolution1" + , [ "accordion-collapse" , "collapse" ] + , [ ( "[Solution hidden]" ) + , ( "[Solution hidden]" ) + ] + ) + [ Div + ( "" , [ "accordion-body" ] , [] ) + [ Para + [ Str "just" + , Space + , Str "write" + , Space + , Str "it," + , Space + , Str "silly." + ] + ] + ] + ] + ] + , Div + ( "accordionInstructor1" + , [ "accordion" , "instructor-note" , "accordion-flush" ] + , [] + ) + [ Div + ( "" , [ "accordion-item" ] , [] ) + [ RawBlock + (Format "html") + "" + , Div + ( "collapseInstructor1" + , [ "accordion-collapse" , "collapse" ] + , [ ( "[Instructor hidden]" ) + , ( "[Instructor hidden]" ) + ] + ) + [ Div + ( "" , [ "accordion-body" ] , [] ) + [ Para + [ Str "This" + , Space + , Str "should" + , Space + , Str "be" + , Space + , Str "aside" + ] + ] + ] + ] + ] + , Div + ( "accordionSpoiler1" + , [ "accordion" , "spoiler-accordion" , "accordion-flush" ] + , [] + ) + [ Div + ( "" , [ "accordion-item" ] , [] ) + [ RawBlock + (Format "html") + "" + , Div + ( "collapseSpoiler1" + , [ "accordion-collapse" , "collapse" ] + , [ ( "[Spoiler hidden]" ) + , ( "[Spoiler hidden]" ) + ] + ) + [ Div + ( "" , [ "accordion-body" ] , [] ) + [ Para + [ Str "That" + , Space + , Str "fin" + , Space + , Str "on" + , Space + , Str "the" + , Space + , Str "rear" + , Space + , Str "end" + , Space + , Str "of" + , Space + , Str "a" + , Space + , Str "car" + ] + ] + ] + ] + ] + , Div + ( "" , [ "nothing" ] , [] ) + [ Para + [ Str "This" , Space , Str "should" , Space , Str "be" ] + ] + ] + +# paragraphs after objectives block are parsed correctly + + Code + cat(readLines(out), sep = "\n") + Output + [ RawBlock (Format "text") "" + , Div + ( "" , [ "overview" , "card" ] , [] ) + [ RawBlock + (Format "html") "

    Overview

    " + , Div + ( "" , [ "row" , "g-0" ] , [] ) + [ Div + ( "" , [ "col-md-4" ] , [] ) + [ Div + ( "" , [ "card-body" ] , [] ) + [ Div + ( "" , [ "inner" ] , [] ) + [ RawBlock + (Format "html") + "

    Questions

    " + , BulletList + [ [ Plain + [ Str "What\8217s" + , Space + , Str "the" + , Space + , Str "point?" + ] + ] + ] + ] + ] + ] + , Div + ( "" , [ "col-md-8" ] , [] ) + [ Div + ( "" , [ "card-body" ] , [] ) + [ Div + ( "" , [ "inner" , "bordered" ] , [] ) + [ RawBlock + (Format "html") + "

    Objectives

    " + , BulletList + [ [ Plain + [ Str "Bake" + , Space + , Str "him" + , Space + , Str "away," + , Space + , Str "toys" + ] + ] + ] + ] + ] + ] + ] + ] + , Para + [ Str "Do" + , Space + , Str "you" + , Space + , Str "think" + , Space + , Str "he" + , Space + , Str "saurus?" + ] + , Header 1 ( "markdown" , [] , [] ) [ Str "Markdown" ] + ] + +# render_html applies the internal lua filter + + Code + cat(res) + Output +
    +

    Overview

    +
    +
    +
    +
    +

    Questions

    +
      +
    • What’s the point?
    • +
    +
    +
    +
    +
    +
    +
    +

    Objectives

    +
      +
    • Bake him away, toys
    • +
    +
    +
    +
    +
    +
    +
    +

    Markdown

    +
    +
    + +
    +
    +

    Challenge

    +
    +

    How do you write markdown divs?

    +

    This link should be transformed

    +

    This rmd link also

    +

    This rmd is safe

    +

    This too

    +
    + alt text +
    link should be transformed
    +
    +
    +
    +
    +
    +
    + +
    +
    +

    just write it, silly.

    +
    +
    +
    +
    +
    +
    + +
    +
    +

    This should be aside

    +
    +
    +
    +
    +
    +
    + +
    +
    +

    That fin on the rear end of a car

    +
    +
    +
    +
    +
    +

    This should be

    +
    +
    + diff --git a/tests/testthat/_snaps/build_html.md b/tests/testthat/_snaps/build_html.md index f246cbde6..47c4c8cf0 100644 --- a/tests/testthat/_snaps/build_html.md +++ b/tests/testthat/_snaps/build_html.md @@ -6,14 +6,11 @@
  • Data Sets
  • Software Setup
  • - Key Points -
  • + Key Points +
  • - Glossary -
  • -
  • - Learner Profiles -
  • + Learner Profiles +
  • Reference
  • # [build_home()] instructor index file is index and schedule @@ -22,14 +19,14 @@ writeLines(as.character(items)) Output
  • - Key Points -
  • + Key Points +
  • - Instructor Notes -
  • + Instructor Notes +
  • - Extract All Images -
  • + Extract All Images +
  • Reference
  • # [build_profiles()] learner and instructor views are identical @@ -55,7 +52,6 @@ TEST title 1. introduction Key Points - Glossary Learner Profiles Reference See all in one page diff --git a/tests/testthat/_snaps/build_lesson.md b/tests/testthat/_snaps/build_lesson.md index a39241468..190095eb9 100644 --- a/tests/testthat/_snaps/build_lesson.md +++ b/tests/testthat/_snaps/build_lesson.md @@ -23,7 +23,6 @@ 1. introduction 2. Second Episode! Key Points - Glossary Learner Profiles Reference See all in one page diff --git a/tests/testthat/helper-local_lesson.R b/tests/testthat/helper-local_lesson.R new file mode 100644 index 000000000..2d069b93d --- /dev/null +++ b/tests/testthat/helper-local_lesson.R @@ -0,0 +1,10 @@ +## Create a temporary local lesson +local_lesson <- function(..., envir = parent.frame()) { + tmpdir <- withr::local_tempdir(.local_envir = envir) + suppressMessages({ + utils::capture.output({ + lsn <- create_lesson(tmpdir, open = FALSE, ...) + }) + }) + lsn +} diff --git a/tests/testthat/helper-python.R b/tests/testthat/helper-python.R new file mode 100644 index 000000000..c3d782f42 --- /dev/null +++ b/tests/testthat/helper-python.R @@ -0,0 +1,37 @@ +## These helpers are used in `test-use_python.R`, they are implemented separately to ensure the +## temporary loading of the renv environment doesn't interfere with the testing environment + +## Allows running relevant checks with the test lesson's renv profile +with_renv_profile <- function(path, code, profile = "lesson-requirements", ...) { + path <- normalizePath(path) + code <- rlang::enexpr(code) + callr::r( + func = function(path, code) { + setwd(path) + renv::load(path) + eval(code) + }, + args = list(path = path, code = code), + env = c(callr::rcmd_safe_env(), "RENV_PROFILE" = profile), + ... + ) +} + +get_renv_env <- function(lsn, which = "RETICULATE_PYTHON") { + which <- rlang::enexpr(which) + with_renv_profile(lsn, Sys.getenv(!!which)) +} + +check_reticulate <- function(lsn) { + with_renv_profile(lsn, rlang::is_installed("reticulate")) +} + +check_reticulate_config <- function(lsn) { + with_renv_profile(lsn, reticulate::py_config()) +} + +local_load_py_pkg <- function(lsn, package) { + package <- rlang::enexpr(package) + with_renv_profile(lsn, reticulate::import(!!package)) +} + diff --git a/tests/testthat/test-build_episode.R b/tests/testthat/test-build_episode.R index dfeffe1c7..d5f7b1b11 100644 --- a/tests/testthat/test-build_episode.R +++ b/tests/testthat/test-build_episode.R @@ -92,4 +92,3 @@ test_that("the chapter-links should be cromulent depending on the view", { expect_match(IN_lines, "Instructor Note\\s+this is an instructor note") }) - diff --git a/tests/testthat/test-build_ipynb.R b/tests/testthat/test-build_ipynb.R new file mode 100644 index 000000000..6e6e754e5 --- /dev/null +++ b/tests/testthat/test-build_ipynb.R @@ -0,0 +1,51 @@ +# setup test fixture +{ + tmp <- res <- restore_fixture() + create_episode("second-episode", path = tmp) +} + +test_that("build_episode_ipynb() works", { + skip_if_not(getRversion() >= "4.2") + + fun_file <- fs::path(tmp, "episodes", "introduction.Rmd") + skip_if_not(file.exists(fun_file), "episodes/introduction.Rmd not found") + + outfile <- fs::path_ext_set(fs::path_file(fun_file), "ipynb") + outpath <- fs::path(path_built(fun_file), outfile) + msg <- paste("Converting", fun_file, "to", outpath) + + res <- build_episode_ipynb(fun_file) + + expect_equal(basename(res), "introduction.ipynb") + expect_true(file.exists(file.path(outpath))) +}) + +test_that("ipynb rendering does not happen if content is not changed", { + skip_if_not(getRversion() >= "4.2") + skip_on_os("windows") + + ## Build ipynb files a first time + build_ipynb(res, quiet = TRUE) + + expect_message(out <- capture.output(build_ipynb(res)), "nothing to rebuild") + expect_length(out, 0) +}) + +test_that("Resetting jupyter notebooks works", { + ## Build ipynb files + build_ipynb(res, quiet = TRUE) + ipynb <- get_ipynb_files(res) + + expect_equal(out <- reset_ipynb(res), ipynb, ignore_attr = TRUE) + new_ipynb <- get_ipynb_files(res) + expect_equal(length(new_ipynb), 0) +}) + +test_that("ipynb rendering fails gracefully on R < 4.2", { + skip_if(getRversion() >= "4.2", message = "Test only valid for R < 4.2") + + fun_file <- fs::path(tmp, "episodes", "introduction.Rmd") + skip_if_not(file.exists(fun_file), "episodes/introduction.Rmd not found") + expect_error(build_episode_ipynb(fun_file)) +}) + diff --git a/tests/testthat/test-build_lesson.R b/tests/testthat/test-build_lesson.R index ad66fe183..763cfbfd8 100644 --- a/tests/testthat/test-build_lesson.R +++ b/tests/testthat/test-build_lesson.R @@ -58,6 +58,20 @@ test_that("build_lesson() also builds the extra pages", { }) +test_that("build_lesson() generates PDF if requested", { + + restore_fixture <- create_test_lesson(pdf = TRUE) + tmp <- res <- restore_fixture() + sitepath <- fs::path(tmp, "site", "docs") + + chrome_available <- check_chrome_available() + skip_if_not(rmarkdown::pandoc_available("2.11")) + skip_if_not(chrome_available, "Chrome not available") + + build_lesson(tmp, preview = FALSE, quiet = FALSE) + expect_true(fs::file_exists(fs::path(sitepath, "introduction.pdf"))) +}) + test_that("local site build produces 404 page with relative links", { @@ -113,6 +127,24 @@ test_that("aio page can be rebuilt", { }) +test_that("aio PDF can be built if Chrome available", { + aio <- fs::path(sitepath, "aio.html") + chrome_available <- check_chrome_available() + skip_if_not(rmarkdown::pandoc_available("2.11")) + skip_if_not(chrome_available, "Chrome not available") + skip_if_not(file.exists(aio)) + + html_to_pdf(aio) + aio_pdf <- fs::path(sitepath, "aio.pdf") + expect_true(file.exists(aio_pdf)) +}) + +test_that("html_to_pdf returns warning if Chrome not available", { + withr::local_envvar(c("PAGEDOWN_CHROME" = "not-a-real-browser")) + expect_warning(html_to_pdf(fs::path(sitepath, "aio.html")), "Chrome is not available") +}) + + test_that("keypoints page can be rebuilt", { skip_if_not(rmarkdown::pandoc_available("2.11")) @@ -454,3 +486,14 @@ test_that("episodes with HTML in the title are rendered correctly", { fixed = TRUE ))) }) + +test_that("build_lesson also creates jupyter notebooks when required", { + skip_if_not(getRversion() >= "4.2") + + ## Re-create lesson with notebooks enabled + tmp <- local_lesson(ipynb = TRUE) + build_lesson(tmp, quiet = TRUE, preview = FALSE) + + built_path <- path_built(tmp) + expect_true(fs::file_exists(fs::path(built_path, "introduction.ipynb"))) +}) diff --git a/tests/testthat/test-ci_deploy.R b/tests/testthat/test-ci_deploy.R index 6d5d50557..3bebf301f 100644 --- a/tests/testthat/test-ci_deploy.R +++ b/tests/testthat/test-ci_deploy.R @@ -126,14 +126,6 @@ test_that("404 page root will be lesson URL", { expect_false(parsed[["server"]] == "") expect_true(startsWith(parsed[["path"]], "/lesson-example")) - # test to ensure that we didn't accidentally duplicate the "more" dropdown - more <- xml2::xml_find_all(html, "//nav//button[@id='navbarDropdown']") - expect_length(more, 1L) - - # test to ensure the sidebar content is not accidentally duplicated - moresb <- xml2::xml_find_all(html, "//div[contains(@class, 'resources')]") - expect_length(more, 1L) - # test that the menu items all have same form navbar <- xml2::xml_find_all(html, "//nav//li/a") hrefs <- xml2::xml_attr(navbar, "href") diff --git a/tests/testthat/test-create_lesson.R b/tests/testthat/test-create_lesson.R index bb929b809..4b2841721 100644 --- a/tests/testthat/test-create_lesson.R +++ b/tests/testthat/test-create_lesson.R @@ -149,3 +149,13 @@ test_that("lessons cannot be created in directories that are occupied", { # This should fail expect_error(create_lesson(tmp, open = FALSE), "lesson-example is not an empty directory.") }) + +test_that("`pdf = TRUE` option works for create_lesson()", { + lsn <- local_lesson(pdf = TRUE) + expect_true(get_config(lsn)$pdf) +}) + +test_that("`ipynb = TRUE` option works for create_lesson()", { + lsn <- local_lesson(ipynb = TRUE) + expect_true(get_config(lsn)$ipynb) +}) diff --git a/tests/testthat/test-manage_deps.R b/tests/testthat/test-manage_deps.R index 3695017a3..22342abb1 100644 --- a/tests/testthat/test-manage_deps.R +++ b/tests/testthat/test-manage_deps.R @@ -214,3 +214,37 @@ test_that("update_cache() will update old package versions", { }) +reticulate_installable <- check_reticulate_installable() +use_python(lsn, type = "virtualenv", open = FALSE) + +test_that("manage_deps() does not overwrite requirements.txt", { + skip_if_not(reticulate_installable, "reticulate is not installable") + skip_on_cran() + skip_on_os("windows") + + old_wd <- setwd(lsn) + withr::defer(setwd(old_wd)) + + ## Set up Python and manually add requirements.txt without actually installing + ## the Python package, mimicking the scenario where a Python dependency is missing + req_file <- fs::path(lsn, "requirements.txt") + numpy_version <- "numpy==1.26.4" + writeLines(numpy_version, req_file) + + res <- manage_deps(lsn, quiet = TRUE) + expect_true(numpy_version %in% readLines(req_file)) +}) + + +test_that("manage_deps() restores Python dependencies", { + skip_if_not(reticulate_installable, "reticulate is not installable") + skip_on_cran() + skip_on_os("windows") + + req_file <- fs::path(lsn, "requirements.txt") + writeLines("numpy", req_file) + res <- manage_deps(lsn, quiet = TRUE) + + expect_no_error({numpy <- local_load_py_pkg(lsn, "numpy")}) + expect_s3_class(numpy, "python.builtin.module") +}) diff --git a/tests/testthat/test-overview.R b/tests/testthat/test-overview.R index fd5097ca9..12ee22ffe 100644 --- a/tests/testthat/test-overview.R +++ b/tests/testthat/test-overview.R @@ -98,10 +98,6 @@ test_that("Lessons without episodes can be built", { expect_true(fs::file_exists(idx_file)) idx <- xml2::read_html(idx_file) - # we should have an edit link in the main page - edit_link <- xml2::xml_find_first(idx, ".//a[text()='Edit on GitHub']") - expect_match(xml2::xml_attr(edit_link, "href"), "edit/main/index.md") - # links to home and setup should appear in the navigation xpath_home_link_mobi <- ".//nav//div[starts-with(@class,'accordion ')]/a" xpath_setup_link_desk <- ".//nav//a[@class='nav-link']" diff --git a/tests/testthat/test-render_html.R b/tests/testthat/test-render_html.R index a5e10dcd8..60bb47b35 100644 --- a/tests/testthat/test-render_html.R +++ b/tests/testthat/test-render_html.R @@ -118,8 +118,8 @@ test_that("render_html applies the internal lua filter", { # Metadata blocks are parsed expect_match(res, "div class=\"overview card\"", fixed = TRUE) - expect_match(res, "div class=\"col-md-4\"", fixed = TRUE) - expect_match(res, "div class=\"col-md-8\"", fixed = TRUE) + expect_match(res, "div class=\"col-md-5\"", fixed = TRUE) + expect_match(res, "div class=\"col-md-7\"", fixed = TRUE) expect_match(res, "div class=\"card-body\"", fixed = TRUE) expect_match(res, "Questions", fixed = TRUE) expect_match(res, "Objectives", fixed = TRUE) @@ -149,7 +149,7 @@ test_that("render_html applies the internal lua filter", { ver <- as.character(rmarkdown::pandoc_version()) non_utf8_windows <- tolower(Sys.info()[["sysname"]]) == "windows" && getRversion() < package_version("4.2.0") - skip_if(non_utf8_windows, + skip_if(non_utf8_windows, message = "This version of Windows cannot handle UTF-8 strings") expect_snapshot(cat(res), transform = formation, variant = ver) }) diff --git a/tests/testthat/test-use_python.R b/tests/testthat/test-use_python.R new file mode 100644 index 000000000..3f025255a --- /dev/null +++ b/tests/testthat/test-use_python.R @@ -0,0 +1,61 @@ +skip_on_os("windows") + +## Set up temporary lesson with Python installed, for use in all subsequent tests +lsn <- restore_fixture() +lsn <- use_python(lsn) + +suppressWarnings({ + reticulate_installable <- check_reticulate_installable() +}) + +test_that("use_python() adds Python environment", { + py_path <- fs::path(lsn, "renv/profiles/lesson-requirements/renv/python") + expect_true(fs::dir_exists(py_path)) +}) + +test_that("reticulate is installed", { + skip_if_not(reticulate_installable, "reticulate is not installable") + has_reticulate <- check_reticulate(lsn) + expect_true(has_reticulate) +}) + +test_that("A warning is generated when reticulate is not installable", { + skip_if(reticulate_installable, "reticulate is installable") + expect_warning(install_reticulate(lsn)) + has_reticulate <- check_reticulate(lsn) + expect_false(has_reticulate) +}) + +test_that("use_python() sets reticulate configuration", { + skip_on_os("windows") + skip_if_not(reticulate_installable, "reticulate is not installable") + reticulate_python_env <- get_renv_env(lsn, "RETICULATE_PYTHON") + py_config <- check_reticulate_config(lsn) + + expect_false(reticulate_python_env == "") + expect_true(py_config$available) + expect_true(py_config$forced == "RETICULATE_PYTHON") +}) + + +## This relates to a bug in renv, see https://github.com/rstudio/renv/issues/1217 +test_that("use_python() does not remove renv/profile", { + expect_true(fs::file_exists(fs::path(lsn, "renv/profile"))) +}) + +test_that("py_install() installs Python packages", { + skip_if_not(reticulate_installable, "reticulate is not installable") + skip_on_os("windows") + + numpy_version <- "numpy==1.26.4" + py_install(numpy_version, path = lsn) + numpy <- local_load_py_pkg(lsn, "numpy") + + expect_no_error({ + numpy <- local_load_py_pkg(lsn, "numpy") + }) + expect_s3_class(numpy, "python.builtin.module") + + req_file <- fs::path(lsn, "requirements.txt") + expect_true(numpy_version %in% readLines(req_file)) +}) diff --git a/tests/testthat/test-utils-releases.R b/tests/testthat/test-utils-releases.R new file mode 100644 index 000000000..e7bdf479f --- /dev/null +++ b/tests/testthat/test-utils-releases.R @@ -0,0 +1,27 @@ +today <- Sys.Date() + +test_that("Unreleased episodes are filtered out", { + episodes <- c("introduction.Rmd", "not-yet-released.Rmd", "not-yet-released-2.Rmd") + lesson_config <- list( + releases = list( + "date1" = "introduction.Rmd", + "date2" = c("not-yet-released.Rmd", "not-yet-released-2.Rmd") + ) + ) + ## Set release dates + names(lesson_config$releases) <- c(today - 1, today + 1) + + filtered_episodes <- filter_out_unreleased(episodes, lesson_config) + + expect_equal(filtered_episodes, "introduction.Rmd") +}) + + +test_that("Release dates are optional", { + episodes <- c("introduction.Rmd", "episode2.Rmd", "episode3.Rmd") + lesson_config <- list() + + filtered_episodes <- filter_out_unreleased(episodes, lesson_config) + + expect_equal(filtered_episodes, episodes) +}) diff --git a/tests/testthat/test-utils-sidebar.R b/tests/testthat/test-utils-sidebar.R index d284e0c93..f0e0e0a7b 100644 --- a/tests/testthat/test-utils-sidebar.R +++ b/tests/testthat/test-utils-sidebar.R @@ -100,3 +100,37 @@ test_that("fix_sidebar_href will return empty string if given empty string", { }) + +test_that("sidebar includes level 3 headings", { + html <- "
    +

    Plotting with ggplot2

    +
    +

    Subheading

    +

    This is how you plot with ggplot2

    +
    +
    +

    Subheading 2

    +

    Another subsection>

    +
    +
    +
    +

    Building your plots iteratively

    +

    This is how you build your plots iteratively

    +
    " + nodes <- xml2::read_html(html) + headings <- create_sidebar_headings(nodes) + headings_html <- xml2::read_html(headings) + + li <- xml2::xml_find_all(headings_html, "./body/*") + # The result is a list element with two items + expect_length(li, 2) + # The first element has 2 children: the level 2 heading body and the level 3 heading bodies + expect_length(xml2::xml_children(li[[1]]), 2) + # the anchors are the URIs + expect_equal(xml2::xml_text(xml2::xml_find_all(li, ".//@href")), + c("#plotting", "#subheading", "#subheading2", "#building")) + + # There should be a ul element for the level 3 headings + ul <- xml2::xml_find_all(li, ".//ul/*") + expect_length(ul, 2) +}) diff --git a/tests/testthat/test-utils-translate.R b/tests/testthat/test-utils-translate.R index 068ffaff6..70752b50b 100644 --- a/tests/testthat/test-utils-translate.R +++ b/tests/testthat/test-utils-translate.R @@ -1,3 +1,5 @@ +skip("Skip translation tests as they're not supported on the L2D fork") + # Generate temporary lesson and set `lang: ja` in config.yaml tmp <- res <- restore_fixture() config_path <- fs::path(tmp, "config.yaml") @@ -59,7 +61,6 @@ sitepath <- fs::path(tmp, "site", "docs") # That's it. Happy testing, don't die! test_that("tr_ helpers will extract the source", { - expect_equal(these$translations$src$computed, tr_src("computed")) expect_equal(these$translations$src$varnish, tr_src("varnish")) @@ -71,7 +72,6 @@ test_that("tr_ helpers will extract the source", { test_that("set_language() uses english by default and test helpers are valid", { - os <- tolower(Sys.info()[["sysname"]]) ver <- getRversion() skip_if(os == "windows" && ver < "4.2") @@ -117,12 +117,10 @@ test_that("set_language() uses english by default and test helpers are valid", { # set back to english (default) set_language() expect_equal(tr_computed("OUTPUT"), src) - }) test_that("set_language() can use country codes", { - os <- tolower(Sys.info()[["sysname"]]) ver <- getRversion() skip_if(os == "windows" && ver < "4.2") @@ -135,12 +133,10 @@ test_that("set_language() can use country codes", { # the country codes will fall back to language code if they don't exist expect_silent(set_language("es")) expect_equal(tr_computed("OUTPUT"), OUTAR) - }) test_that("is_known_language returns a warning for an unknown language", { - os <- tolower(Sys.info()[["sysname"]]) ver <- getRversion() skip_if(os == "windows" && ver < "4.2") @@ -148,16 +144,18 @@ test_that("is_known_language returns a warning for an unknown language", { expect_true(is_known_language("ja")) expect_false(is_known_language("xx")) suppressMessages({ - expect_message({ - expect_false(is_known_language("xx", warn = TRUE)) - }, "languages", label = "is_known_language(warn = TRUE)") + expect_message( + { + expect_false(is_known_language("xx", warn = TRUE)) + }, + "languages", + label = "is_known_language(warn = TRUE)" + ) }) - }) test_that("Lessons can be translated with lang setting", { - # NOTE: this requires the following functions defined in # tests/testthat/helper-translate.R: # - expect_set_translated() @@ -182,50 +180,61 @@ test_that("Lessons can be translated with lang setting", { expect_equal(xml2::xml_attr(xml, "lang"), "ja") to_main <- xml2::xml_find_first(xml, "//a[@href='#main-content']") ito_main <- xml2::xml_find_first(instruct, "//a[@href='#main-content']") - expect_set_translated(to_main, + expect_set_translated( + to_main, tr_src("varnish", "SkipToMain") ) - expect_set_translated(ito_main, + expect_set_translated( + ito_main, tr_src("varnish", "SkipToMain") ) expect_equal(xml2::xml_attr(instruct, "lang"), "ja") - expect_title_translated(xml, + expect_title_translated( + xml, tr_src("computed", "SummaryAndSetup") ) - expect_title_translated(instruct, + expect_title_translated( + instruct, tr_src("computed", "SummaryAndSchedule") ) # Extract first header (Summary and Setup) from index h1_xpath <- "//h1[@class='schedule-heading']" h1_header <- xml2::xml_find_all(xml, h1_xpath) - expect_set_translated(h1_header, + expect_set_translated( + h1_header, tr_src("computed", "SummaryAndSetup") ) ih1_header <- xml2::xml_find_all(instruct, h1_xpath) - expect_set_translated(ih1_header, + expect_set_translated( + ih1_header, tr_src("computed", "SummaryAndSchedule") ) # Schedule for instructor view ends with "Finish" final_cell <- xml2::xml_find_first(instruct, "//tr[last()]/td[2]") - expect_set_translated(final_cell, + expect_set_translated( + final_cell, tr_src("computed", "Finish") ) # Navbar has expected text nav_xpath <- "//a[starts-with(@class,'nav-link')]" nav_links <- xml2::xml_find_all(xml, nav_xpath) - expect_set_translated(nav_links, - c(tr_src("varnish", "KeyPoints"), + expect_set_translated( + nav_links, + c( + tr_src("varnish", "KeyPoints"), tr_src("varnish", "Glossary"), tr_src("varnish", "LearnerProfiles") ) ) inav_links <- xml2::xml_find_all(instruct, nav_xpath) - expect_set_translated(inav_links, - c(tr_src("varnish", "KeyPoints"), + expect_set_translated( + inav_links, + c( + tr_src("varnish", "KeyPoints"), tr_src("varnish", "InstructorNotes"), tr_src("varnish", "ExtractAllImages") ) @@ -255,34 +264,41 @@ test_that("Lessons can be translated with lang setting", { inst_notes_path <- fs::path(sitepath, "instructor/instructor-notes.html") inst_notes <- xml2::read_html(inst_notes_path) expect_equal(xml2::xml_attr(inst_notes, "lang"), "ja") - expect_h1_translated(inst_notes, + expect_h1_translated( + inst_notes, tr_src("varnish", "InstructorNotes") ) - expect_title_translated(inst_notes, + expect_title_translated( + inst_notes, tr_src("varnish", "InstructorNotes") ) profiles <- xml2::read_html(fs::path(sitepath, "profiles.html")) expect_equal(xml2::xml_attr(profiles, "lang"), "ja") - expect_h1_translated(profiles, + expect_h1_translated( + profiles, tr_src("varnish", "LearnerProfiles") ) - expect_title_translated(profiles, + expect_title_translated( + profiles, tr_src("varnish", "LearnerProfiles") ) fof <- xml2::read_html(fs::path(sitepath, "404.html")) expect_equal(xml2::xml_attr(fof, "lang"), "ja") - expect_h1_translated(fof, + expect_h1_translated( + fof, tr_src("computed", "PageNotFound") ) - expect_title_translated(fof, + expect_title_translated( + fof, tr_src("computed", "PageNotFound") ) imgs <- xml2::read_html(fs::path(sitepath, "instructor/images.html")) expect_equal(xml2::xml_attr(imgs, "lang"), "ja") - expect_title_translated(imgs, + expect_title_translated( + imgs, tr_src("computed", "AllImages") ) @@ -296,15 +312,16 @@ test_that("Lessons can be translated with lang setting", { expect_set_translated(to_main, tr_src("varnish", "SkipToMain")) previous <- xml2::xml_find_all(xml, "//a[@class='chapter-link']") expect_set_translated(previous, c( - tr_src("varnish", "Home"), - tr_src("varnish", "Previous") - ) - ) + tr_src("varnish", "Home"), + tr_src("varnish", "Previous") + )) # navbar has expected text nav_links <- xml2::xml_find_all(xml, "//a[starts-with(@class,'nav-link')]") - expect_set_translated(nav_links, - c(tr_src("varnish", "KeyPoints"), + expect_set_translated( + nav_links, + c( + tr_src("varnish", "KeyPoints"), tr_src("varnish", "InstructorNotes"), tr_src("varnish", "ExtractAllImages") ) @@ -329,8 +346,10 @@ test_that("Lessons can be translated with lang setting", { # overview, objectives, and questions overview_card <- xml2::xml_find_first(xml, ".//div[@class='overview card']") over_heads <- xml2::xml_find_all(overview_card, ".//h2 | .//h3") - expect_set_translated(over_heads, - c(tr_src("computed", "Overview"), + expect_set_translated( + over_heads, + c( + tr_src("computed", "Overview"), tr_src("computed", "Questions"), tr_src("computed", "Objectives") ) @@ -339,7 +358,8 @@ test_that("Lessons can be translated with lang setting", { # Keypoints are always the last block and should be auto-translated xpath_keypoints <- ".//div[@class='callout keypoints']//h3[@class='callout-title']" keypoints <- xml2::xml_find_first(xml, xpath_keypoints) - expect_set_translated(keypoints, + expect_set_translated( + keypoints, tr_src("computed", "Keypoints") ) @@ -359,8 +379,8 @@ test_that("Lessons can be translated with lang setting", { # print(solution) solution <- solution[[length(solution)]] - expect_set_translated(solution, + expect_set_translated( + solution, tr_src("computed", "Show me the solution") ) - }) diff --git a/vignettes/articles/img/broad-flow.dot.svg b/vignettes/articles/img/broad-flow.dot.svg index 924b4f7f3..b4e5f8ca2 100644 --- a/vignettes/articles/img/broad-flow.dot.svg +++ b/vignettes/articles/img/broad-flow.dot.svg @@ -4,154 +4,160 @@ - - + + Lesson Infrastructure Workflow - + source - -Source Files + +Source Files sandpaper - -{sandpaper} -Lesson Engine + +{sandpaper} +Lesson Engine source->sandpaper - - + + + + + +pegboard + +{pegboard} +Validator + + + +source->pegboard + + stage - - - -Staging Area + + + +Staging Area pandoc - -Pandoc + +Pandoc - + stage->pandoc - - + + site - - -Lesson Website + + +Lesson Website - - -pegboard - -{pegboard} -Validator - - - -sandpaper->pegboard - - + + +sandpaper->stage + + varnish - -{varnish} -Lesson Style + +{varnish} +Lesson Style - + varnish->site - - + + - + -pegboard->stage - - +pegboard->sandpaper + + engine - -Static Site Generator + +Static Site Generator - + engine->varnish - - + + - + pandoc->engine - - + + maintainer - -Lesson Maintainers + +Lesson Maintainers maintainer->source - - + + contributor - -Lesson Contributors + +Lesson Contributors contributor->source - - + + instructor - -Instructor + +Instructor instructor->site - - + + learner - -Learner + +Learner learner->site - - + + diff --git a/vignettes/instructor-guide.Rmd b/vignettes/instructor-guide.Rmd new file mode 100644 index 000000000..b46633582 --- /dev/null +++ b/vignettes/instructor-guide.Rmd @@ -0,0 +1,173 @@ +--- +title: "Instructor guide to using {sandpaper}" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Instructor guide to using {sandpaper}} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup} +library(sandpaper) +``` + +## Introduction + +This guide provides an overview of how to use the {sandpaper} package to create and maintain +lessons, mainly targeted at instructors. +**For a complete guide to using {sandpaper}, head over to +.** + +## Creating a new lesson + +### Setting up the directory structure + +To create a lesson with {sandpaper}, use the `create_lesson()` function: + +```r +sandpaper::create_lesson("~/Desktop/r-intermediate-penguins") +``` + +This will create folder on your desktop called `r-intermediate-penguins` with +the following structure: + +``` +|-- .gitignore # - Ignore everything in the site/ folder +|-- .github/ # - Scripts used for continuous integration +| `-- workflows/ # +| |-- deploy-site.yaml # - Build the source files on github pages +| |-- build-md.yaml # - Build the markdown files on github pages +| `-- cron.yaml # - reset package cache and test +|-- episodes/ # - PUT YOUR MARKDOWN FILES IN THIS FOLDER +| |-- data/ # - Data for your lesson goes here +| |-- figures/ # - All static figures and diagrams are here +| |-- files/ # - Additional files (e.g. handouts) +| `-- introduction.Rmd # - Lessons start with a two-digit number +|-- instructors/ # - Information for Instructors +|-- learners/ # - Information for Learners +| `-- setup.md # - setup instructions (REQUIRED) +|-- profiles/ # - Learner and/or Instructor Profiles +|-- site/ # - This folder is where the rendered markdown files and static site will live +| `-- README.md # - placeholder +|-- config.yaml # - Use this to configure commonly used variables +|-- CONTRIBUTING.md # - Carpentries Rules for Contributions (REQUIRED) +|-- CODE_OF_CONDUCT.md # - Carpentries Code of Conduct (REQUIRED) +|-- LICENSE.md # - Carpentries Licenses (REQUIRED) +`-- README.md # - Introduces folks how to use this lesson and where they can find more information. +``` + +Once you have your site set up, you can add your RMarkdown files in the episodes +folder. By default, they will be built in alphabetical order, but you can use +the `set_episodes()` command to build the schedule in your `config.yaml` file: + +```r +s <- sandpaper::get_episodes() +sandpaper::set_episodes(order = s, write = TRUE) +``` + +### Adding new episodes + +You can add new episodes, using + +```r +sandpaper::create_episode("plotting") +``` + +This will add a new `episodes/plotting.Rmd` file to your lesson with some example content. You can +then edit this file to add your own content. + +### Episode structure + +See [Introduction to The Carpentries Workbench: Episode Structure](https://carpentries.github.io/sandpaper-docs/episodes.html). + +### Previewing your new lesson + +After you created your lesson, you will want to preview it locally. First, make sure that you are in +your newly-created repository and then use the following command: + +```r +sandpaper::serve() +``` + +This function will open a preview window in RStudio or your browser and will **update +automatically** as you work on the lesson. + +If you are using RStudio, you can use the following keyboard shortcuts: + +- Render and preview the whole lesson: `ctrl/cmd + shift + B` +- Render and preview an episode: `ctrl/cmd + shift + K` + +### Pushing to GitHub + +Once you are happy with your lesson, you can push it to GitHub to run the automated lesson building +workflows and to publish your lesson on GitHub pages. You can set up a GitHub repository and activate GitHub pages using the [{usethis}](https://usethis.r-lib.org/) package: + +```r +usethis::use_github(organisation = "", private = FALSE) +usethis::use_github_pages() +``` + +Replace `""` with the name of your GitHub organisation or set it to `NULL` to use your +personal account. You can also make the repository private by setting `private = TRUE`. This will +create a new GitHub repository at `https:://github.com//r-intermediate-penguins` and will +set up GitHub pages at `https://.github.io/r-intermediate-penguins`. It will take a couple +of minutes for the GitHub Action workflows to validate, build and deploy the lesson. You can track +the progress at `https:://github.com//r-intermediate-penguins/actions`. + +## Modifying an existing lesson + +To work on an existing lesson already hosted on GitHub, +[fork and clone the repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo) to +your computer using your method of choice. When working in R, you can do this easily with the [{usethis}](https://usethis.r-lib.org/) package: + +```r +## Using an example repo from the Carpentries +usethis::create_from_github("carpentries/sandpaper-docs", "~/Documents/Lessons/") +``` + +After editing the content and previewing with `sandpaper::serve()`, you should first **save and +commit your changes** with `git`. When working in RStudio, you can do this in the Git tab in the top +right corner of the screen: + +1. Click the "Git" tab in the top right corner of the screen +2. Check the "Staged" box for the file(s) you want to commit +3. Type a concise but descriptive commit message in the "Commit message" box +4. Click "commit" + +After committing your changes, you can push them to GitHub using the "Push" button in the Git tab. +Note that it's often +[a good idea to to make several, small commits](https://happygitwithr.com/repeated-amend.html#rock-climbing-analogy) +rather than one large commit. You also don't need to push after each commit. + +For a comprehensive guide on using Git and GitHub with R, see [Happy Git and GitHub for the useR](https://happygitwithr.com/). + +## Managing and updating the package cache + +>*Note: this is only relevant for lessons with __generated content__. I.e. lessons based on RMarkdown episodes* + +Full guide: [Building Lessons With A Package Cache • sandpaper](https://carpentries.github.io/sandpaper/articles/building-with-renv.html). + +{sandpaper} can set up a package cache for your lesson to create a reliable setup that ensures the +same package versions are used to build your lesson, both locally and on the GitHub Actions runners. + +To make use of the package cache, you need to *explicitly* give {sandpaper} permission to create and use a cache with: + +```r +sandpaper::use_package_cache() +``` + +From this point on, {sandpaper} will detect any **R packages** used throughout the lesson with +`library(package)` or `package::some_function()` and add them to the cache. + +>*Note: this currently does not work for __Python packages__, which still need to be manually installed for the lesson with `sandpaper::py_install()`.* + +Now every time you build the lesson (either locally or on GitHub Actions), {sandpaper} will check +that all the packages used in the lesson are available in the cache. If a package is missing, +{sandpaper} will install it and add it to the cache.