Skip to content

Prototype single files for web and slides - #7

Draft
willgearty wants to merge 5 commits into
mainfrom
single-file-prototype
Draft

Prototype single files for web and slides#7
willgearty wants to merge 5 commits into
mainfrom
single-file-prototype

Conversation

@willgearty

@willgearty willgearty commented Jul 14, 2026

Copy link
Copy Markdown
Member

This introduces some infrastructure to make a single longform module quarto file render well for both a webpage (html) and for slides (revealjs). At the moment it uses no external extensions.

There are two components:

  1. web_and_slides.R: this script can be used to convert a standard longform quarto file to one that supports webpage/slides rendering
    • wraps each paragraph in ::: {.narration}, which is treated as notes for revealjs and as normal prose for html
    • adds the web_and_slides.lua filter to the yaml front matter if it isn't there (see # 2 below)
    • adds execute: output-location: fragment and execute: echo: true to the yaml front matter; this ensures that code chunk output is included in the slides and is added as fragments (separate slides) by default
    • adds revealjs.smaller = true to the yaml front matter: this ensures that figures shrink to fit on slides
    • throws a report of any headings that will end up as awkward in the slides (e.g., a # header with text after it)
    • the output can be the same as the input or they can be different (in case you want to keep the original, like in this PR as an example)
    • lives at the root of the repo
  2. web_and_slides.lua: contains various filters and processing steps that alter the way the quarto file is rendered to slides
    • handles the treatment of .narration (notes for slides, prose for webpage)
    • headings below the slide level (default is ##) are promoted so each becomes its own slide
    • back-to-back code chunks are brought in one-by-one on separate slides (like the fragment functionality)
    • back-to-back plots are split to separate slides (the prose/notes before each plot are kept with the plot)
    • callouts are converted to separate slides
    • ideally lives within each module folder (could also theoretically live at the root, but would need to be copied for each archived folder for reproducibility)

So the general workflow would look something like this:

  1. User makes original longform quarto document (as if they were making a teaching webpage)
  2. User runs the quarto document through web_and_slides.R
  3. User locally previews the rendering for both webpage and slides
  4. User removes .narration wherever text should be shown on slides verbatim
  5. User adds additional speaker notes as needed (these are never shown on the webpage format)
  6. User further adjusts the content with conditional content as needed
  7. User makes small finishing touches for either format (e.g., https://emilhvitfeldt.github.io/quarto-revealjs-editable/)

Rendered webpage:
https://palaeoverse.org/modules/pr-preview/pr-7/training/modules/ggplot/index.html

Rendered slides:
https://palaeoverse.org/modules/pr-preview/pr-7/training/modules/ggplot/index-slides.html

TODO:

  • demonstrate removing some .narration usage
  • demonstrate extra speaker notes
  • demonstrate conditional content
  • demonstrate slide formatting
  • fix reported headers

Note that the script and .lua filter were generated iteratively over the course of a substantial conversation with Claude. The original module file was made entirely by me, and I've reviewed all of the code along the way.

@github-actions

Copy link
Copy Markdown

@etiennebacher

etiennebacher commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary: I adapted the Lua filter and tried it on the draft module on "browser() and reprex()", and it seems to work fine. My main concern is that it makes index.qmd quite harder to read (I expand on this and provide an example below). Also, I think there are things to improve in the workflow to make this both easier to use and more flexible.

I'm curious to see how this will work in practice in the long run, but it's true there's a risk of drift between the written content and the slides, so it is nice to have everything in a single file. I thought theming and options would be an issue but it's easy to have format-specific options in the YAML.

What I'd do:

  • add {.slides} to have text that should be on slides only
  • rename {.narration} to {.long-format} so that the distinction with {.slides} is clearer

Workflow complexity

The workflow seems pretty complicated to me. In particular, I don't understand the purpose of the R script: is this here only to convert qmd files that weren't made with this workflow and therefore need the {.narration} divs (i.e. currently your ggplot2 module and my couple of draft PRs), or is it supposed to be run regularly? Maybe my misunderstanding comes from the fact that I tried this workflow by writing the long-form content and the slides in parallel, instead of doing the long-form content first and then adapt it to the slides format.

It just seems to me that steps 2-7 in the workflow you detailed could be replaced by:

Wrap the content you're writing in either ::: {.narration} or ::: {.slides} depending on whether it should end up in the written module or in slides. If the content should appear in both, do not wrap it in :::

This would also require a single index.qmd (I was a bit confused on whether I needed an index.orig.qmd and an index.qmd).

Either way, this increases the complexity for people who are not familiar with Quarto, which I think is unavoidable but we should ensure that we have screenshots / screen captures to clearly show how this is supposed to be used.

Readability

I usually include custom text in slides (bullet points most of the time). I (locally) adapted the Lua filter to have a {.slides} tag for text that should appear in slides only. I adapted the first couple of sections of the module on "browser() and reprex()" to use this syntax and the file is harder to read but still okay to me, what do you think?

Example of revamped `index.qmd` in module on browser() and reprex()
---
title: "Debugging with reprex() and browser()"
description: "How to efficiently solve bugs."
author: "Etienne Bacher"
date: "2026-06-25"
categories: [r]
difficulty: Intermediate
image: R_logo.png
format:
  html: 
    toc: true
  revealjs:
    smaller: true
    output-file: index-slides.html
    theme: [serif, custom.scss]
    pdf-separate-fragments: true
    strip-comments: true
    auto-animate-duration: 0.8
    code-copy: true
    slide-number: true
    toc: false
    incremental: true
execute:
  output-location: fragment
  echo: true
  warning: false
  message: false
  freeze: auto
editor: 
  markdown: 
    wrap: 72
filters:
  - at: pre-ast
    path: ../web_and_slides.lua
---
[ TODO: why is it useful to know how to debug in the age of AI? ]


::: {.slides}
---
:::


::: {.narration}
The following code is taken from the ["Latitudinal trends in Phanerozoic reefs" vignette](https://palaeoverse.palaeoverse.org/articles/phanerozoic-reefs.html), but let's pretend we have written this code from scratch:
:::

::: {.slides}
<br>
Code from ["Latitudinal trends in Phanerozoic reefs" vignette](https://palaeoverse.palaeoverse.org/articles/phanerozoic-reefs.html):

<br>
:::


```{r, error = TRUE}
library(palaeoverse)

data(reefs)
reef_counts <- group_apply(occdf = reefs, group = "interval", fun = nrow)
# Assign a common time scale based on an interval key
reefs <- look_up(occdf = reefs,
                 early_interval = "interval",
                 late_interval = "interval",
                 int_key = interval_key)
reefs <- subset(reefs, interval_max_ma <= 541)
# Extract Phanerozoic stage-level stages for time bins
bins <- time_bins(interval = "Phanerozoic", rank = "stage")

bin_time(occdf = reefs, bins = bins, method = "mid")
```

::: {.narration}
We now have an error, but its cause is not immediately clear.
Therefore, we'll have to do some debugging.
:::

::: {.slides}
. . .

<br>
Hmm, not a very helpful error message...
:::

# Reducing the problem

::: {.narration}
The longer the code you try to debug, the more potential causes you have to explore. The first thing to do when you can't find the bug in your code right away is to build a minimal **repr**oducible **ex**ample, aka a *reprex* (this is also known as a MRE, for "Minimal Reproducible Example").

In our example above, we can reduce the problem in two ways: we can reduce the data first, and then reduce the code.
:::

::: {.slides}
## Reducing the problem

<br><br><br>

- **Longer code means more potential causes to explore.** 

<br>

- First thing to do: build a minimal **repr**oducible **ex**ample (aka a *reprex*, aka a MRE, for "Minimal Reproducible Example")

<br>

- We can reduce the problem in two ways: 
  
  - reduce the data first
  - reduce the code
:::


## Reduce the size of the data

::: {.narration}

In the debugging process, we will likely run the code at least a handful of times. 
Therefore, we want to make sure that it runs as fast as possible so that we don't lose our focus.
The easiest way to do this is to reduce the size of our input data.

In addition to making the error appear faster, it is also easier to compare the actual and expected outputs when the data is as small as possible.

In this example, the dataset we are using has thousands of rows and 14 columns:
:::

::: {.slides}
<br><br>

::: {.nonincremental}

* When we debug, we run the code at least a few times.

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{< fa arrow-right >}} &nbsp; we want to make sure the code runs as fast as possible

<br>

* Easiest way to do that: reduce the size of our input data.

<br>

* Here, our data is pretty large:

<br>
:::
:::

```{r}
dim(reefs)
```

::: {.narration}
Let's try to reduce the size of our data:
:::

::: {.slides}
## Reduce the size of the data

<br><br>

Let's try to reduce the size of our data:
:::
```{r, error = TRUE}
reefs2 <- head(reefs)

reef_counts <- group_apply(occdf = reefs2, group = "interval", fun = nrow)
# Assign a common time scale based on an interval key
reefs2 <- look_up(occdf = reefs2,
                 early_interval = "interval",
                 late_interval = "interval",
                 int_key = interval_key)
reefs2 <- subset(reefs2, interval_max_ma <= 541)
# Extract Phanerozoic stage-level stages for time bins
bins <- time_bins(interval = "Phanerozoic", rank = "stage")

bin_time(occdf = reefs2, bins = bins, method = "mid")
```

::: {.narration}
We still see the same error but now we only have 6 rows left, much better!
Let's try to reduce that to one row and to remove some columns.
:::

::: {.slides}
<br>
{{< fa arrow-right >}} &nbsp; Same error but with 6 rows only!
:::


::: {.slides}
## Reduce the size of the data

<br>
Let's try to reduce that to one row and fewer columns:
:::

```{r, error = TRUE}
reefs2 <- head(reefs, n = 1)
reefs2 <- reefs2[, 1:5]

reef_counts <- group_apply(occdf = reefs2, group = "interval", fun = nrow)
```

::: {.narration}
Hmm, we don't have the same error anymore, so we removed too many columns.
Let's try to keep 6 instead of 5:
:::

::: {.slides}
<br>
{{< fa arrow-right >}} &nbsp; Hmm, we removed too many columns... let's try to keep 6 instead of 5.

## Reduce the size of the data
:::

```{r, error = TRUE}
reefs2 <- head(reefs, n = 1)
reefs2 <- reefs2[, 1:6]

reef_counts <- group_apply(occdf = reefs2, group = "interval", fun = nrow)
# Assign a common time scale based on an interval key
reefs2 <- look_up(occdf = reefs2,
                 early_interval = "interval",
                 late_interval = "interval",
                 int_key = interval_key)
reefs2 <- subset(reefs2, interval_max_ma <= 541)
# Extract Phanerozoic stage-level stages for time bins
bins <- time_bins(interval = "Phanerozoic", rank = "stage")

bin_time(occdf = reefs2, bins = bins, method = "mid")
```

::: {.narration}
Bingo! We have the same error as before, but now we only have one row and six columns.

Let's now try to reduce the size of the code.
:::


::: {.slides}
{{< fa arrow-right >}} &nbsp; Nice! Same error as before, but with only one row and six columns.

Let's now try to reduce the size of the code.
:::

Small papercut to fix in the current implementation IMO: contiguous {.narration} blocks should be concatenated. Currently, if we have two paragraphs in markdown then they both get wrapped in a separate {.narration} block, which increases clutter. For instance:

hello there
   
hello again

becomes

::: {.narration}
hello there
:::
   
::: {.narration}
hello again
:::

It would be nice to have this instead:

::: {.narration}
hello there
   
hello again
:::

Other

Not an actionable comment, but I just wanted to point out that Quarto v2 will be welcome here to give good error messages for syntax errors, e.g. when some fenced divs are not closed (currently it warns only).

Re:

web_and_slides.lua ideally lives within each module folder (could also theoretically live at the root, but would need to be copied for each archived folder for reproducibility)

We could have this:

filters:
  - at: pre-ast
    path: web_and_slides_autogenerated.lua

and have a script that copies the Lua script from ../web_and_slides.lua (root) to <module_name>/web_and_slides_autogenerated.lua at the very start of quarto render. This way the module is self-contained but we have only one Lua script to maintain?

@willgearty

willgearty commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Just some clarifying responses:

Workflow complexity

The workflow seems pretty complicated to me. In particular, I don't understand the purpose of the R script: is this here only to convert qmd files that weren't made with this workflow and therefore need the {.narration} divs (i.e. currently your ggplot2 module and my couple of draft PRs), or is it supposed to be run regularly?

Just once per module (perhaps this is made confusing by the existence of the original file, which is only here for demonstration purposes). Once it's run on someone's original file, the script should ideally never be used again and the user should just edit the new (or now edited) file.

Maybe my misunderstanding comes from the fact that I tried this workflow by writing the long-form content and the slides in parallel, instead of doing the long-form content first and then adapt it to the slides format.

It just seems to me that steps 2-7 in the workflow you detailed could be replaced by:

Wrap the content you're writing in either ::: {.narration} or ::: {.slides} depending on whether it should end up in the written module or in slides. If the content should appear in both, do not wrap it in :::

Ah, yes, based on Lewis's prompting, I designed the script for people who made ONLY the long-form content first. If you make both in parallel, doing it as you describe is likely easier.

This would also require a single index.qmd (I was a bit confused on whether I needed an index.orig.qmd and an index.qmd).

See above

Either way, this increases the complexity for people who are not familiar with Quarto, which I think is unavoidable but we should ensure that we have screenshots / screen captures to clearly show how this is supposed to be used.

Agreed. My thought is that we could have a module that walks people through this process (or just a document).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants