--- title: "Including Child Documents" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Including Child Documents} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} # comment to fix https://github.com/r-lib/pkgdown/issues/1843 knitr::opts_chunk$set( collapse = TRUE, compact = TRUE, comment = "#>" ) # setup the lesson to include a child document library("sandpaper") library("pegboard") snd <- asNamespace("sandpaper") # do not use the package cache because it will slow us down no_package_cache() suppressMessages(lsn <- create_lesson(tempfile(), open = FALSE)) # Create the child file ----------------------------------------- txt <- c("## Session Information\n", "The following is the session information of this R session", "\n```{r sessioninfo}", "sessionInfo()", "```\n") writeLines(txt, fs::path(lsn, "episodes", "files", "child.Rmd")) # Append the episode -------------------------------------------- ep <- Episode$new(fs::path(lsn, "episodes", "introduction.Rmd")) n <- length(xml2::xml_children(ep$body)) # we want to add the child just before the keypoints, which consist of the # last three paragraphs ep$add_md("```{r si, child = 'files/child.Rmd'}\n```\n", where = n - 3) ep$write(fs::path(lsn, "episodes"), "Rmd") # Load the lesson object ---------------------------------------- lsn_obj <- snd$this_lesson(lsn) ``` ## Introduction Writing a lesson using The Workbench consists of writing your lesson in markdown or R Markdown files, placing them in the `episodes/` directory and defining the order of those files in the `config.yaml` file. If you want to author a lesson that contains content that is repeated across episodes, before {sandpaper} version 0.14, you had to copy and paste the content across these documents, which would be frustrating to update down the line. This is where [child documents][child-doc] come in. You can save a repeated snippet of markdown inside of a separate file and have R Markdown render the parent as if it had that snippet embedded the whole time. The only difference between child documents and the regular episodic content is that these documents are not independent episodes in their own right. They do not need to have a title, they do not need to be listed in the `config.yaml`, they do not need any callout blocks. They only exist to be used by other markdown documents. Because of this, it is NOT recommended to make these documents overly complex. [child-doc]: https://bookdown.org/yihui/rmarkdown-cookbook/child-document.html ## An Example: One Child Document You can define these using the `child` attribute in code chunks in R Markdown files (note: this requires R Markdown). You can use as many child documents as you wish, but when you create child files, be sure the following two rules apply: 1. The child documents live in the `files/` folder under their parent folders 2. The reference to child files is _relative_ to the parent file. For example, you have a file structure like this: ```{r first-tree, echo = FALSE, comment = "#"} withr::with_dir(lsn, fs::dir_tree("./episodes/")) ``` The last few lines of `episodes/introduction.Rmd` looks like this: ````markdown ```{r echo-parent, echo = FALSE, results = 'asis'} lsn_obj$episodes[[1]]$tail(15) ``` ```` You can see there the declaration of the child document is an empty code chunk: ````{verbatim} ```{r si, child = "files/child.Rmd"} ``` ```` This tells {knitr} to take a detour and render the contents of `files/child.Rmd`(relative to the current working directory, which is the episodes folder). The entire document of `episodes/files/child.Rmd` looks like this: ````markdown ```{r echo-child, echo = FALSE, results = 'asis'} lsn_obj$children[[1]]$show() ``` ```` This means that when `episodes/introduction.Rmd` is built, `episodes/files/child.Rmd` will also be built, evaulating the `sessionInfo()`. So the resulting markdown document will look like this: ````markdown ```{r echo-built, echo = FALSE, results = 'asis'} snd$build_markdown(lsn, rebuild = FALSE, quiet = TRUE, slug = "introduction") lsn_obj$load_built() lsn_obj$built[[1]]$tail(47) ``` ```` ## Workspace consideration The {sandpaper} machinery aims to keep the working space as clean as possible while still allowing authors to write in a way that feels natural. During processing, assets (all `.R` files, everything in `files/`, `fig/`, and `data/`) are copied over from their source folders to `site/built/`. This allows us to build the R Markdown files using `site/built/` as the working directory which accomplishes two things: 1. The default sandpaper folder structure is flattened for the website. 2. any files the R Markdown document creates or deletes will not affect the source files of the project For example, let's say we added a chunk to our source R Markdown folder that will create a file in the `data/` directory called "time.txt" and then reads in the contents of that file. ````markdown ```{r add-writer, echo = FALSE, results = 'asis'} ep <- lsn_obj$episodes[[1]] txt <- c( "```{r setup, echo = FALSE}", "writeLines(format(Sys.time()), 'data/time.txt')", "cat(paste0('The time is: ', readLines('data/time.txt')))", "```" ) ep$add_md(txt, 1) ep$write(fs::path(lsn, "episodes"), format = "Rmd") ep$head(10) ``` ```` When the lesson gets built, the `episodes/` directory will continue to like this: ```{r echo-episodes, echo = FALSE, comment = "#"} snd$build_markdown(lsn, rebuild = FALSE, quiet = TRUE) withr::with_dir(lsn, fs::dir_tree("./episodes/")) ``` Notice that there is no file called `data/time.txt`. This is because that file was built inside the `site/built` directory. The contents of this directory contains the output _along with all of the other files in the lesson_: ```{r echo-built-too, echo = FALSE, comment = "#"} withr::with_dir(lsn, fs::dir_tree("./site/built/")) ``` You can see in `data/`, it now contains `time.txt`. The rendered Markdown file `site/built/introduction.md` reflects the updates: ````markdown ```{r echo-time, echo = FALSE, results = 'asis'} lsn_obj$load_built() lsn_obj$built[["site/built/introduction.md"]]$head(10) ``` ```` ## Child document assets When you use a child file, it is best to avoid relying on assets external to the child document itself. If you do decide to reference other assets, **child documents need to have assets written as if they were already embedded in the _build parent_**. In this case, when I say _build parent_, I am refering to the final parent of the child document where the output will eventually end up. Let's say you wanted to refererence a figure `episodes/fig/one.png` and a link to a document for learners `learners/info.md` from the child document, but instead it's located at `episodes/files/children/child.Rmd`, which is built by `episodes/introduction.Rmd`. ```{r setup-new-child, echo = FALSE, comment = "#"} lsn_obj <- snd$this_lesson(lsn) withr::with_dir(fs::path(lsn, "episodes", "files"), { fs::dir_create("children") fs::file_touch("../fig/one.png") writeLines(c( "---", "title: info", "---", "\nthis is a test file\n" ), con = "../../learners/info.md" ) fs::file_move("child.Rmd", "children/child.Rmd") }) # setup the child to use the correct code. withr::with_dir(lsn, fs::dir_tree("./episodes/")) ep <- lsn_obj$episodes[[1]] code <- ep$code xml2::xml_set_attr(code[[5]], "child", sQuote("files/children/child.Rmd", q = 2) ) ep$write(fs::path(lsn, "episodes"), format = "Rmd") lsn_obj <- snd$this_lesson(lsn) lsn_obj$children[[1]]$add_md(" Here is a link to [the info document](../learners/info.md) ![example](fig/one.png){alt='example figure'} ") lsn_obj$children[[1]]$write(fs::path(lsn, "episodes/files/children"), format = "Rmd" ) ``` The parent document (`episodes/introduction.Rmd`) referencing the child document would look like this: ````markdown ```{r echo-parent-assets, echo = FALSE, results = 'asis'} lsn_obj$episodes[[1]]$tail(15) ``` ```` In `episodes/files/children/child.Rmd`, under [internal link rules][internal-link-ref], you you should reference these resources with `fig/one.png` and `../learners/info.md`, which are _in the context of the build parent directory (in this case, `episodes/`)_, as demonstrated here: [internal-link-ref]: https://carpentries.github.io/sandpaper-docs/episodes.html#internal-links) ````markdown ```{r echo-child-assets, echo = FALSE, results = 'asis'} lsn_obj$children[[1]]$show() ``` ```` Again, the reason for this is because when the document gets built, the result is all in the context of the parent document in the `site/built` directory[^1]\: [^1]: there is a caveat to this paradigm, on translation to HTML, all links that begin with `../[folder]/` prefixes have those stripped in the final version of the site. ```{r show-built-assets, echo = FALSE, comment = "#"} snd$build_markdown(lsn, rebuild = TRUE, quiet = TRUE) withr::with_dir(lsn, fs::dir_tree("./site/built/")) ``` Which makes sense in `site/built/introduction.md`: ````markdown ```{r echo-built-assets, echo = FALSE, results = 'asis'} lsn_obj$load_built() lsn_obj$built[["site/built/introduction.md"]]$tail(50) ``` ```` ### What happens if we use relative links here? If you use relative links in the child document, you will find warnings about missing files during validation: ```{r, warn-missing, echo = FALSE} child <- lsn_obj$children[[1]] prepend_dest <- function(nodes, new = "../../") { dst <- xml2::xml_attr(nodes, "destination") xml2::xml_set_attr(nodes, "destination", paste0(new, dst)) } prepend_dest(child$links) prepend_dest(child$images) child$write(fs::path(lsn, "episodes/files/children"), format = "Rmd") snd$clear_this_lesson() validate_lesson(lsn) ``` ## Child documents of child documents It is also possible to reference _other child documents_ within the child document, but again, these **child documents paths must be relative to the build parent path** Thus, if you have a child document calling a second child document, you must write the relative path from the build parent. This means that if you want to reference `episodes/files/child-two.md` from `episodes/files/child-one.Rmd`, you must write it this way: in `episodes/files/child-one.Rmd`: ````{verbatim} ```{r child='files/child-two.md'} ``` ````