Programmatic Manipulation of
Quarto Documents for Teaching

JSM 2024 - Portland

Colin Rundel

Duke University

parsermd

implements a C++ parser and abstract syntax tree (AST) for Quarto and R Markdown documents in R.

  • supports manipulating ASTs (filtering, editing, etc.)

  • ability to directly source and render ASTs

  • project started during covid to aide in the grading of student assignments in a large machine learning course

  • v0.1.3 is on CRAN, v0.2.0 with full Quarto support on GitHub (CRAN imminently)

  • Examples today will be with Quarto but all of this works the same with RMarkdown

Quarto elements -   hello.qmd

---
title: "Hello, Quarto"
format:
  html:
    self-contained: true
---
  
```{r}
#| label: load-packages
#| include: false

library(tidyverse)
library(palmerpenguins)
```

## Meet Quarto

Quarto enables you to weave together content and executable code into a finished document. 
To learn more about Quarto see <https://quarto.org>.

## Meet the penguins

![](https://raw.githubusercontent.com/quarto-dev/quarto-web/main/docs/get-started/hello/rstudio/lter_penguins.png){style="float:right;" fig-alt="Illustration of three species of Palmer Archipelago penguins: Chinstrap, Gentoo, and Adelie. Artwork by @allison_horst." width="401"}

The `penguins` data from the [**palmerpenguins**](https://allisonhorst.github.io/palmerpenguins "palmerpenguins R package") 
package contains size measurements for `{r} nrow(penguins)` penguins from three species
observed on three islands in the Palmer Archipelago, Antarctica.

The plot below shows the relationship between flipper and bill lengths of these penguins.

```{r}
#| label: plot-penguins
#| warning: false
#| echo: false

ggplot(penguins, 
       aes(x = flipper_length_mm, y = bill_length_mm)) +
  geom_point(aes(color = species, shape = species)) +
  scale_color_manual(values = c("darkorange","purple","cyan4")) +
  labs(
    title = "Flipper and bill length",
    subtitle = "Dimensions for penguins at Palmer Station LTER",
    x = "Flipper length (mm)", y = "Bill length (mm)",
    color = "Penguin species", shape = "Penguin species"
  ) +
  theme_minimal()
```

## Other Quarto features

### Fenced divs

:::{.callout-note}
Note that there are five types of callouts, including: 
`note`, `tip`, `warning`, `caution`, and `important`.
:::

### Markdown code blocks

Some sample python code,

```python
import numpy as np
import matplotlib.pyplot as plt

r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
fig, ax = plt.subplots(
  subplot_kw = {'projection': 'polar'} 
)
ax.plot(theta, r)
ax.set_rticks([0.5, 1, 1.5, 2])
ax.grid(True)
plt.show()
```

### Short codes

Shortcodes are special markdown directives that generate various types of content,

{{< lipsum 1 >}}

Elements as AST

qmd = parse_qmd("hello.qmd")
qmd |> print(flat = TRUE)
├── YAML [2 fields]
├── Markdown [1 line]
├── Chunk [r, 1 option, 4 lines] - load-packages
├── Heading [h2] - Meet Quarto
├── Markdown [2 lines]
├── Heading [h2] - Meet the penguins
├── Markdown [8 lines]
├── Chunk [r, 3 options, 12 lines] - plot-penguins
├── Heading [h2] - Other Quarto features
├── Heading [h3] - Fenced divs
├── Open Fenced div [.callout-note]
├── Markdown [2 lines]
├── Close Fenced div 
├── Heading [h3] - Markdown code blocks
├── Markdown [2 lines]
├── Code block [python, 12 lines]
├── Heading [h3] - Short codes
└── Markdown [3 lines]
qmd |> print()
├── YAML [2 fields]
├── Markdown [1 line]
├── Chunk [r, 1 option, 4 lines] - load-packages
├── Heading [h2] - Meet Quarto
│   └── Markdown [2 lines]
├── Heading [h2] - Meet the penguins
│   ├── Markdown [8 lines]
│   └── Chunk [r, 3 options, 12 lines] - plot-penguins
└── Heading [h2] - Other Quarto features
    ├── Heading [h3] - Fenced divs
    │   ├── Open Fenced div [.callout-note]
    │   │   └── Markdown [2 lines]
    │   └── Close Fenced div 
    ├── Heading [h3] - Markdown code blocks
    │   ├── Markdown [2 lines]
    │   └── Code block [python, 12 lines]
    └── Heading [h3] - Short codes
        └── Markdown [3 lines]

As a tibble

parse_qmd("hello.qmd") |> as_tibble()
# A tibble: 18 × 5
   sec_h2                sec_h3               type          label ast           
   <chr>                 <chr>                <chr>         <chr> <rmd_ast>     
 1 <NA>                  <NA>                 rmd_yaml      <NA>  <yaml>        
 2 <NA>                  <NA>                 rmd_markdown  <NA>  <markdown>    
 3 <NA>                  <NA>                 rmd_chunk     load… <chunk [r]>   
 4 Meet Quarto           <NA>                 rmd_heading   <NA>  <heading [h2]>
 5 Meet Quarto           <NA>                 rmd_markdown  <NA>  <markdown>    
 6 Meet the penguins     <NA>                 rmd_heading   <NA>  <heading [h2]>
 7 Meet the penguins     <NA>                 rmd_markdown  <NA>  <markdown>    
 8 Meet the penguins     <NA>                 rmd_chunk     plot… <chunk [r]>   
 9 Other Quarto features <NA>                 rmd_heading   <NA>  <heading [h2]>
10 Other Quarto features Fenced divs          rmd_heading   <NA>  <heading [h3]>
11 Other Quarto features Fenced divs          rmd_fenced_d… <NA>  <rmd_fn__ [1]>
12 Other Quarto features Fenced divs          rmd_markdown  <NA>  <markdown>    
13 Other Quarto features Fenced divs          rmd_fenced_d… <NA>  <fdiv [close]>
14 Other Quarto features Markdown code blocks rmd_heading   <NA>  <heading [h3]>
15 Other Quarto features Markdown code blocks rmd_markdown  <NA>  <markdown>    
16 Other Quarto features Markdown code blocks rmd_code_blo… <NA>  <code block>  
17 Other Quarto features Short codes          rmd_heading   <NA>  <heading [h3]>
18 Other Quarto features Short codes          rmd_markdown  <NA>  <markdown>    

Why hierarchy?

Use a CSS selector like approach to target specific nodes based on headings,

qmd |> rmd_select(by_section("Meet Quarto"))
└── Heading [h2] - Meet Quarto
    └── Markdown [2 lines]
qmd |> rmd_select(by_section("Fenced divs"))
└── Heading [h3] - Fenced divs
    ├── Open Fenced div [.callout-note]
    │   └── Markdown [2 lines]
    └── Close Fenced div 
qmd |> rmd_select(by_section(c("Other Quarto features", "*code*")))
└── Heading [h2] - Other Quarto features
    ├── Heading [h3] - Markdown code blocks
    │   ├── Markdown [2 lines]
    │   └── Code block [python, 12 lines]
    └── Heading [h3] - Short codes
        └── Markdown [3 lines]
qmd |> rmd_select(by_section(c("Other Quarto features", "*code*"), keep_parents = FALSE))
├── Heading [h3] - Markdown code blocks
│   ├── Markdown [2 lines]
│   └── Code block [python, 12 lines]
└── Heading [h3] - Short codes
    └── Markdown [3 lines]

ast to text

These ASTs can be converted back to Quarto documents at any point,

qmd |> 
  rmd_select(by_section("Meet Quarto")) |>
  as_document() |>
  cat(sep = "\n")
## Meet Quarto

Quarto enables you to weave together content and executable code into a finished document. To learn more about Quarto see <https://quarto.org>.
qmd |> 
  rmd_select(by_section("Fenced divs")) |>
  as_document() |>
  cat(sep = "\n")
### Fenced divs

::: {.callout-note}

Note that there are five types of callouts, including: 
`note`, `tip`, `warning`, `caution`, and `important`.

:::
qmd |> 
  rmd_select(by_section(c("Other Quarto features", "*code blocks*"), keep_parents = FALSE)) |>
  as_document() |>
  cat(sep = "\n")
### Markdown code blocks

Some sample python code,

``` python
import numpy as np
import matplotlib.pyplot as plt

r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
fig, ax = plt.subplots(
  subplot_kw = {'projection': 'polar'} 
)
ax.plot(theta, r)
ax.set_rticks([0.5, 1, 1.5, 2])
ax.grid(True)
plt.show()
```

Additional selectors

While a work in progress, we are adding additional helpers based on tidyselect,

qmd |> rmd_select("plot-penguins")
└── Chunk [r, 3 options, 12 lines] - plot-penguins
qmd |> rmd_select(has_label("*p*"))
├── Chunk [r, 1 option, 4 lines] - load-packages
└── Chunk [r, 3 options, 12 lines] - plot-penguins
qmd |> rmd_select(has_type(c("rmd_yaml", "rmd_chunk")))
├── YAML [2 fields]
├── Chunk [r, 1 option, 4 lines] - load-packages
└── Chunk [r, 3 options, 12 lines] - plot-penguins
qmd |> rmd_select(!has_type("rmd_markdown"))
├── YAML [2 fields]
├── Chunk [r, 1 option, 4 lines] - load-packages
├── Heading [h2] - Meet Quarto
├── Heading [h2] - Meet the penguins
│   └── Chunk [r, 3 options, 12 lines] - plot-penguins
└── Heading [h2] - Other Quarto features
    ├── Heading [h3] - Fenced divs
    │   ├── Open Fenced div [.callout-note]
    │   └── Close Fenced div 
    ├── Heading [h3] - Markdown code blocks
    │   └── Code block [python, 12 lines]
    └── Heading [h3] - Short codes

Rendering

ASTs can also be directly rendered

qmd |> render("hello_quarto")

In Practice

Spring 2024 - hw1

fs::dir_tree("repos/")
repos/
├── hw01_team01
│   ├── README.md
│   ├── hw1.Rproj
│   └── hw1.qmd
├── hw01_team02
│   ├── README.md
│   ├── hw1.Rproj
│   └── hw1.qmd
├── hw01_team03
│   ├── README.md
│   ├── hw1.Rproj
│   └── hw1.qmd
├── hw01_team04
│   ├── README.md
│   ├── hw1.Rproj
│   └── hw1.qmd
└── hw01_team05
    ├── README.md
    ├── hw1.Rproj
    └── hw1.qmd

Homework structure

(hw1 = parse_qmd("repos/hw01_team01/hw1.qmd"))
├── YAML [2 fields]
├── Chunk [r, 0 options, 1 line] - load-packages
├── Heading [h2] - Task 1 - Implement fizzbuzz
│   ├── Heading [h3] - Write up
│   │   └── Markdown [12 lines]
│   ├── Heading [h3] - Function
│   │   └── Chunk [r, 0 options, 32 lines] - fizzbuzz
│   └── Heading [h3] - Testing
│       ├── Heading [h4] - Valid Inputs
│       │   └── Chunk [r, 1 option, 9 lines] - good_inputs
│       └── Heading [h4] - Bad Inputs
│           ├── Chunk [r, 0 options, 4 lines] - throws_error
│           ├── Chunk [r, 1 option, 15 lines] - bad_inputs
│           └── Chunk [r, 0 options, 6 lines] - additional-test-cases
├── Heading [h2] - Task 2 - Re-Implement fizzbuzz
│   ├── Heading [h3] - Write up
│   │   └── Markdown [8 lines]
│   ├── Heading [h3] - Function
│   │   └── Chunk [r, 0 options, 52 lines] - fizzbuzz_s3
│   └── Heading [h3] - Testing
│       ├── Heading [h4] - Valid Inputs
│       │   └── Chunk [r, 1 option, 9 lines] - good_inputs_s3
│       └── Heading [h4] - Bad Inputs
│           ├── Chunk [r, 1 option, 15 lines] - bad_inputs_s3
│           └── Chunk [r, 0 options, 6 lines] - additional-test-cases-fizzbuzz_s3
└── Heading [h2] - Task 3 - Printing Secrets
    ├── Heading [h3] - Write up
    │   └── Markdown [7 lines]
    ├── Heading [h3] - Function
    │   ├── Chunk [r, 0 options, 6 lines] - secret
    │   ├── Chunk [r, 0 options, 20 lines] - numeric_secret
    │   ├── Chunk [r, 0 options, 4 lines] - char_secret
    │   └── Chunk [r, 0 options, 50 lines] - print_secret
    └── Heading [h3] - Testing
        ├── Chunk [r, 0 options, 9 lines] - unnamed-chunk-1
        ├── Heading [h4] - Valid Inputs
        │   └── Chunk [r, 1 option, 11 lines] - good_inputs_secret
        └── Heading [h4] - Bad Inputs
            ├── Chunk [r, 1 option, 12 lines] - bad_inputs_secret
            └── Chunk [r, 0 options, 10 lines] - additional-test-cases-secret

Simplifying the document

hw1 |>
  rmd_select(has_type("rmd_yaml"), by_section(c("Task 1*")))
├── YAML [2 fields]
└── Heading [h2] - Task 1 - Implement fizzbuzz
    ├── Heading [h3] - Write up
    │   └── Markdown [12 lines]
    ├── Heading [h3] - Function
    │   └── Chunk [r, 0 options, 32 lines] - fizzbuzz
    └── Heading [h3] - Testing
        ├── Heading [h4] - Valid Inputs
        │   └── Chunk [r, 1 option, 9 lines] - good_inputs
        └── Heading [h4] - Bad Inputs
            ├── Chunk [r, 0 options, 4 lines] - throws_error
            ├── Chunk [r, 1 option, 15 lines] - bad_inputs
            └── Chunk [r, 0 options, 6 lines] - additional-test-cases
hw1 |>
  rmd_select(has_type("rmd_yaml"), by_section(c("Task 1*"))) |>
  rmd_select(!by_section("Testing"))
├── YAML [2 fields]
└── Heading [h2] - Task 1 - Implement fizzbuzz
    ├── Heading [h3] - Write up
    │   └── Markdown [12 lines]
    └── Heading [h3] - Function
        └── Chunk [r, 0 options, 32 lines] - fizzbuzz

Adding something more

hw1 |>
  rmd_select(has_type("rmd_yaml"), by_section(c("Task 1*"))) |>
  rmd_select(!by_section("Testing")) |>
  rmd_ast_append(
    rmd_chunk("tests", code = c(
      "fizzbuzz(1:5)",
      "fizzbuzz(5:1)",
      "fizzbuzz(-1)"
    ), options = list(error = TRUE))
  )
├── YAML [2 fields]
└── Heading [h2] - Task 1 - Implement fizzbuzz
    ├── Heading [h3] - Write up
    │   └── Markdown [12 lines]
    └── Heading [h3] - Function
        ├── Chunk [r, 0 options, 32 lines] - fizzbuzz
        └── Chunk [r, 1 option, 3 lines] - tests

Rendering the result

hw1 |>
  rmd_select(has_type("rmd_yaml"), by_section(c("Task 1*"))) |>
  rmd_select(!by_section("Testing")) |>
  rmd_ast_append(
    rmd_chunk("tests", code = c(
      "fizzbuzz(1:5)",
      "fizzbuzz(5:1)",
      "fizzbuzz(-1)"
    ), options = list(error = TRUE))
  ) |>
  render("hw1_task1")


processing file: hw1_task1.qmd

  |                                                                 
  |                                                           |   0%
  |                                                                 
  |............                                               |  20%           
  |                                                                 
  |........................                                   |  40% [fizzbuzz]
  |                                                                 
  |...................................                        |  60%           
  |                                                                 
  |...............................................            |  80% [tests]   
  |                                                                 
  |...........................................................| 100%           
                                                                                                     
output file: hw1_task1.knit.md

pandoc 
  to: html
  output-file: hw1_task1.html
  standalone: true
  self-contained: true
  section-divs: true
  html-math-method: mathjax
  wrap: none
  default-image-extension: png
  
metadata
  document-css: false
  link-citations: true
  date-format: long
  lang: en
  title: Homework 1
  
Output created: hw1_task1.html

Interactive tests

If you prefer to be able to interactively run the tests, you can use rmd_source() to execute code from the ast in your environment,

hw1 |>
  rmd_select("fizzbuzz") |>
  rmd_source()
fizzbuzz(1:5)
[1] "1"    "2"    "Fizz" "4"    "Buzz"
fizzbuzz(5:1)
[1] "Buzz" "4"    "Fizz" "2"    "1"   
fizzbuzz(-1)
Error in fizzbuzz(-1): Input vector has values less than 0

“Of course someone has to write for loops. It doesn’t have to be you.”
- Jenny Bryan

Document collections

This is a newish feature and still a bit experimental,

(hw1 = parse_qmd_collection("repos/"))
# A tibble: 5 × 3
  name                path                                             ast      
  <chr>               <fs::path>                                       <list>   
1 hw01_team01/hw1.qmd …Presentations/JSM2024/repos/hw01_team01/hw1.qmd <rmd_ast>
2 hw01_team02/hw1.qmd …Presentations/JSM2024/repos/hw01_team02/hw1.qmd <rmd_ast>
3 hw01_team03/hw1.qmd …Presentations/JSM2024/repos/hw01_team03/hw1.qmd <rmd_ast>
4 hw01_team04/hw1.qmd …Presentations/JSM2024/repos/hw01_team04/hw1.qmd <rmd_ast>
5 hw01_team05/hw1.qmd …Presentations/JSM2024/repos/hw01_team05/hw1.qmd <rmd_ast>
hw1 |> as_ast()
├── Heading [h1] - hw01_team01/hw1.qmd
│   ├── Chunk [r, 0 options, 1 line] - load-packages
│   ├── Heading [h2] - Task 1 - Implement fizzbuzz
│   │   ├── Heading [h3] - Write up
│   │   │   └── Markdown [12 lines]
│   │   ├── Heading [h3] - Function
│   │   │   └── Chunk [r, 0 options, 32 lines] - fizzbuzz_1
│   │   └── Heading [h3] - Testing
│   │       ├── Heading [h4] - Valid Inputs
│   │       │   └── Chunk [r, 1 option, 9 lines] - good_inputs_1
│   │       └── Heading [h4] - Bad Inputs
│   │           ├── Chunk [r, 0 options, 4 lines] - throws_error_1
│   │           ├── Chunk [r, 1 option, 15 lines] - bad_inputs_1
│   │           └── Chunk [r, 0 options, 6 lines] - additional-test-cases
│   ├── Heading [h2] - Task 2 - Re-Implement fizzbuzz
│   │   ├── Heading [h3] - Write up
│   │   │   └── Markdown [8 lines]
│   │   ├── Heading [h3] - Function
│   │   │   └── Chunk [r, 0 options, 52 lines] - fizzbuzz_s3_1
│   │   └── Heading [h3] - Testing
│   │       ├── Heading [h4] - Valid Inputs
│   │       │   └── Chunk [r, 1 option, 9 lines] - good_inputs_s3_1
│   │       └── Heading [h4] - Bad Inputs
│   │           ├── Chunk [r, 1 option, 15 lines] - bad_inputs_s3_1
│   │           └── Chunk [r, 0 options, 6 lines] - additional-test-cases-fizzbuzz_s3
│   └── Heading [h2] - Task 3 - Printing Secrets
│       ├── Heading [h3] - Write up
│       │   └── Markdown [7 lines]
│       ├── Heading [h3] - Function
│       │   ├── Chunk [r, 0 options, 6 lines] - secret_1
│       │   ├── Chunk [r, 0 options, 20 lines] - numeric_secret
│       │   ├── Chunk [r, 0 options, 4 lines] - char_secret
│       │   └── Chunk [r, 0 options, 50 lines] - print_secret_1
│       └── Heading [h3] - Testing
│           ├── Chunk [r, 0 options, 9 lines] - unnamed-chunk-1_1
│           ├── Heading [h4] - Valid Inputs
│           │   └── Chunk [r, 1 option, 11 lines] - good_inputs_secret_1
│           └── Heading [h4] - Bad Inputs
│               ├── Chunk [r, 1 option, 12 lines] - bad_inputs_secret_1
│               └── Chunk [r, 0 options, 10 lines] - additional-test-cases-secret
├── Heading [h1] - hw01_team02/hw1.qmd
│   ├── Heading [h2] - Task 1 - Implement fizzbuzz
│   │   ├── Heading [h3] - Write up
│   │   │   └── Markdown [2 lines]
│   │   ├── Heading [h3] - Function
│   │   │   └── Chunk [r, 0 options, 35 lines] - fizzbuzz_2
│   │   └── Heading [h3] - Testing
│   │       ├── Heading [h4] - Valid Inputs
│   │       │   └── Chunk [r, 1 option, 11 lines] - good_inputs_2
│   │       └── Heading [h4] - Bad Inputs
│   │           ├── Chunk [r, 0 options, 4 lines] - throws_error_2
│   │           └── Chunk [r, 1 option, 17 lines] - bad_inputs_2
│   ├── Heading [h2] - Task 2 - Re-Implement fizzbuzz
│   │   ├── Heading [h3] - Write up
│   │   │   └── Markdown [2 lines]
│   │   ├── Heading [h3] - Function
│   │   │   └── Chunk [r, 0 options, 50 lines] - fizzbuzz_s3_2
│   │   └── Heading [h3] - Testing
│   │       ├── Heading [h4] - Valid Inputs
│   │       │   └── Chunk [r, 1 option, 11 lines] - good_inputs_s3_2
│   │       └── Heading [h4] - Bad Inputs
│   │           └── Chunk [r, 1 option, 17 lines] - bad_inputs_s3_2
│   └── Heading [h2] - Task 3 - Printing Secrets
│       ├── Heading [h3] - Write up
│       │   └── Markdown [2 lines]
│       ├── Heading [h3] - Function
│       │   ├── Chunk [r, 0 options, 8 lines] - secret_2
│       │   ├── Chunk [r, 0 options, 30 lines] - print_secret_2
│       │   ├── Chunk [r, 0 options, 12 lines] - unnamed-chunk-1_2
│       │   ├── Chunk [r, 0 options, 12 lines] - print_character_helper
│       │   ├── Chunk [r, 0 options, 22 lines] - print_numeric_helper
│       │   └── Chunk [r, 0 options, 29 lines] - print_complex_helper
│       └── Heading [h3] - Testing
│           ├── Chunk [r, 0 options, 9 lines] - unnamed-chunk-2_1
│           ├── Heading [h4] - Valid Inputs
│           │   └── Chunk [r, 1 option, 13 lines] - good_inputs_secret_2
│           └── Heading [h4] - Bad Inputs
│               └── Chunk [r, 1 option, 16 lines] - bad_inputs_secret_2
├── Heading [h1] - hw01_team03/hw1.qmd
│   ├── Heading [h2] - Task 1 - Implement fizzbuzz
│   │   ├── Heading [h3] - Write up
│   │   │   └── Markdown [11 lines]
│   │   ├── Heading [h3] - Function
│   │   │   └── Chunk [r, 0 options, 27 lines] - fizzbuzz_3
│   │   └── Heading [h3] - Testing
│   │       ├── Heading [h4] - Valid Inputs
│   │       │   └── Chunk [r, 1 option, 15 lines] - good_inputs_3
│   │       └── Heading [h4] - Bad Inputs
│   │           ├── Chunk [r, 0 options, 4 lines] - throws_error_3
│   │           └── Chunk [r, 1 option, 16 lines] - bad_inputs_3
│   ├── Heading [h2] - Task 2 - Re-Implement fizzbuzz
│   │   ├── Heading [h3] - Write up
│   │   │   └── Markdown [12 lines]
│   │   ├── Heading [h3] - Function
│   │   │   └── Chunk [r, 0 options, 25 lines] - fizzbuzz_s3_3
│   │   └── Heading [h3] - Testing
│   │       ├── Heading [h4] - Valid Inputs
│   │       │   └── Chunk [r, 1 option, 10 lines] - good_inputs_s3_3
│   │       └── Heading [h4] - Bad Inputs
│   │           └── Chunk [r, 1 option, 18 lines] - bad_inputs_s3_3
│   └── Heading [h2] - Task 3 - Printing Secrets
│       ├── Heading [h3] - Write up
│       │   └── Markdown [19 lines]
│       ├── Heading [h3] - Function
│       │   ├── Chunk [r, 0 options, 6 lines] - secret_3
│       │   └── Chunk [r, 0 options, 97 lines] - print_secret_3
│       └── Heading [h3] - Testing
│           ├── Chunk [r, 0 options, 9 lines] - unnamed-chunk-1_3
│           ├── Heading [h4] - Valid Inputs
│           │   └── Chunk [r, 1 option, 14 lines] - good_inputs_secret_3
│           └── Heading [h4] - Bad Inputs
│               └── Chunk [r, 1 option, 12 lines] - bad_inputs_secret_3
├── Heading [h1] - hw01_team04/hw1.qmd
│   ├── Heading [h2] - Task 1 - Implement fizzbuzz
│   │   ├── Heading [h3] - Write up
│   │   │   └── Markdown [11 lines]
│   │   ├── Heading [h3] - Function
│   │   │   └── Chunk [r, 0 options, 32 lines] - fizzbuzz_4
│   │   └── Heading [h3] - Testing
│   │       ├── Heading [h4] - Valid Inputs
│   │       │   └── Chunk [r, 1 option, 12 lines] - good-inputs-fizzbuzz
│   │       └── Heading [h4] - Bad Inputs
│   │           ├── Chunk [r, 0 options, 4 lines] - throws-error
│   │           └── Chunk [r, 1 option, 22 lines] - bad-inputs-fizzbuzz
│   ├── Heading [h2] - Task 2 - Re-Implement fizzbuzz
│   │   ├── Heading [h3] - Write up
│   │   │   └── Markdown [8 lines]
│   │   ├── Heading [h3] - Function
│   │   │   └── Chunk [r, 0 options, 43 lines] - fizzbuzz-s3
│   │   └── Heading [h3] - Testing
│   │       ├── Heading [h4] - Valid Inputs
│   │       │   └── Chunk [r, 1 option, 13 lines] - good-inputs-s3
│   │       └── Heading [h4] - Bad Inputs
│   │           └── Chunk [r, 1 option, 21 lines] - bad-inputs-s3
│   └── Heading [h2] - Task 3 - Printing Secrets
│       ├── Heading [h3] - Write up
│       │   └── Markdown [7 lines]
│       ├── Heading [h3] - Function
│       │   ├── Chunk [r, 0 options, 7 lines] - secret_4
│       │   └── Chunk [r, 0 options, 36 lines] - print-secret
│       └── Heading [h3] - Testing
│           ├── Chunk [r, 0 options, 10 lines] - print-secret-function
│           ├── Heading [h4] - Valid Inputs
│           │   └── Chunk [r, 1 option, 15 lines] - good-inputs-secret
│           └── Heading [h4] - Bad Inputs
│               └── Chunk [r, 1 option, 27 lines] - bad-inputs-secret
└── Heading [h1] - hw01_team05/hw1.qmd
    ├── Heading [h2] - Task 1 - Implement fizzbuzz
    │   ├── Heading [h3] - Write up
    │   │   ├── Markdown [4 lines]
    │   │   └── Raw Attr Chunk [html, 5 lines]
    │   ├── Heading [h3] - Function
    │   │   └── Chunk [r, 0 options, 42 lines] - fizzbuzz_5
    │   └── Heading [h3] - Testing
    │       ├── Heading [h4] - Valid Inputs
    │       │   └── Chunk [r, 1 option, 12 lines] - good_inputs_4
    │       └── Heading [h4] - Bad Inputs
    │           ├── Chunk [r, 0 options, 4 lines] - throws_error_4
    │           └── Chunk [r, 1 option, 20 lines] - bad_inputs_4
    ├── Heading [h2] - Task 2 - Re-Implement fizzbuzz
    │   ├── Heading [h3] - Write up
    │   │   ├── Markdown [4 lines]
    │   │   └── Raw Attr Chunk [html, 5 lines]
    │   ├── Heading [h3] - Function
    │   │   └── Chunk [r, 0 options, 50 lines] - unnamed-chunk-1_4
    │   └── Heading [h3] - Testing
    │       ├── Heading [h4] - Valid Inputs
    │       │   └── Chunk [r, 1 option, 12 lines] - good_inputs_s3_4
    │       └── Heading [h4] - Bad Inputs
    │           └── Chunk [r, 1 option, 20 lines] - bad_inputs_s3_4
    └── Heading [h2] - Task 3 - Printing Secrets
        ├── Heading [h3] - Write up
        │   ├── Markdown [12 lines]
        │   └── Raw Attr Chunk [html, 5 lines]
        ├── Heading [h3] - Function
        │   ├── Chunk [r, 0 options, 6 lines] - secret_5
        │   ├── Chunk [r, 0 options, 9 lines] - makeAsterisks_helper
        │   └── Chunk [r, 0 options, 69 lines] - print_secret_4
        └── Heading [h3] - Testing
            ├── Chunk [r, 0 options, 9 lines] - unnamed-chunk-2_2
            ├── Heading [h4] - Valid Inputs
            │   └── Chunk [r, 1 option, 13 lines] - good_inputs_secret_4
            └── Heading [h4] - Bad Inputs
                └── Chunk [r, 1 option, 18 lines] - bad_inputs_secret_4

Filtering

(task1 = hw1 |> 
   rmd_select(by_section(c("Task 1*"))) |>
   rmd_select(!by_section("Testing"))
)
# A tibble: 5 × 3
  name                path                                             ast      
  <chr>               <fs::path>                                       <list>   
1 hw01_team01/hw1.qmd …Presentations/JSM2024/repos/hw01_team01/hw1.qmd <rmd_ast>
2 hw01_team02/hw1.qmd …Presentations/JSM2024/repos/hw01_team02/hw1.qmd <rmd_ast>
3 hw01_team03/hw1.qmd …Presentations/JSM2024/repos/hw01_team03/hw1.qmd <rmd_ast>
4 hw01_team04/hw1.qmd …Presentations/JSM2024/repos/hw01_team04/hw1.qmd <rmd_ast>
5 hw01_team05/hw1.qmd …Presentations/JSM2024/repos/hw01_team05/hw1.qmd <rmd_ast>
task1 |> as_ast()
├── Heading [h1] - hw01_team01/hw1.qmd
│   └── Heading [h2] - Task 1 - Implement fizzbuzz
│       ├── Heading [h3] - Write up
│       │   └── Markdown [12 lines]
│       └── Heading [h3] - Function
│           └── Chunk [r, 0 options, 32 lines] - fizzbuzz_1
├── Heading [h1] - hw01_team02/hw1.qmd
│   └── Heading [h2] - Task 1 - Implement fizzbuzz
│       ├── Heading [h3] - Write up
│       │   └── Markdown [2 lines]
│       └── Heading [h3] - Function
│           └── Chunk [r, 0 options, 35 lines] - fizzbuzz_2
├── Heading [h1] - hw01_team03/hw1.qmd
│   └── Heading [h2] - Task 1 - Implement fizzbuzz
│       ├── Heading [h3] - Write up
│       │   └── Markdown [11 lines]
│       └── Heading [h3] - Function
│           └── Chunk [r, 0 options, 27 lines] - fizzbuzz_3
├── Heading [h1] - hw01_team04/hw1.qmd
│   └── Heading [h2] - Task 1 - Implement fizzbuzz
│       ├── Heading [h3] - Write up
│       │   └── Markdown [11 lines]
│       └── Heading [h3] - Function
│           └── Chunk [r, 0 options, 32 lines] - fizzbuzz_4
└── Heading [h1] - hw01_team05/hw1.qmd
    └── Heading [h2] - Task 1 - Implement fizzbuzz
        ├── Heading [h3] - Write up
        │   ├── Markdown [4 lines]
        │   └── Raw Attr Chunk [html, 5 lines]
        └── Heading [h3] - Function
            └── Chunk [r, 0 options, 42 lines] - fizzbuzz_5

Rendered

task1 |> render(name = "hw1_task1_coll", quarto_args = c("--embed-resources"))

With modifications

(task1 = task1 |> 
  rmd_ast_append(
    rmd_chunk("tests", code = c(
      "fizzbuzz(1:5)",
      "fizzbuzz(5:1)",
      "fizzbuzz(-1)"
    ), options = list(error = TRUE))
  ) |>
  as_ast() |>
  rmd_ast_prepend(
    rmd_chunk(
      "load-libraries", 
      code = "library(tidyverse)", 
      options = list(message=FALSE)
    )
  )
)
task1 |> render("hw1_task1_coll2", quarto_args = c("--embed-resources"))
├── Chunk [r, 1 option, 1 line] - load-libraries
├── Heading [h1] - hw01_team01/hw1.qmd
│   └── Heading [h2] - Task 1 - Implement fizzbuzz
│       ├── Heading [h3] - Write up
│       │   └── Markdown [12 lines]
│       └── Heading [h3] - Function
│           ├── Chunk [r, 0 options, 32 lines] - fizzbuzz_1
│           └── Chunk [r, 1 option, 3 lines] - tests_1
├── Heading [h1] - hw01_team02/hw1.qmd
│   └── Heading [h2] - Task 1 - Implement fizzbuzz
│       ├── Heading [h3] - Write up
│       │   └── Markdown [2 lines]
│       └── Heading [h3] - Function
│           ├── Chunk [r, 0 options, 35 lines] - fizzbuzz_2
│           └── Chunk [r, 1 option, 3 lines] - tests_2
├── Heading [h1] - hw01_team03/hw1.qmd
│   └── Heading [h2] - Task 1 - Implement fizzbuzz
│       ├── Heading [h3] - Write up
│       │   └── Markdown [11 lines]
│       └── Heading [h3] - Function
│           ├── Chunk [r, 0 options, 27 lines] - fizzbuzz_3
│           └── Chunk [r, 1 option, 3 lines] - tests_3
├── Heading [h1] - hw01_team04/hw1.qmd
│   └── Heading [h2] - Task 1 - Implement fizzbuzz
│       ├── Heading [h3] - Write up
│       │   └── Markdown [11 lines]
│       └── Heading [h3] - Function
│           ├── Chunk [r, 0 options, 32 lines] - fizzbuzz_4
│           └── Chunk [r, 1 option, 3 lines] - tests_4
└── Heading [h1] - hw01_team05/hw1.qmd
    └── Heading [h2] - Task 1 - Implement fizzbuzz
        ├── Heading [h3] - Write up
        │   ├── Markdown [4 lines]
        │   └── Raw Attr Chunk [html, 5 lines]
        └── Heading [h3] - Function
            ├── Chunk [r, 0 options, 42 lines] - fizzbuzz_5
            └── Chunk [r, 1 option, 3 lines] - tests_5

What’s next?

  • The current version will be going up on CRAN imminently

  • More work on helper functions for rmd_select()

  • Implementing pipeable modifier functions

  • Thinking about more high-level user facing functions like rmd_template() and rmd_check_template() - see the vignette for more details

  • Building out and documenting any and all interesting use cases

Reach out