Skip to content

Commit

Permalink
Merge pull request #146 from carpentries/add-lesson-object-vignette
Browse files Browse the repository at this point in the history
Add lesson object vignette
  • Loading branch information
zkamvar committed Nov 22, 2023
2 parents e8f9ec6 + 4d7b11e commit a647628
Show file tree
Hide file tree
Showing 19 changed files with 799 additions and 144 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: pegboard
Title: Explore and Manipulate Markdown Curricula from the Carpentries
Version: 0.7.2
Version: 0.7.2.9000
Authors@R: c(
person(given = "Zhian N.",
family = "Kamvar",
Expand Down
14 changes: 14 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# pegboard 0.7.2.9000 (unreleased)

## DOCUMENTATION

* New `vignette("intro-lesson", package = "pegboard")` provides information
about working with `pegboard::Lesson` objects.
* Documentation for `Episode` and `Lesson` objects have been updated to be
a bit more descriptive and point to the vignettes.

## NEW FEATURES

* `Episode$unblock()` method gains the `force` argument which will allow the
method to run even if it has previously been run.

# pegboard 0.7.2 (2023-11-17)

## MISC
Expand Down
22 changes: 18 additions & 4 deletions R/Episode.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
#'
#' @description
#' Wrapper around an xml document to manipulate and inspect Carpentries episodes
#'
#' @details
#' This class is a fancy wrapper around the results of [tinkr::to_xml()] and
#' has method specific to the Carpentries episodes.
#' The Episode class is a superclass of [tinkr::yarn()], which transforms
#' (commonmark-formatted) Markdown to XML and back again. The extension that
#' the Episode class provides is support for both [Pandoc](https://pandoc.org)
#' and [kramdown](https://kramdown.gettalong.org/) flavours of Markdown.
#'
#' Read more about this class in `vignette("intro-episode", package =
#' "pegboard")`.
#'
#' @export
Episode <- R6::R6Class("Episode",
inherit = tinkr::yarn,
Expand Down Expand Up @@ -99,7 +106,12 @@ Episode <- R6::R6Class("Episode",
self$yaml <- ep$yaml
self$body <- ep$body
self$ns <- ep$ns
# if the parents are missing, this walk will do nothing
purrr::walk(parents, function(parent) add_parent(self, parent))
# the parent here is used to determine the build path for the
# child document, which is dependent on the build parent, aka the final
# ancestor. If there is no parent, then the children are relative to the
# parent.
self$children <- find_children(ep, ancestor = parents[[1]])
},

Expand Down Expand Up @@ -476,6 +488,8 @@ Episode <- R6::R6Class("Episode",
},
#' @description convert challenge blocks to roxygen-like code blocks
#' @param token the token to use to indicate non-code, Defaults to "#'"
#' @param force force the conversion even if the conversion has already
#' taken place
#' @return the Episode object, invisibly
#' @examples
#' loop <- Episode$new(file.path(lesson_fragment(), "_episodes", "14-looping-data-sets.md"))
Expand All @@ -484,8 +498,8 @@ Episode <- R6::R6Class("Episode",
#' loop$unblock()
#' loop$get_blocks() # no blocks
#' loop$code # now there are two blocks with challenge tags
unblock = function(token = "#'") {
if (private$mutations['unblock']) {
unblock = function(token = "#'", force = FALSE) {
if (!force && private$mutations['unblock']) {
return(invisible(self))
}
if (private$mutations['use_dovetail']) {
Expand Down
13 changes: 10 additions & 3 deletions R/Lesson.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
#'
#' @description
#' This is a wrapper for several [Episode] class objects.
#'
#' @details
#' Lessons are made of up several episodes within the `_episodes/` directory of
#' a lesson. This class keeps track of several episodes and allows us to switch
#' between RMarkdown and markdown episodes
#'
#' This class contains and keeps track of relationships between [Episode]
#' objects contained within [Carpentries
#' Workbench](https://carpentries.github.io/workbench) and [Carpentries
#' styles](https://carpentries.github.io/lesson-example) lessons.
#'
#' Read more about how to use this class in `vignette("intro-lesson", package =
#' "pegboard")`
#'
#' @export
Lesson <- R6::R6Class("Lesson",
public = list(
Expand Down
62 changes: 32 additions & 30 deletions R/find_children.R
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#' Detect the child files of an Episode object
#' Detect the child documents of an Episode object
#'
#' @description
#' - `find_children()` returns the _immediate_ children for any given Episode
Expand All @@ -11,7 +11,7 @@
#' @param ancestor an [Episode] object that is used to determine the parent path
#' this also can be `NULL`.
#' @param lsn a [Lesson] object that contains the `parent` and all its children.
#' @return a character vector of the absolute paths to child files.
#' @return a character vector of the absolute paths to child documents
#'
#' @details
#'
Expand Down Expand Up @@ -43,7 +43,7 @@
#' ## Tracing full lineages
#'
#' It is possible for a child document to have further children defined, but
#' there is a caveat: The child file is going to be read from the context of
#' there is a caveat: The child document is going to be read from the context of
#' the `root.dir` knitr option, which in {sandpaper} is set to be `site/built`
#' after the markdown contents and assets are copied over.
#'
Expand All @@ -55,25 +55,25 @@
#' ````
#'
#' When an Episode is read _in the context of a Lesson_, the children are
#' processed with [load_children()] so that each file with children will have a
#' processed with [load_children()] so that each document with children will have a
#' non-zero value of the `$children` element. We recurse through the `$children`
#' element in the [Lesson] object to exhaust the search for the children files.
#' element in the [Lesson] object to exhaust the search for the child documents.
#'
#' The `trace_children()` will return the entire lineage for a given _parent_
#' file. Which, in the case of the examples defined above would be:
#' document. Which, in the case of the examples defined above would be:
#' `/path/to/episodes/parent.Rmd`, `/path/to/episodes/files/the-child.Rmd`,
#' and `/path/to/episodes/files/the-grandchild.md`.
#'
#' ## NOTE
#'
#' For standard lessons, child files are written relative to the directory of
#' the build parent file. Usually, these child files will be in the `files`
#' folder under their parent folder. Overview lessons are a little different.
#' For overview lessons (in The Workbench, these are lessons which contain
#' `overview: true` in config.yaml), the child files may point to
#' `files/child.md`, but in reality, the child file is at the root of the
#' For standard lessons, child documents are written relative to the directory
#' of the build parent document. Usually, these child documents will be in the
#' `files` folder under their parent folder. Overview lessons are a little
#' different. For overview lessons (in The Workbench, these are lessons which
#' contain `overview: true` in config.yaml), the child documents may point to
#' `files/child.md`, but in reality, the child document is at the root of the
#' lesson `../files/child.md`. We correct for this by first checking that the
#' child files exist and if they don't defaulting to the top of the lesson.
#' child documents exist and if they don't defaulting to the top of the lesson.
#'
#' @keywords internal
#' @rdname find_children
Expand All @@ -88,15 +88,15 @@
#' # `find_children()` --------------------------------------------------------
#' ex <- lesson_fragment("sandpaper-fragment-with-child")
#'
#' # The introduction has a single child file
#' # The introduction has a single child document
#' intro <- tinkr::yarn$new(fs::path(ex, "episodes", "intro.Rmd"))
#' intro$head(21) # show the child file
#' intro$head(21) # show the child document
#' pb$find_children(intro)
#' # this is identical to the `$children` element of an Episode object
#' ep <- Episode$new(fs::path(ex, "episodes", "intro.Rmd"))
#' ep$children
#'
#' # Loading the child file reveals another child
#' # Loading the child document reveals another child
#' child <- Episode$new(ep$children[[1]])
#' child$children
#' child$show()
Expand All @@ -116,7 +116,7 @@
#' )
find_children <- function(parent, ancestor = NULL) {
code_blocks <- get_code(parent$body, type = NULL, attr = NULL)
children <- child_file_from_code_blocks(code_blocks)
children <- child_document_from_code_blocks(code_blocks)
any_children <- length(children) > 0L
ancestor_has_parents <- identical(ancestor$has_parents, TRUE)
parent_has_parents <- identical(parent$has_parents, TRUE)
Expand Down Expand Up @@ -160,7 +160,7 @@ find_children <- function(parent, ancestor = NULL) {
return(children)
}
# get the child file from code block if it exists
child_file_from_code_blocks <- function(nodes) {
child_document_from_code_blocks <- function(nodes) {
use_children <- xml2::xml_has_attr(nodes, "child")
if (any(use_children)) {
nodes <- nodes[use_children]
Expand All @@ -183,8 +183,8 @@ child_file_from_code_blocks <- function(nodes) {
}
}

# trace the lineage of a source file and return a recursive list of children
# files. This assumes that the lesson has been set up to process children
# trace the lineage of a source file and return a recursive list of child
# documents. This assumes that the lesson has been set up to process children
#' @rdname find_children
trace_children <- function(parent, lsn) {
if (parent$has_children) {
Expand Down Expand Up @@ -216,24 +216,26 @@ trace_children <- function(parent, lsn) {
#' @details
#'
#' When we want to build lessons, it's important to be able to find all of the
#' files that are necessary to build a particular file. If there is a modification in a child file, \pkg{sandpaper} needs to know that it should flag the parent
#' for rebuilding. To do this, we need two pieces of information:
#' documents that are necessary to build a particular file. If there is a
#' modification in a child document, \pkg{sandpaper} needs to know that it
#' should flag the parent for rebuilding. To do this, we need two pieces of
#' information:
#'
#' 1. The earliest ancestors of a given child file
#' 2. The full list of descendants of a given parent file
#' 1. The earliest ancestors of a given child document.
#' 2. The full list of descendants of a given parent document.
#'
#' Each Episode object only knows about itself, so it can only report its
#' immediate children, but not the children of children, or even its parent
#' (unless we explicitly tell it what its parent is). The [Lesson] object
#' contains the context of all of the Episodes and can provide this information.
#'
#' During Lesson object initialisation, the `load_children()` function is
#' called to process all source files for their children. This creates an
#' called to process all source documents for their children. This creates an
#' empty list of children that is continuously appended to during the function
#' call. It then calls `read_children()` on each parent file, which will append
#' itself as a parent to any existing children in the `all_children` list,
#' intitialize new [Episode] objects from the unread child files, and then
#' search those for children until there are no children left to read.
#' call. It then calls `read_children()` on each parent document, which will
#' append itself as a parent to any existing children in the `all_children`
#' list, intitialize new [Episode] objects from the unread child documents, and
#' then search those for children until there are no children left to read.
#'
#' @keywords internal
#' @seealso [find_children()] for details on how child documents are discovered
Expand Down Expand Up @@ -273,7 +275,7 @@ load_children <- function(all_parents) {
return(the_children)
}

# Read in and/or update all recursive child files for a given parent
# Read in and/or update all recursive child documents for a given parent
#' @rdname load_children
read_children <- function(parent, all_children = list(), ...) {
# if the parent has no children, return NULL. This is the exit condition
Expand Down
37 changes: 29 additions & 8 deletions R/lesson_fragment.R
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
#' An example lesson fragment
#' Example Lesson Fragments
#'
#' This example was taken from the python novice gapminder lesson
#' @param name the name of the lesson fragment
#' Partial lessons mainly used for testing and demonstration purposes
#'
#' @return a path to a lesson fragment that contains one `_episodes` directory
#' with three files: "10-lunch.md", "14-looping-data-sets.md", and
#' "17-scope.md"
#' @note
#' The lesson-fragment example was taken from the python novice gapminder
#' lesson
#'
#' @param name the name of the lesson fragment. Can be one of:
#' - lesson-fragment
#' - rmd-lesson
#' - sandpaper-fragment
#' - sandpaper-fragment with child
#'
#' @return a path to a lesson fragment whose contents are:
#' - `lesson-fragment` contains one `_episodes` directory with three files:
#' "10-lunch.md", "14-looping-data-sets.md", and "17-scope.md"
#' - `rmd-fragment` contains one episode under `_episodes_rmd` called
#' `01-test.Rmd`.
#' - `sandpaper-fragment` contains a trimmed-down Workbench lesson that
#' has its R Markdown content pre-built
#' - `sandpaper-fragment-with-child` contains much of the same content as
#' `sandpaper-fragment`, but the `episodes/index.Rmd` file references child
#' documents.
#' @export
#' @examples
#'
#' lesson_fragment()
#' lesson_fragment("rmd-lesson")
#' lesson_fragment("sandpaper-fragment")
#' lesson_fragment("sandpaper-fragment-with-child")
lesson_fragment <- function(name = "lesson-fragment") {
system.file(name, package = "pegboard")
allowed <- c("lesson-fragment", "rmd-lesson",
"sandpaper-fragment", "sandpaper-fragment-with-child")
name <- match.arg(name, allowed)
return(system.file(name, package = "pegboard"))
}
14 changes: 13 additions & 1 deletion R/use_sandpaper.R
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
#' Convert a Jekyll-based lesson to a sandpaper-based lesson
#'
#' @details A Jekyll episode is littered with kramdown inline attribute tags
#' and liquid-formatted links. Converting to sandpaper means the following:
#'
#' - links using liquid formatting (e.g. `[text]({{ site.path }}/01-episode/)`
#' are replaced with their relative counterparts (`[text](01-episode.md)`.
#' - include statements for `links.md` and `base_path.md` are removed
#' - image attributes have the kramdown `:` removed
#' - code blocks with a kramdown inline attribute tag are converted to
#' commonmark via the internal [liquid_to_commonmark()].
#' - Lesson template-specific code is removed from the setup chunk in R
#' Markdown files.
#'
#' @param body the xml body of an episode
#' @param rmd if `TRUE`, the chunks will be converted to RMarkdown chunks
#' @param yml a list derived from the `_config.yml` file that defines the site
Expand All @@ -9,7 +21,7 @@
#' @param known_paths a character vector with the known paths in the lesson.
#' This is used to determine the correct path to other files in the lesson.
#' @return the body
#' @noRd
#' @keywords internal
use_sandpaper <- function(body, rmd = TRUE, yml = list(), path = NULL, known_paths = NULL) {
if (inherits(body, "xml_missing")) {
warning("episode body missing", call. = FALSE)
Expand Down
3 changes: 2 additions & 1 deletion _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ articles:
- title: "Developer Guides"
navbar: ~
contents:
- articles/validation
- intro-xml
- intro-episode
- intro-lesson
- articles/validation
2 changes: 1 addition & 1 deletion inst/sandpaper-fragment/site/built/md5sum.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"index.md" "fe56dfea3c4269cceb361470e2ca1437" "site/built/index.md" "2022-05-25"
"episodes/intro.Rmd" "3deb3673b36c3fd801e23cb85d9cd195" "site/built/intro.md" "2022-05-25"
"instructors/a.md" "bbaa216f7ef3b17267c460a8bd4fda6b" "site/built/a.md" "2022-05-25"
"learners/setup.md" "0e4e4d7028efbf7d756a08217126884f" "site/built/setup.md" "2022-05-25"
"learners/setup.md" "975096ee5adaf14b68c2ef89c2cade78" "site/built/setup.md" "2023-11-17"
"profiles/b.md" "bbaa216f7ef3b17267c460a8bd4fda6b" "site/built/b.md" "2022-05-25"
13 changes: 10 additions & 3 deletions man/Episode.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions man/Lesson.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a647628

Please sign in to comment.