#' Class representing XML source of a Carpentries episode
#'
#' @description
#' Wrapper around an xml document to manipulate and inspect Carpentries episodes
#'
#' @details
#' 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,
public = list(
#' @field children \[`character`\] a vector of absolute paths to child
#' files if they exist.
children = character(0),
#' @field parents \[`character`\] a vector of absolute paths to immediate
#' parent files if they exist
parents = character(0),
#' @field build_parents \[`character`\] a vector of absolute paths to the
#' final parent files that will trigger this child file to build
build_parents = character(0),
#' @description Create a new Episode
#' @param path \[`character`\] path to a markdown episode file on disk
#' @param process_tags \[`logical`\] if `TRUE` (default), kramdown tags will
#' be processed into attributes of the parent nodes. If `FALSE`, these
#' tags will be treated as text
#' @param fix_links \[`logical`\] if `TRUE` (default), links pointing to
#' liquid tags (e.g. `{{ page.root }}`) and included links (those supplied
#' by a call to `{\% import links.md \%}`) will be appropriately processed
#' as valid links.
#' @param fix_liquid \[`logical`\] defaults to `FALSE`, which means data is
#' immediately passed to [tinkr::yarn]. If `TRUE`, all liquid variables
#' in relative links have spaces removed to allow the commonmark parser to
#' interpret them as links.
#' @param parents \[`list`\] a list of `Episode` objects that represent the
#' immediate parents of this child
#' @param ... arguments passed on to [tinkr::yarn] and [tinkr::to_xml()]
#' @return A new Episode object with extracted XML data
#' @examples
#' scope <- Episode$new(file.path(lesson_fragment(), "_episodes", "17-scope.md"))
#' scope$name
#' scope$lesson
#' scope$challenges
initialize = function(path = NULL, process_tags = TRUE, fix_links = TRUE, fix_liquid = FALSE, parents = NULL, ...) {
if (!fs::file_exists(path)) {
stop(glue::glue("the file '{path}' does not exist"))
}
links <- getOption("sandpaper.links")
if (length(links) && fs::file_exists(links)) {
# if we have links, we concatenate our input files
tmpin <- tempfile(fileext = ".md")
fs::file_copy(path, tmpin)
cat("\n", append = TRUE, file = tmpin)
file.append(tmpin, links)
path <- tmpin
on.exit(unlink(tmpin), add = TRUE)
}
default <- list(
yaml = NULL,
body = xml2::xml_missing()
)
TOX <- purrr::safely(super$initialize, otherwise = default, quiet = FALSE)
if (fix_liquid) {
tmp <- fix_liquid_relative_link(path)
ep <- TOX(tmp, sourcepos = TRUE, ...)
close(tmp)
} else {
ep <- TOX(path, sourcepos = TRUE, ...)
}
if (!is.null(ep$error)) {
private$record_problem(ep$error)
}
ep <- ep$result
# Process the kramdown tags
if (process_tags) {
tags <- kramdown_tags(ep$body)
blocks <- tags[are_blocks(tags)]
tags <- tags[!are_blocks(tags)]
# recording problems to inspect later
bproblem <- purrr::map(blocks, set_ktag_block)
cproblem <- purrr::map(tags, set_ktag_code)
bproblem <- bproblem[!purrr::map_lgl(bproblem, is.null)]
cproblem <- cproblem[!purrr::map_lgl(cproblem, is.null)]
if (length(bproblem) > 0) {
private$record_problem(list(blocks = bproblem))
}
if (length(cproblem) > 0) {
private$record_problem(list(code = cproblem))
}
}
if (fix_links) {
ep$body <- fix_links(ep$body)
}
# Initialize the object
self$path <- path
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]])
},
#' @description enforce that the episode is a {sandpaper} episode withtout
#' going through the conversion steps. The default Episodes from pegboard
#' were assumed to be generated using Jekyll with kramdown syntax. This is
#' a bit of a kludge to bypass the normal checks for kramdown syntax and
#' just assume pandoc syntax
confirm_sandpaper = function() {
ok <- c("unblock", "use_sandpaper_md", "use_sandpaper_rmd",
"move_questions", "move_objectives", "move_keypoints")
muts <- private$mutations
muts[ok] <- TRUE
private$mutations <- muts
invisible(
tryCatch(self$label_divs(),
error = function(e) {
msg <- glue::glue("
{e$message}
Section (div) tags for {self$name} will not be labelled"
)
pb_message(msg, call. = FALSE)
self
})
)
},
#' @description return all `block_quote` elements within the Episode
#' @param type the type of block quote in the Jekyll syntax like ".challenge",
#' ".discussion", or ".solution"
#' @param level the level of the block within the document. Defaults to `1`,
#' which represents all of the block_quotes are not nested within any other
#' block quotes. Increase the nubmer to increase the level of nesting.
#' @return \[`xml_nodeset`\] all the blocks from the episode with the given
#' tag and level.
#' @examples
#' scope <- Episode$new(file.path(lesson_fragment(), "_episodes", "17-scope.md"))
#' # get all the challenges
#' scope$get_blocks(".challenge")
#' # get the solutions
#' scope$get_blocks(".solution", level = 2)
#' \dontrun{
#'
#' # download the source files for r-novice-gampinder into a Lesson object
#' rng <- get_lesson("swcarpentry/r-novice-gapminder")
#' dsp1 <- rng$episodes[["04-data-structures-part1.md"]]
#' # There are 9 blocks in total
#' dsp1$get_blocks()
#' # One is a callout block
#' dsp1$get_blocks(".callout")
#' # One is a discussion block
#' dsp1$get_blocks(".discussion")
#' # Seven are Challenge blocks
#' dsp1$get_blocks(".challenge")
#' # There are eight solution blocks:
#' dsp1$get_blocks(".solution", level = 2L)
#' }
get_blocks = function(type = NULL, level = 1L) {
get_blocks(self$body, type = type, level = level)
},
#' @description
#' fetch the image sources and optionally process them for easier parsing.
#' The default version of this function is equivalent to the active binding
#' `$images`.
#'
#' @param process if `TRUE`, images will be processed via the internal
#' function [process_images()], which will add the `alt` attribute, if
#' available and extract img nodes from HTML blocks.
#' @return an `xml_nodelist`
#' @examples
#'
#' loop <- Episode$new(file.path(lesson_fragment(), "_episodes", "14-looping-data-sets.md"))
#' loop$get_images()
#' loop$get_images(process = TRUE)
get_images = function(process = FALSE) {
get_images(self, process = process)
},
#' @description
#' label all the div elements within the Episode to extract them with
#' `$get_divs()`
label_divs = function() {
label_div_tags(self)
return(invisible(self))
},
#' @description
#' return all div elements within the Episode
#' @param type the type of div tag (e.g. 'challenge' or 'solution')
#' @param include `\[logical\]` if `TRUE`, the div tags will be included in
#' the output. Defaults to `FALSE`, which will only return the text between
#' the div tags.
get_divs = function(type = NULL, include = FALSE) {
get_divs(self$body, type = type, include = include)
},
#' @description
#' Extract the yaml metadata from the episode
get_yaml = function() {
yaml::yaml.load(self$yaml)
},
#' @description
#' Ammend or add a setup code block to use `{dovetail}`
#'
#' This will convert your lesson to use the {dovetail} R package for
#' processing specialized block quotes which will do two things:
#'
#' 1. convert your lesson from md to Rmd
#' 2. add to your setup chunk the following code
#' ```
#' library('dovetail')
#' source(dvt_opts())
#' ```
#' If there is no setup chunk, one will be created. If there is a setup
#' chunk, then the `source` and `knitr_fig_path` calls will be removed.
use_dovetail = function() {
if (private$mutations['use_dovetail']) {
return(invisible(self))
}
use_dovetail(self$body)
private$mutations['use_dovetail'] <- TRUE
invisible(self)
},
#' @description
#' Use the sandpaper package for processing
#'
#' This will convert your lesson to use the `{sandpaper}` R package for
#' processing the lesson instead of Jekyll (default). Doing this will have
#' the following effects:
#'
#' 1. code blocks that were marked with liquid tags (e.g. `{: .language-r}`
#' are converted to standard code blocks or Rmarkdown chunks (with
#' language information at the top of the code block)
#' 2. If rmarkdown is used and the lesson contains python code,
#' `library('reticulate')` will be added to the setup chunk of the
#' lesson.
#'
#' @param rmd if `TRUE`, lessons will be converted to RMarkdown documents
#' @param yml the list derived from the yml file for the episode
use_sandpaper = function(rmd = FALSE, yml = list()) {
if (rmd && private$mutations['use_sandpaper_rmd']) {
return(invisible(self))
}
if (!rmd && private$mutations['use_sandpaper_md']) {
return(invisible(self))
}
if (length(yml) == 0) {
pth <- fs::path(self$lesson, "_config.yml")
if (fs::file_exists(pth)) {
suppressWarnings(yml <- yaml::read_yaml(pth))
}
}
known <- fs::dir_ls(self$lesson, glob = "*md", recurse = 1L)
known <- fs::path_rel(known, start = self$lesson)
path <- fs::path_rel(self$path, start = self$lesson)
self$body <- use_sandpaper(self$body, rmd, yml, path, known)
# Remove the common yaml offenders
suppressWarnings(this_yaml <- self$get_yaml())
this_yaml[["root"]] <- NULL
this_yaml[["layout"]] <- NULL
self$yaml <- c("---", strsplit(yaml::as.yaml(this_yaml), "\n")[[1]], "---")
type <- if (rmd) 'use_sandpaper_rmd' else 'use_sandpaper_md'
private$mutations[type] <- TRUE
invisible(self)
},
#' @description
#' Remove error blocks
remove_error = function() {
if (private$mutations['remove_error']) {
return(invisible(self))
}
purrr::walk(self$error, xml2::xml_remove)
private$mutations['remove_error'] <- TRUE
invisible(self)
},
#' @description
#' Remove output blocks
remove_output = function() {
if (private$mutations['remove_output']) {
return(invisible(self))
}
purrr::walk(self$output, xml2::xml_remove)
private$mutations['remove_output'] <- TRUE
invisible(self)
},
#' @description
#' move the objectives yaml item to the body
move_objectives = function() {
if (private$mutations['move_objectives']) {
invisible(self)
}
dovetail <- private$mutations['use_dovetail']
yml <- self$get_yaml()
move_yaml(yml, self$body, "objectives", dovetail)
private$clear_yaml_item("objectives")
private$mutations['move_objectives'] <- TRUE
invisible(self)
},
#' @description
#' move the keypoints yaml item to the body
move_keypoints = function() {
if (private$mutations['move_keypoints']) {
invisible(self)
}
dovetail <- private$mutations['use_dovetail']
yml <- self$get_yaml()
move_yaml(yml, self$body, "keypoints", dovetail)
private$clear_yaml_item("keypoints")
private$mutations['move_keypoints'] <- TRUE
invisible(self)
},
#' @description
#' move the questions yaml item to the body
move_questions = function() {
if (private$mutations['move_questions']) {
invisible(self)
}
dovetail <- private$mutations['use_dovetail']
yml <- self$get_yaml()
move_yaml(yml, self$body, "questions", dovetail)
private$clear_yaml_item("questions")
private$mutations['move_questions'] <- TRUE
invisible(self)
},
#' @description
#' Create a graph of the top-level elements for the challenges.
#'
#' @param recurse if `TRUE` (default), the content of the solutions will be
#' included in the graph; `FALSE` will keep the solutions as `block_quote`
#' elements.
#' @return a data frame with four columns representing all the elements
#' within the challenges in the Episode:
#' - Block: The sequential number of the challenge block
#' - from: the inward elements
#' - to: the outward elements
#' - pos: the position in the markdown document
#'
#' Note that there are three special node names:
#' - challenge: start or end of the challenge block
#' - solution: start of the solution block
#' - lesson: start of the lesson block
#' @examples
#' scope <- Episode$new(file.path(lesson_fragment(), "_episodes", "17-scope.md"))
#' scope$get_challenge_graph()
get_challenge_graph = function(recurse = TRUE) {
purrr::map_dfr(self$challenges, feature_graph, recurse = recurse, .id = "Block")
},
#' @description show the markdown contents on the screen
#' @param n a subset of elements to show, default TRUE for all lines
#' @return a character vector with one line for each line of output
#' @examples
#' scope <- Episode$new(file.path(lesson_fragment(), "_episodes", "17-scope.md"))
#' scope$head()
#' scope$tail()
#' scope$show()
show = function(n = TRUE) {
super$show(n, get_stylesheet())
},
#' @description show the first n lines of markdown contents on the screen
#' @param n the number of lines to show from the top
#' @return a character vector with one line for each line of output
head = function(n = 6L) {
super$head(n, get_stylesheet())
},
#' @description show the first n lines of markdown contents on the screen
#' @param n the number of lines to show from the top
#' @return a character vector with one line for each line of output
tail = function(n = 6L) {
super$tail(n, get_stylesheet())
},
#' @description write the episode to disk as markdown
#'
#' @param path the path to write your file to. Defaults to an empty
#' directory in your temporary folder
#' @param format one of "md" (default) or "xml". This will
#' create a file with the correct extension in the path
#' @param edit if `TRUE`, the file will open in an editor. Defaults to
#' `FALSE`.
#' @return the episode object
#' @note The current XLST spec for {tinkr} does not support kramdown, which
#' the Carpentries Episodes are styled with, thus some block tags will be
#' destructively modified in the conversion.
#' @examples
#' scope <- Episode$new(file.path(lesson_fragment(), "_episodes", "17-scope.md"))
#' scope$write()
write = function(path = NULL, format = "md", edit = FALSE) {
if (is.null(path)) {
path <- fs::file_temp(pattern = "dir")
pb_message(glue::glue("Creating temporary directory '{path}'"))
fs::dir_create(path)
}
if (!fs::dir_exists(path)) {
stop(glue::glue("the directory '{path}' does not exist"), call. = FALSE)
}
the_file <- fs::path(path, self$name)
fs::path_ext(the_file) <- format
if (format %in% c("md", "Rmd")) {
stylesheet <- get_stylesheet()
on.exit(fs::file_delete(stylesheet))
tinkr::to_md(self, path = the_file, stylesheet_path = stylesheet)
} else if (format == "xml") {
xml2::write_xml(self$body, file = the_file, options = c("format", "as_xml"))
} else if (format == "html") {
xml2::write_html(self$body, file = the_file, options = c("format", "as_html"))
} else {
stop(glue::glue("format = '{format}' is not a valid option"), call. = FALSE)
}
# nocov start
if (fs::file_exists(the_file) && edit) file.edit(the_file)
# nocov end
return(invisible(self))
},
#' @description
#' Create a trimmed-down RMarkdown document that strips prose and contains
#' only important code chunks and challenge blocks without solutions.
#' @param path (handout) a path to an R Markdown file to write. If this is
#' `NULL`, no file will be written and the lines of the output will be
#' returned.
#' @param solutions if `TRUE`, include solutions in the output. Defaults to
#' `FALSE`, which removes the solution blocks.
#' @return a character vector if `path = NULL`, otherwise, it is called for
#' the side effect of creating a file.
#' @examples
#' lsn <- Lesson$new(lesson_fragment("sandpaper-fragment"), jekyll = FALSE)
#' e <- lsn$episodes[[1]]
#' cat(e$handout())
#' cat(e$handout(solution = TRUE))
handout = function(path = NULL, solutions = FALSE) {
cp <- self$clone(deep = TRUE)
cp$unblock()$use_sandpaper()
if (!solutions) {
purrr::walk(cp$solutions, xml2::xml_remove)
}
challenges <- purrr::map(cp$challenges, trim_fence)
code <- cp$code
code <- code[xml2::xml_attr(code, "purl") %in% "TRUE"]
isolate_elements(cp$body, challenges, code)
cp$yaml <- c()
res <- tinkr::to_md(cp, path = path, stylesheet_path = get_stylesheet())
if (is.null(path)) {
invisible(res)
} else {
invisible(self)
}
},
#' @description
#' Re-read episode from disk
#' @return the episode object
#' @examples
#' scope <- Episode$new(file.path(lesson_fragment(), "_episodes", "17-scope.md"))
#' xml2::xml_text(scope$tags[1])
#' xml2::xml_set_text(scope$tags[1], "{: .code}")
#' xml2::xml_text(scope$tags[1])
#' scope$reset()
#' xml2::xml_text(scope$tags[1])
reset = function() {
self$initialize(self$path)
private$mutations <- private$mutations & FALSE
return(invisible(self))
},
#' @description
#' Remove all elements except for those within block quotes that have a
#' kramdown tag. Note that this is a destructive process.
#' @return the Episode object, invisibly
#' @examples
#' scope <- Episode$new(file.path(lesson_fragment(), "_episodes", "17-scope.md"))
#' scope$body # a full document with block quotes and code blocks, etc
#' scope$isolate_blocks()$body # only one challenge block_quote
isolate_blocks = function() {
if (private$mutations['isolate_blocks']) {
return(invisible(self))
}
isolate_kram_blocks(self$body)
private$mutations['isolate_blocks'] <- TRUE
invisible(self)
},
#' @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"))
#' loop$body # a full document with block quotes and code blocks, etc
#' loop$get_blocks() # all the blocks in the episode
#' loop$unblock()
#' loop$get_blocks() # no blocks
#' loop$code # now there are two blocks with challenge tags
unblock = function(token = "#'", force = FALSE) {
if (!force && private$mutations['unblock']) {
return(invisible(self))
}
if (private$mutations['use_dovetail']) {
purrr::walk(self$get_blocks(), to_dovetail, token = token)
} else {
purrr::walk(self$get_blocks(level = 0), replace_with_div)
label_div_tags(self)
}
private$mutations['unblock'] <- TRUE
invisible(self)
},
#' @description Get a high-level summary of the elements in the episode
#' @return a data frame with counts of the following elements per page:
#' - sections: level 2 headings
#' - headings: all headings
#' - callouts: all callouts
#' - challenges: subset of callouts
#' - solutions: subset of callouts
#' - code: all code block elements (excluding inline code)
#' - output: subset of code that is displayed as output
#' - warnining: subset of code that is displayed as a warning
#' - error: subset of code that is displayed as an error
#' - images: all images in markdown or HTML
#' - links: all links in markdown or HTML
summary = function() {
sandpaper <- any(private$mutations[c('use_sandpaper_md', 'use_sandpaper_rmd')])
if (!sandpaper) {
issue_warning("Summary not guaranteed for kramdown formatted files.")
}
res <- list(
sections = list(),
headings = self$headings,
callouts = if (sandpaper) self$get_divs() else self$get_blocks(),
challenges = self$challenges,
solutions = self$solutions,
code = self$code,
output = self$output,
warning = self$warning,
error = self$error,
images = self$get_images(process = TRUE),
links = self$links
)
res$sections <- res$headings[xml2::xml_attr(res$headings, "level") == 2]
purrr::map_int(res, length)
},
#' @description perform validation on headings in a document.
#'
#' This will validate the following aspects of all headings:
#'
#' - first heading starts at level 2 (`first_heading_is_second_level`)
#' - greater than level 1 (`greater_than_first_level`)
#' - increse sequentially (e.g. no jumps from 2 to 4) (`are_sequential`)
#' - have names (`have_names`)
#' - unique in their own hierarchy (`are_unique`)
#'
#' @param verbose if `TRUE` (default), a message for each rule broken will
#' be issued to the stderr. if `FALSE`, this will be silent.
#' @param warn if `TRUE` (default), a warning will be issued if there are
#' any failures in the tests.
#' @return a data frame with a variable number of rows and the follwoing
#' columns:
#' - **episode** the filename of the episode
#' - **heading** the text from a heading
#' - **level** the heading level
#' - **pos** the position of the heading in the document
#' - **node** the XML node that represents the heading
#' - (the next five columns are the tests listed above)
#' - **path** the path to the file.
#'
#' Each row in the data frame represents an individual heading across the
#' Lesson. See [validate_headings()] for more details.
#' @examples
#' # Example: There are multiple headings called "Solution" that are not
#' # nested within a higher-level heading and will throw an error
#' loop <- Episode$new(file.path(lesson_fragment(), "_episodes", "14-looping-data-sets.md"))
#' loop$validate_headings()
validate_headings = function(verbose = TRUE, warn = TRUE) {
out <- validate_headings(self$headings,
self$get_yaml()$title,
offset = length(self$yaml))
if (is.null(out)) {
return(out)
}
res <- out$result
res$path <- fs::path_rel(self$path, self$lesson)
failures <- !all(apply(res[names(heading_tests)], MARGIN = 2L, all))
if (warn) {
throw_heading_warnings(res)
}
if (verbose && failures) {
show_heading_tree(out$tree)
}
invisible(res)
},
#' @description perform validation on divs in a document.
#'
#' This will validate the following aspects of divs. See [validate_divs()]
#' for details.
#'
#' - divs are of a known type (`is_known`)
#'
#' @param warn if `TRUE` (default), a warning message will be if there are
#' any divs determined to be invalid. Set to `FALSE` if you want the
#' table for processing later.
#' @return a logical `TRUE` for valid divs and `FALSE` for invalid
#' divs.
#' @examples
#' loop <- Episode$new(file.path(lesson_fragment(), "_episodes", "14-looping-data-sets.md"))
#' loop$validate_divs()
validate_divs = function(warn = TRUE) {
res <- validate_divs(self)
if (warn) {
throw_div_warnings(res)
}
invisible(res)
},
#' @description perform validation on links and images in a document.
#'
#' This will validate the following aspects of links. See [validate_links()]
#' for details.
#'
#' - External links use HTTPS (`enforce_https`)
#' - Internal links exist (`internal_okay`)
#' - External links are reachable (`all_reachable`) (planned)
#' - Images have alt text (`img_alt_text`)
#' - Link text is descriptive (`descriptive`)
#' - Link text is more than a single letter (`link_length`)
#'
#' @param warn if `TRUE` (default), a warning message will be if there are
#' any links determined to be invalid. Set to `FALSE` if you want the
#' table for processing later.
#' @return a logical `TRUE` for valid links and `FALSE` for invalid
#' links.
#' @examples
#' loop <- Episode$new(file.path(lesson_fragment(), "_episodes", "14-looping-data-sets.md"))
#' loop$validate_links()
validate_links = function(warn = TRUE) {
res <- validate_links(self)
if (warn) {
throw_link_warnings(res)
}
invisible(res)
}
),
active = list(
#' @field show_problems \[`list`\] a list of all the problems that occurred in parsing the episode
show_problems = function() {
private$problems
},
#' @field headings \[`xml_nodeset`\] all headings in the document
headings = function() {
get_headings(self$body)
},
#' @field links \[`xml_nodeset`\] all links (not images) in the document
links = function() {
xpath <- ".//md:link | .//md:text[klink]"
xml2::xml_find_all(self$body, xpath, self$ns)
},
#' @field images \[`xml_nodeset`\] all image sources in the document
images = function() {
get_images(self, process = FALSE)
},
#' @field tags \[`xml_nodeset`\] all the kramdown tags from the episode
tags = function() {
xml2::xml_find_all(self$body, ".//@ktag")
},
#' @field questions \[`character`\] the questions from the episode
questions = function() {
get_list_block(self, type = "questions", in_yaml = !private$mutations['move_questions'])
},
#' @field keypoints \[`character`\] the keypoints from the episode
keypoints = function() {
get_list_block(self, type = "keypoints", in_yaml = !private$mutations['move_keypoints'])
},
#' @field objectives \[`character`\] the objectives from the episode
objectives = function() {
get_list_block(self, type = "objectives", in_yaml = !private$mutations['move_objectives'])
},
#' @field challenges \[`xml_nodeset`\] all the challenges blocks from the episode
challenges = function() {
if (!private$mutations['unblock']) {
type <- "block"
} else if (private$mutations['use_dovetail']) {
type <- "chunk"
} else {
type <- "div"
}
get_challenges(self$body, type = type)
},
#' @field solutions \[`xml_nodeset`\] all the solutions blocks from the episode
solutions = function() {
if (!private$mutations['unblock']) {
type <- "block"
} else if (private$mutations['use_dovetail']) {
type <- "chunk"
} else {
type <- "div"
}
get_solutions(self$body, type = type)
},
#' @field output \[`xml_nodeset`\] all the output blocks from the episode
output = function() {
if (any(private$mutations[c('use_sandpaper_md', 'use_sandpaper_rmd')])) {
find_code_type(self$code, "output")
} else {
get_code(self$body, ".output")
}
},
#' @field error \[`xml_nodeset`\] all the error blocks from the episode
error = function() {
if (any(private$mutations[c('use_sandpaper_md', 'use_sandpaper_rmd')])) {
find_code_type(self$code, "error")
} else {
get_code(self$body, ".error")
}
},
#' @field warning \[`xml_nodeset`\] all the warning blocks from the episode
warning = function() {
if (any(private$mutations[c('use_sandpaper_md', 'use_sandpaper_rmd')])) {
find_code_type(self$code, "warning")
} else {
get_code(self$body, ".warning")
}
},
#' @field code \[`xml_nodeset`\] all the code blocks from the episode
code = function() {
get_code(self$body, type = NULL, attr = NULL)
},
#' @field name \[`character`\] the name of the source file without the path
name = function() {
fs::path_file(self$path)
},
#' @field lesson \[`character`\] the path to the lesson where the episode is from
lesson = function() {
components <- fs::path_split(self$path)[[1]]
sub_folders <- c("episodes", "learners", "instructors", "profiles",
"_episodes", "_episodes_rmd", "_extras")
the_folder <- sub_folders[purrr::map_lgl(sub_folders, is.element, components)]
if (length(the_folder) > 0L) {
# reverse the components so that we take only the sub folders relevant
# to our purposes
components <- rev(components)
# discard everything up to the folder
discard <- seq(which(components == the_folder)[1])
lsn <- fs::path_join(rev(components[-discard]))
} else {
# if we do not encounter one of the known subfolders, we must be at
# the top of a lesson
lsn <- fs::path_dir(self$path)
}
as.character(lsn)
},
#' @field has_children \[`logical`\] an indicator of the presence of child
#' files (`TRUE`) or their absence (`FALSE`)
has_children = function() {
length(self$children) > 0L
},
#' @field has_parents \[`logical`\] an indicator of the presence of parent
#' files (`TRUE`) or their absence (`FALSE`)
has_parents = function() {
length(self$parents) > 0L
}
),
private = list(
clear_yaml_item = function(what) {
yml <- self$get_yaml()
yml[[what]] <- NULL
self$yaml <- c("---", strsplit(yaml::as.yaml(yml), "\n")[[1]], "---")
},
record_problem = function(x) {
private$problems <- c(private$problems, x)
},
mutations = c(
unblock = FALSE, # have kramdown blocks been converted?
use_dovetail = FALSE, # are we keeping challenges in code blocks?
use_sandpaper_md = FALSE, # are we using a sandpaper lesson?
use_sandpaper_rmd = FALSE, # e.g. code has label, not liquid tag
isolate_blocks = FALSE, # does our lesson consist of only blocks?
move_keypoints = FALSE, # are the keypoints in the body?
move_questions = FALSE, # are the questions in the body?
move_objectives = FALSE, # are the objectives in the body?
remove_error = FALSE, # have errors been removed?
remove_output = FALSE, # have output been removed?
NULL
),
problems = list(),
deep_clone = function(name, value) {
if (name == "body") {
xml2::read_xml(as.character(value))
} else {
value
}
}
)
)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.