Skip to content

Commit 4c60a3e

Browse files
cdervclaude
andauthored
Move revealjs scroll-view from extraConfig to template (#14100)
Scroll-view options are now rendered by Quarto's Pandoc template instead of injected at runtime via fixupRevealJsInitialization(). - Add scrollProgressAuto helper flag for "auto" string handling (Pandoc templates can't distinguish "auto" from boolean true) - Set scroll-view defaults in extras.metadata instead of relying on Pandoc's defField - Render scroll-view block in template.html with $if/$else$ type guards for correct JS types (boolean, string, number) - Add 10 smoke tests covering all scroll-view option combinations - Add llm-docs architecture reference for revealjs format Custom revealjs templates may need updating to include the scroll-view configuration block. Closes #13852 --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent d1c3de0 commit 4c60a3e

15 files changed

Lines changed: 452 additions & 26 deletions
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
---
2+
main_commit: 98be8a667
3+
analyzed_date: 2026-02-23
4+
key_files:
5+
- src/format/reveal/format-reveal.ts
6+
- src/format/reveal/constants.ts
7+
- src/resources/formats/revealjs/pandoc/template.html
8+
- src/resources/formats/revealjs/pandoc/revealjs.template
9+
---
10+
11+
# Revealjs Format Architecture
12+
13+
How Quarto configures reveal.js presentations, covering metadata handling, template rendering, and the division of responsibilities between TypeScript and Pandoc templates.
14+
15+
## Key Files
16+
17+
| File | Role |
18+
|------|------|
19+
| `src/format/reveal/format-reveal.ts` | Format definition, metadata normalization, extras |
20+
| `src/format/reveal/constants.ts` | Metadata key constants |
21+
| `src/resources/formats/revealjs/pandoc/template.html` | Quarto's active Pandoc template |
22+
| `src/resources/formats/revealjs/pandoc/revealjs.template` | Reference copy of Pandoc's upstream template |
23+
24+
## Two Template Files
25+
26+
Quarto maintains two revealjs template files. They serve different purposes:
27+
28+
**`revealjs.template`** is a direct copy of Pandoc's `default.revealjs`, auto-generated by `package/src/common/update-pandoc.ts` during Pandoc version updates. Do not modify this file — changes will be overwritten on the next Pandoc update.
29+
30+
**`template.html`** is Quarto's active template, specified in `formatExtras()` via `templateContext`. It diverges from the upstream template where Quarto needs different behavior (type-safe rendering, additional features, etc.).
31+
32+
## Metadata Handling Pattern
33+
34+
Revealjs configuration flows through three stages with distinct responsibilities:
35+
36+
### Stage 1: Normalization — `revealResolveFormat()`
37+
38+
Maps user-facing YAML keys to reveal.js configuration keys. Runs early in format resolution.
39+
40+
Responsibilities:
41+
- Map compound YAML structures to flat metadata (e.g., `scroll-view.snap``scrollSnap`)
42+
- Normalize values (e.g., `navigationMode: "vertical"``"default"`)
43+
- Create helper flags for template type handling (e.g., `scrollProgressAuto`)
44+
- Remove intermediate metadata keys (e.g., delete `scroll-view` after extracting sub-options)
45+
46+
Does NOT set defaults — only transforms what the user provided.
47+
48+
### Stage 2: Defaults — `extras.metadata` in `formatExtras()`
49+
50+
Sets opinionated default values that can be overridden by user metadata. These are the lowest priority in the metadata chain.
51+
52+
```typescript
53+
// General revealjs defaults
54+
extras.metadata = {
55+
...extras.metadata,
56+
...revealMetadataFilter({
57+
width: 1050,
58+
height: 700,
59+
center: false,
60+
transition: "none",
61+
// ...
62+
}),
63+
};
64+
65+
// Conditional defaults (e.g., scroll-view options when view is "scroll")
66+
if (format.metadata[kView] === "scroll") {
67+
extras.metadata = {
68+
...extras.metadata,
69+
[kScrollSnap]: "mandatory",
70+
[kScrollLayout]: "full",
71+
[kScrollActivationWidth]: 0,
72+
};
73+
}
74+
```
75+
76+
Setting defaults explicitly (rather than relying on Pandoc's `defField`) ensures the template always has values to render.
77+
78+
### Stage 3: Post-processing — `fixupRevealJsInitialization()`
79+
80+
DOM manipulation of the rendered HTML, handling values that can't be fixed in the template:
81+
82+
- Quoting `slideNumber` string values (e.g., `h.v``'h.v'`)
83+
- Quoting percentage-based `width`/`height` values
84+
- Injecting `extraConfig` values (options not in the template)
85+
- Registering plugins
86+
87+
Use this stage only when template-level handling isn't possible.
88+
89+
### Metadata Priority (highest to lowest)
90+
91+
1. `metadataOverride` — forces values regardless of user settings
92+
2. `format.metadata` — user values + normalization from `revealResolveFormat()`
93+
3. Pandoc `defField` — Pandoc writer defaults for unset variables
94+
4. `extras.metadata` — Quarto's opinionated defaults
95+
96+
## Template Type Handling
97+
98+
Pandoc templates render values as text. This creates type mismatches when reveal.js expects specific JavaScript types. Quarto's `template.html` uses conditional guards to render correct JS types.
99+
100+
### The Problem
101+
102+
Pandoc template variables have limited type awareness:
103+
- `$var$` renders `BoolVal True` as `true`, `BoolVal False` as `false` (correct for JS booleans)
104+
- `'$var$'` always renders as a quoted string, even for `false``'false'` (wrong — truthy in JS)
105+
- Numbers rendered inside quotes become strings: `'$var$'` with `0``'0'` (wrong if JS expects a number)
106+
107+
### Pattern: Mixed-Type Options
108+
109+
When an option accepts both strings and booleans (e.g., `scrollSnap: "mandatory" | "proximity" | false`):
110+
111+
```
112+
$if(scrollSnap)$
113+
scrollSnap: '$scrollSnap/nowrap$',
114+
$else$
115+
scrollSnap: false,
116+
$endif$
117+
```
118+
119+
This works because Pandoc's `$if()$` evaluates `BoolVal False` as false, so:
120+
- String values (`"mandatory"`, `"proximity"`) → `$if$` is true → quoted output
121+
- Boolean `false``$if$` is false → `$else$` renders unquoted `false`
122+
123+
### Pattern: String "auto" with Boolean Fallback
124+
125+
When an option accepts `"auto" | true | false` (e.g., `scrollProgress`), a helper flag avoids rendering `"auto"` as a boolean:
126+
127+
TypeScript (normalization stage):
128+
```typescript
129+
if (value === "auto" || value === undefined) {
130+
format.metadata[kHelperFlag] = true;
131+
delete format.metadata[kOriginalKey];
132+
}
133+
```
134+
135+
Template:
136+
```
137+
$if(helperFlag)$
138+
option: 'auto',
139+
$elseif(option)$
140+
option: $option$,
141+
$else$
142+
option: false,
143+
$endif$
144+
```
145+
146+
### Pattern: Numeric Options
147+
148+
When reveal.js checks `typeof value === 'number'`, the template must NOT quote the value:
149+
150+
```
151+
scrollActivationWidth: $scrollActivationWidth$,
152+
```
153+
154+
Not `'$scrollActivationWidth$'` — that renders as string `'0'` instead of number `0`.
155+
156+
## Known Pandoc Template Issues
157+
158+
Pandoc's upstream `revealjs.template` (copied to `revealjs.template`) has type issues in the scroll-view block that Quarto's `template.html` fixes. Tracked in [jgm/pandoc#11486](https://github.com/jgm/pandoc/issues/11486):
159+
160+
- `scrollSnap: '$scrollSnap$'` renders `false` as string `'false'` (truthy in JS)
161+
- `scrollActivationWidth: '$scrollActivationWidth$'` renders numbers as strings
162+
- `scrollProgress` defField defaults to `true` instead of reveal.js's `'auto'`
163+
164+
## Adding New Reveal.js Options
165+
166+
When adding support for a new reveal.js configuration option:
167+
168+
1. Add the constant to `constants.ts`
169+
2. If the option needs YAML normalization (e.g., a compound structure), add to `revealResolveFormat()`
170+
3. If the option needs a default value, add to `extras.metadata` in `formatExtras()`
171+
4. If the option needs type-safe rendering, add to `template.html` with appropriate `$if/$else$` guards
172+
5. If the option can only be handled post-render, add to `extraConfig` (last resort)
173+
6. Add smoke-all tests covering type edge cases

news/changelog-1.9.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ All changes included in 1.9:
102102
### `revealjs`
103103

104104
- ([#13722](https://github.com/quarto-dev/quarto-cli/issues/13722)): Fix `light-content` / `dark-content` SCSS rules not included in Reveal.js format. (author: @mcanouil)
105+
- ([#13852](https://github.com/quarto-dev/quarto-cli/issues/13852)): Scroll-view options (`scrollSnap`, `scrollProgress`, `scrollActivationWidth`, `scrollLayout`) are now rendered in the Pandoc template instead of injected at runtime. Custom revealjs templates may need updating to include the scroll-view configuration block.
105106

106107
### `ipynb`
107108

src/format/reveal/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ export const kScrollProgress = "scrollProgress";
3131
export const kScrollSnap = "scrollSnap";
3232
export const kScrollActivationWidth = "scrollActivationWidth";
3333
export const kScrollLayout = "scrollLayout";
34+
export const kScrollProgressAuto = "scrollProgressAuto";

src/format/reveal/format-reveal.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
kScrollActivationWidth,
6969
kScrollLayout,
7070
kScrollProgress,
71+
kScrollProgressAuto,
7172
kScrollSnap,
7273
kScrollView,
7374
kSlideFooter,
@@ -118,6 +119,21 @@ export function revealResolveFormat(format: Format) {
118119
}
119120
// remove scroll-view from metadata
120121
delete format.metadata[kScrollView];
122+
123+
// Handle scrollProgress "auto" for the template.
124+
// Pandoc templates render BoolVal as true/false literals, but "auto" needs
125+
// to be a quoted string. A helper variable scrollProgressAuto handles this.
126+
// When no scrollProgress is specified and view is "scroll", default to "auto"
127+
// (RevealJS default) rather than Pandoc's defField default of true.
128+
if (format.metadata[kView] === "scroll") {
129+
if (
130+
format.metadata[kScrollProgress] === "auto" ||
131+
format.metadata[kScrollProgress] === undefined
132+
) {
133+
format.metadata[kScrollProgressAuto] = true;
134+
delete format.metadata[kScrollProgress];
135+
}
136+
}
121137
}
122138

123139
export function revealjsFormat() {
@@ -193,23 +209,9 @@ export function revealjsFormat() {
193209
format.metadata[kPdfMaxPagesPerSlide];
194210
}
195211

196-
// pass scroll view settings as they are not yet in revealjs template
197-
if (format.metadata[kView]) {
198-
extraConfig[kView] = format.metadata[kView];
199-
}
200-
if (format.metadata[kScrollProgress] !== undefined) {
201-
extraConfig[kScrollProgress] = format.metadata[kScrollProgress];
202-
}
203-
if (format.metadata[kScrollSnap] !== undefined) {
204-
extraConfig[kScrollSnap] = format.metadata[kScrollSnap];
205-
}
206-
if (format.metadata[kScrollLayout] !== undefined) {
207-
extraConfig[kScrollLayout] = format.metadata[kScrollLayout];
208-
}
209-
if (format.metadata[kScrollActivationWidth] !== undefined) {
210-
extraConfig[kScrollActivationWidth] =
211-
format.metadata[kScrollActivationWidth];
212-
}
212+
// Scroll view settings (view, scrollProgress, scrollSnap, scrollLayout,
213+
// scrollActivationWidth) are rendered by the template via metadata
214+
// variables set in revealResolveFormat().
213215

214216
// get theme info (including text highlighing mode)
215217
const theme = await revealTheme(
@@ -359,6 +361,18 @@ export function revealjsFormat() {
359361
};
360362
}
361363

364+
// Scroll-view defaults (only when view is "scroll").
365+
// Set explicitly so the template $if/$else$ type guards always have
366+
// values and don't depend on Pandoc's defField.
367+
if (format.metadata[kView] === "scroll") {
368+
extras.metadata = {
369+
...extras.metadata,
370+
[kScrollSnap]: "mandatory",
371+
[kScrollLayout]: "full",
372+
[kScrollActivationWidth]: 0,
373+
};
374+
}
375+
362376
// hash-type: number (as shorthand for -auto_identifiers)
363377
if (format.metadata[kHashType] === "number") {
364378
extras.pandoc = {

src/resources/formats/revealjs/pandoc/template.html

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,27 @@
217217
// devices. It is advisable to set this to a lower number than
218218
// viewDistance in order to save resources.
219219
mobileViewDistance: $mobileViewDistance$,
220-
$-- // TODO: Add scroll view option in template: https://github.com/quarto-dev/quarto-cli/issues/13852
220+
$-- Scroll view: differs from Pandoc's revealjs.template to fix JS type issues.
221+
$-- See jgm/pandoc#11486 and llm-docs/revealjs-format-architecture.md
222+
$if(view)$
223+
224+
// Enable scroll view
225+
view: '$view/nowrap$',
226+
$if(scrollProgressAuto)$
227+
scrollProgress: 'auto',
228+
$elseif(scrollProgress)$
229+
scrollProgress: $scrollProgress$,
230+
$else$
231+
scrollProgress: false,
232+
$endif$
233+
scrollActivationWidth: $scrollActivationWidth$,
234+
$if(scrollSnap)$
235+
scrollSnap: '$scrollSnap/nowrap$',
236+
$else$
237+
scrollSnap: false,
238+
$endif$
239+
scrollLayout: '$scrollLayout/nowrap$',
240+
$endif$
221241
$if(parallaxBackgroundImage)$
222242

223243
// Parallax background image
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
title: "No scroll view"
3+
format:
4+
revealjs: default
5+
_quarto:
6+
tests:
7+
revealjs:
8+
ensureFileRegexMatches:
9+
- []
10+
-
11+
- 'scrollActivationWidth'
12+
- 'scrollSnap'
13+
- 'scrollLayout'
14+
- 'scrollProgress'
15+
---
16+
17+
## test
18+
19+
Content
20+
21+
## test
22+
23+
Content
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
title: "Scroll view activation width"
3+
format:
4+
revealjs:
5+
scroll-view:
6+
activate: true
7+
activation-width: 600
8+
_quarto:
9+
tests:
10+
revealjs:
11+
ensureFileRegexMatches:
12+
-
13+
- 'view: ''scroll'','
14+
- 'scrollActivationWidth: 600,'
15+
- []
16+
---
17+
18+
## test
19+
20+
Content
21+
22+
## test
23+
24+
Content

tests/docs/smoke-all/revealjs/scroll-view-config.qmd renamed to tests/docs/smoke-all/revealjs/scroll-view/scroll-view-config.qmd

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
title: "Jump to slide"
3-
format:
2+
title: "Scroll view config"
3+
format:
44
revealjs:
55
scroll-view:
66
activate: true
@@ -12,12 +12,12 @@ _quarto:
1212
tests:
1313
revealjs:
1414
ensureFileRegexMatches:
15-
-
16-
- '''view'': "scroll",'
17-
- '''scrollProgress'': true,'
18-
- '''scrollSnap'': false,'
19-
- '''scrollLayout'': "compact",'
20-
- '''scrollActivationWidth'': 0,'
15+
-
16+
- 'view: ''scroll'','
17+
- 'scrollProgress: true,'
18+
- 'scrollSnap: false,'
19+
- 'scrollLayout: ''compact'','
20+
- 'scrollActivationWidth: 0,'
2121
- []
2222
---
2323

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
title: "Scroll view defaults"
3+
format:
4+
revealjs:
5+
scroll-view: true
6+
_quarto:
7+
tests:
8+
revealjs:
9+
ensureFileRegexMatches:
10+
-
11+
- 'view: ''scroll'','
12+
- 'scrollProgress: ''auto'','
13+
- 'scrollSnap: ''mandatory'','
14+
- 'scrollLayout: ''full'','
15+
- 'scrollActivationWidth: 0,'
16+
- []
17+
---
18+
19+
## test
20+
21+
Content
22+
23+
## test
24+
25+
Content

0 commit comments

Comments
 (0)