|
1 | | -List of modules built by the Palaeoverse team. |
| 1 | +# Palaeoverse Modules |
2 | 2 |
|
3 | | -One folder per module. |
| 3 | +List of teaching modules built by the Palaeoverse team. |
| 4 | + |
| 5 | +One folder per module: `<module>/index.qmd`, plus whatever images and data it |
| 6 | +needs. Every module is rendered twice from that single source: 1) as a page on the |
| 7 | +[Palaeoverse website](https://palaeoverse.org/training/modules) and 2) as a |
| 8 | +reveal.js slide deck (`index-slides.html`) for teaching the same material live. |
| 9 | + |
| 10 | +The two files at the root of this repo exist to make one source serve both |
| 11 | +outputs: |
| 12 | + |
| 13 | +| File | Role | |
| 14 | +| --- | --- | |
| 15 | +| `web_and_slides.lua` | Pandoc/Quarto filter, applied at render time. Decides what appears on the website, what appears on the slides, and how the slides are broken up. | |
| 16 | +| `web_and_slides.r` | One-shot authoring helper. Converts a finished long-form document into the tagged form the filter expects. | |
| 17 | + |
| 18 | +# Authoring a module |
| 19 | + |
| 20 | +## Front matter |
| 21 | + |
| 22 | +Start from an existing module (`ggplot/index.qmd` is a good template). These |
| 23 | +fields matter: |
| 24 | + |
| 25 | +```yaml |
| 26 | +--- |
| 27 | +title: "Plotting in R with ggplot2" |
| 28 | +description: "Visualizing your data with the grammar of graphics" |
| 29 | +author: "Will Gearty" |
| 30 | +date: "2026-06-12" |
| 31 | +categories: [r, tidyverse, dataviz] # various tags |
| 32 | +difficulty: Beginner # Beginner/Intermediate/Advanced |
| 33 | +image: images/2d_density.png # thumbnail for module |
| 34 | +format: |
| 35 | + html: default |
| 36 | + revealjs: |
| 37 | + smaller: true |
| 38 | + output-file: index-slides.html # the deck, alongside the page |
| 39 | +execute: |
| 40 | + output-location: fragment # slide output reveals on click |
| 41 | + echo: true |
| 42 | + freeze: auto # must be auto, not true |
| 43 | +filters: |
| 44 | + - at: pre-ast |
| 45 | + path: web_and_slides_autogenerated.lua |
| 46 | +--- |
| 47 | +``` |
| 48 | + |
| 49 | +`difficulty`, `categories` and `freeze: auto` are enforced by CI (see |
| 50 | +[Technical summary](#technical-summary)). The `filters` entry is what activates |
| 51 | +everything below; `web_and_slides.r` adds it for you, or you can copy it. |
| 52 | + |
| 53 | +## Writing for two outputs |
| 54 | + |
| 55 | +Three fenced-div (`:::`) classes control where content lands: |
| 56 | + |
| 57 | +| Class | Website Tutorial | Slides | |
| 58 | +| --- | --- | --- | |
| 59 | +| `.narration` | normal prose | speaker notes | |
| 60 | +| `.slides-only` | dropped | shown on the slide | |
| 61 | +| `.html-only` | shown | dropped | |
| 62 | + |
| 63 | +Anything not wrapped in one of these appears in both outputs. So the usual shape |
| 64 | +of a module is: headings and code chunks shared by both outputs, the connecting |
| 65 | +prose in `.narration` (a paragraph on the page, a note you talk from on the |
| 66 | +slide), and the occasional `.slides-only` bullet summary or `.html-only` aside. |
| 67 | + |
| 68 | +````markdown |
| 69 | +## Making a scatter plot |
| 70 | + |
| 71 | +::: {.narration} |
| 72 | +On the website this is a paragraph of explanation. On the slides it is what you |
| 73 | +say out loud while the plot is up. |
| 74 | + |
| 75 | +Consecutive paragraphs can share one block. |
| 76 | +::: |
| 77 | + |
| 78 | +::: {.slides-only} |
| 79 | +- x is body mass |
| 80 | +- y is flipper length |
| 81 | +::: |
| 82 | + |
| 83 | +```{r} |
| 84 | +ggplot(penguins) + |
| 85 | + aes(x = body_mass, y = flipper_len) + |
| 86 | + geom_point() |
| 87 | +``` |
| 88 | +```` |
| 89 | + |
| 90 | +## Automated slide rendering via `web_and_slides.lua` |
| 91 | + |
| 92 | +The following changes are applied to the reveal.js slides via our custom Lua filter: |
| 93 | + |
| 94 | +- **Headings become slides:** `##` starts a slide as usual; `###` and deeper are |
| 95 | + promoted so each also gets its own slide, instead of piling onto the parent. |
| 96 | + A `#` heading becomes a centered divider slide, so don't put content under one. |
| 97 | +- **One plot per slide:** The slide closes after each figure. Prose that follows |
| 98 | + a figure moves to the next slide (introducing it), unless nothing but prose |
| 99 | + remains before the next heading — then it stays put rather than making a blank |
| 100 | + slide. Split slides repeat the current heading, so they keep a title. |
| 101 | +- **Callouts get their own slide:** A callout is un-boxed onto a slide of its |
| 102 | + own, titled by its own heading. Give a callout a `## Heading` as its first line |
| 103 | + (rather than `title="..."`) if you want that title on the slide; an untitled |
| 104 | + callout keeps the section title. |
| 105 | +- **Multi-chunk slides build up:** A slide holding two or more code chunks is |
| 106 | + expanded into an auto-animate sequence: one step per chunk, earlier chunks |
| 107 | + staying on screen, and the notes for each chunk advancing with it. |
| 108 | + |
| 109 | +Preview both outputs with: |
| 110 | + |
| 111 | +```sh |
| 112 | +quarto render ggplot/index.qmd # -> index.html and index-slides.html |
| 113 | +``` |
| 114 | + |
| 115 | +## `web_and_slides.r` Helper |
| 116 | + |
| 117 | +The `web_and_slides.r` helper file takes a long-form document and mechanically prepares it: |
| 118 | + |
| 119 | +```sh |
| 120 | +Rscript web_and_slides.r <input.qmd> [output.qmd] |
| 121 | + [--filter=<path/to/filter.lua>] [--no-inject] [--no-settings] |
| 122 | +``` |
| 123 | + |
| 124 | +It does the following: |
| 125 | + |
| 126 | +1. Wraps each set of consecutive prose paragraphs in a single |
| 127 | + `::: {.narration}` block. Headings, code chunks, lists, blockquotes, |
| 128 | + tables, standalone images, and existing fenced divs (including callouts and |
| 129 | + everything inside them) are left untouched. |
| 130 | +2. Registers the filter in the front matter at the `pre-ast` stage, replacing any |
| 131 | + earlier registration. `--no-inject` skips this; `--filter=` names a different |
| 132 | + file. |
| 133 | +3. Sets slide-friendly YAML (skip with `--no-settings`): |
| 134 | + - adds `execute.echo: true` (renders source code on slides) |
| 135 | + - adds `execute.output-location: fragment` (renders code results as separate chunk) |
| 136 | + - adds `format.revealjs.smaller: true` (text shrinks to fit on slides) |
| 137 | + - removes `format.revealjs.scrollable` (disables scrolling through slides) |
| 138 | +4. Reports headings that will render awkwardly (e.g., `#` become centered title slides). |
| 139 | + You should fix these by hand. |
| 140 | + |
| 141 | +The front matter is checked for valid YAML before anything is written. Needs the |
| 142 | +`readr`, `stringr`, `yaml`, and `fs` packages. |
| 143 | + |
| 144 | +With no `output.qmd` specified it rewrites the input in place. |
| 145 | + |
| 146 | +# Two ways to build a module |
| 147 | + |
| 148 | +## 1. Long-form first, then convert |
| 149 | + |
| 150 | +_Best when the module is primarily a written tutorial, or already exists as prose._ |
| 151 | + |
| 152 | +Write the module as an ordinary prose-and-code document, ignoring slides |
| 153 | +entirely. When it reads well, run `web_and_slides.r` over it once, then clean up |
| 154 | +the result by hand: act on any headings it flagged and add `:::{.slides-only}` / |
| 155 | +`:::{.html-only}` blocks where the two outputs should diverge. |
| 156 | + |
| 157 | +```sh |
| 158 | +# write the preliminary tutorial in mymodule/index.qmd.orig |
| 159 | +Rscript web_and_slides.r mymodule/index.qmd.orig mymodule/index.qmd |
| 160 | +quarto render mymodule/index.qmd |
| 161 | +``` |
| 162 | + |
| 163 | +The script is intended to be run as a **single pass once the long-form version is |
| 164 | +finished**. However, in principle it can be run on the same file multiple times. |
| 165 | +Prose already inside a `.narration` block is left alone, so a second run over its |
| 166 | +own output changes nothing. If you start hand-editing the converted file, keep |
| 167 | +editing that file. If you would rather keep iterating on the long-form text, |
| 168 | +keep it alongside as `index.qmd.orig` and regenerate `index.qmd` (but any hand |
| 169 | +cleanup you did to `index.qmd` will need redoing). |
| 170 | + |
| 171 | +## 2. Tag as you go |
| 172 | + |
| 173 | +_Best when you are thinking about the slides and the prose tutorial at the same |
| 174 | +time, and when you want fine control over which output gets what._ |
| 175 | + |
| 176 | +Skip `web_and_slides.r` completely. Write `index.qmd` with the intent to render |
| 177 | +both output types from the beginning. Copy the `filters` entry and the |
| 178 | +slide-friendly `execute` / `revealjs` settings from another module's front matter. |
| 179 | +Then, as you write use an `.html-only` block if you only want the text to |
| 180 | +appear in the prose tutorial, a `.slides-only` block if a section should only |
| 181 | +appear on the slides (e.g., bullet points), or a `.narration` block have |
| 182 | +content appear in the prose tutorial and be shown as speaker notes for the slides. |
| 183 | + |
| 184 | +# Additional references: |
| 185 | + |
| 186 | +- Quarto tutorial ([with Positron](https://quarto.org/docs/get-started/hello/positron.html) | [with RStudio](https://quarto.org/docs/get-started/computations/rstudio.html)) |
| 187 | +- [Markdown basics in Quarto](https://quarto.org/docs/authoring/markdown-basics.html) |
| 188 | +- [HTML basics in Quarto](https://quarto.org/docs/output-formats/html-basics.html) |
| 189 | +- [Revealjs in Quarto](https://quarto.org/docs/presentations/revealjs/) |
| 190 | +- [Quarto Listings](https://quarto.org/docs/websites/website-listings.html) |
0 commit comments