Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions llm-docs/revealjs-format-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
---
main_commit: 98be8a667
analyzed_date: 2026-02-23
key_files:
- src/format/reveal/format-reveal.ts
- src/format/reveal/constants.ts
- src/resources/formats/revealjs/pandoc/template.html
- src/resources/formats/revealjs/pandoc/revealjs.template
---

# Revealjs Format Architecture

How Quarto configures reveal.js presentations, covering metadata handling, template rendering, and the division of responsibilities between TypeScript and Pandoc templates.

## Key Files

| File | Role |
|------|------|
| `src/format/reveal/format-reveal.ts` | Format definition, metadata normalization, extras |
| `src/format/reveal/constants.ts` | Metadata key constants |
| `src/resources/formats/revealjs/pandoc/template.html` | Quarto's active Pandoc template |
| `src/resources/formats/revealjs/pandoc/revealjs.template` | Reference copy of Pandoc's upstream template |

## Two Template Files

Quarto maintains two revealjs template files. They serve different purposes:

**`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.

**`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.).

## Metadata Handling Pattern

Revealjs configuration flows through three stages with distinct responsibilities:

### Stage 1: Normalization — `revealResolveFormat()`

Maps user-facing YAML keys to reveal.js configuration keys. Runs early in format resolution.

Responsibilities:
- Map compound YAML structures to flat metadata (e.g., `scroll-view.snap` → `scrollSnap`)
- Normalize values (e.g., `navigationMode: "vertical"` → `"default"`)
- Create helper flags for template type handling (e.g., `scrollProgressAuto`)
- Remove intermediate metadata keys (e.g., delete `scroll-view` after extracting sub-options)

Does NOT set defaults — only transforms what the user provided.

### Stage 2: Defaults — `extras.metadata` in `formatExtras()`

Sets opinionated default values that can be overridden by user metadata. These are the lowest priority in the metadata chain.

```typescript
// General revealjs defaults
extras.metadata = {
...extras.metadata,
...revealMetadataFilter({
width: 1050,
height: 700,
center: false,
transition: "none",
// ...
}),
};

// Conditional defaults (e.g., scroll-view options when view is "scroll")
if (format.metadata[kView] === "scroll") {
extras.metadata = {
...extras.metadata,
[kScrollSnap]: "mandatory",
[kScrollLayout]: "full",
[kScrollActivationWidth]: 0,
};
}
```

Setting defaults explicitly (rather than relying on Pandoc's `defField`) ensures the template always has values to render.

### Stage 3: Post-processing — `fixupRevealJsInitialization()`

DOM manipulation of the rendered HTML, handling values that can't be fixed in the template:

- Quoting `slideNumber` string values (e.g., `h.v` → `'h.v'`)
- Quoting percentage-based `width`/`height` values
- Injecting `extraConfig` values (options not in the template)
- Registering plugins

Use this stage only when template-level handling isn't possible.

### Metadata Priority (highest to lowest)

1. `metadataOverride` — forces values regardless of user settings
2. `format.metadata` — user values + normalization from `revealResolveFormat()`
3. Pandoc `defField` — Pandoc writer defaults for unset variables
4. `extras.metadata` — Quarto's opinionated defaults

## Template Type Handling

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.

### The Problem

Pandoc template variables have limited type awareness:
- `$var$` renders `BoolVal True` as `true`, `BoolVal False` as `false` (correct for JS booleans)
- `'$var$'` always renders as a quoted string, even for `false` → `'false'` (wrong — truthy in JS)
- Numbers rendered inside quotes become strings: `'$var$'` with `0` → `'0'` (wrong if JS expects a number)

### Pattern: Mixed-Type Options

When an option accepts both strings and booleans (e.g., `scrollSnap: "mandatory" | "proximity" | false`):

```
$if(scrollSnap)$
scrollSnap: '$scrollSnap/nowrap$',
$else$
scrollSnap: false,
$endif$
```

This works because Pandoc's `$if()$` evaluates `BoolVal False` as false, so:
- String values (`"mandatory"`, `"proximity"`) → `$if$` is true → quoted output
- Boolean `false` → `$if$` is false → `$else$` renders unquoted `false`

### Pattern: String "auto" with Boolean Fallback

When an option accepts `"auto" | true | false` (e.g., `scrollProgress`), a helper flag avoids rendering `"auto"` as a boolean:

TypeScript (normalization stage):
```typescript
if (value === "auto" || value === undefined) {
format.metadata[kHelperFlag] = true;
delete format.metadata[kOriginalKey];
}
```

Template:
```
$if(helperFlag)$
option: 'auto',
$elseif(option)$
option: $option$,
$else$
option: false,
$endif$
```

### Pattern: Numeric Options

When reveal.js checks `typeof value === 'number'`, the template must NOT quote the value:

```
scrollActivationWidth: $scrollActivationWidth$,
```

Not `'$scrollActivationWidth$'` — that renders as string `'0'` instead of number `0`.

## Known Pandoc Template Issues

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):

- `scrollSnap: '$scrollSnap$'` renders `false` as string `'false'` (truthy in JS)
- `scrollActivationWidth: '$scrollActivationWidth$'` renders numbers as strings
- `scrollProgress` defField defaults to `true` instead of reveal.js's `'auto'`

## Adding New Reveal.js Options

When adding support for a new reveal.js configuration option:

1. Add the constant to `constants.ts`
2. If the option needs YAML normalization (e.g., a compound structure), add to `revealResolveFormat()`
3. If the option needs a default value, add to `extras.metadata` in `formatExtras()`
4. If the option needs type-safe rendering, add to `template.html` with appropriate `$if/$else$` guards
5. If the option can only be handled post-render, add to `extraConfig` (last resort)
6. Add smoke-all tests covering type edge cases
1 change: 1 addition & 0 deletions news/changelog-1.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ All changes included in 1.9:
### `revealjs`

- ([#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)
- ([#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.

### `ipynb`

Expand Down
1 change: 1 addition & 0 deletions src/format/reveal/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export const kScrollProgress = "scrollProgress";
export const kScrollSnap = "scrollSnap";
export const kScrollActivationWidth = "scrollActivationWidth";
export const kScrollLayout = "scrollLayout";
export const kScrollProgressAuto = "scrollProgressAuto";
48 changes: 31 additions & 17 deletions src/format/reveal/format-reveal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
kScrollActivationWidth,
kScrollLayout,
kScrollProgress,
kScrollProgressAuto,
kScrollSnap,
kScrollView,
kSlideFooter,
Expand Down Expand Up @@ -118,6 +119,21 @@ export function revealResolveFormat(format: Format) {
}
// remove scroll-view from metadata
delete format.metadata[kScrollView];

// Handle scrollProgress "auto" for the template.
// Pandoc templates render BoolVal as true/false literals, but "auto" needs
// to be a quoted string. A helper variable scrollProgressAuto handles this.
// When no scrollProgress is specified and view is "scroll", default to "auto"
// (RevealJS default) rather than Pandoc's defField default of true.
if (format.metadata[kView] === "scroll") {
if (
format.metadata[kScrollProgress] === "auto" ||
format.metadata[kScrollProgress] === undefined
) {
format.metadata[kScrollProgressAuto] = true;
delete format.metadata[kScrollProgress];
}
}
}

export function revealjsFormat() {
Expand Down Expand Up @@ -193,23 +209,9 @@ export function revealjsFormat() {
format.metadata[kPdfMaxPagesPerSlide];
}

// pass scroll view settings as they are not yet in revealjs template
if (format.metadata[kView]) {
extraConfig[kView] = format.metadata[kView];
}
if (format.metadata[kScrollProgress] !== undefined) {
extraConfig[kScrollProgress] = format.metadata[kScrollProgress];
}
if (format.metadata[kScrollSnap] !== undefined) {
extraConfig[kScrollSnap] = format.metadata[kScrollSnap];
}
if (format.metadata[kScrollLayout] !== undefined) {
extraConfig[kScrollLayout] = format.metadata[kScrollLayout];
}
if (format.metadata[kScrollActivationWidth] !== undefined) {
extraConfig[kScrollActivationWidth] =
format.metadata[kScrollActivationWidth];
}
// Scroll view settings (view, scrollProgress, scrollSnap, scrollLayout,
// scrollActivationWidth) are rendered by the template via metadata
// variables set in revealResolveFormat().

// get theme info (including text highlighing mode)
const theme = await revealTheme(
Expand Down Expand Up @@ -359,6 +361,18 @@ export function revealjsFormat() {
};
}

// Scroll-view defaults (only when view is "scroll").
// Set explicitly so the template $if/$else$ type guards always have
// values and don't depend on Pandoc's defField.
if (format.metadata[kView] === "scroll") {
extras.metadata = {
...extras.metadata,
[kScrollSnap]: "mandatory",
[kScrollLayout]: "full",
[kScrollActivationWidth]: 0,
};
}

// hash-type: number (as shorthand for -auto_identifiers)
if (format.metadata[kHashType] === "number") {
extras.pandoc = {
Expand Down
22 changes: 21 additions & 1 deletion src/resources/formats/revealjs/pandoc/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,27 @@
// devices. It is advisable to set this to a lower number than
// viewDistance in order to save resources.
mobileViewDistance: $mobileViewDistance$,
$-- // TODO: Add scroll view option in template: https://github.com/quarto-dev/quarto-cli/issues/13852
$-- Scroll view: differs from Pandoc's revealjs.template to fix JS type issues.
$-- See jgm/pandoc#11486 and llm-docs/revealjs-format-architecture.md
$if(view)$

// Enable scroll view
view: '$view/nowrap$',
$if(scrollProgressAuto)$
scrollProgress: 'auto',
$elseif(scrollProgress)$
scrollProgress: $scrollProgress$,
$else$
scrollProgress: false,
$endif$
scrollActivationWidth: $scrollActivationWidth$,
$if(scrollSnap)$
scrollSnap: '$scrollSnap/nowrap$',
$else$
scrollSnap: false,
$endif$
scrollLayout: '$scrollLayout/nowrap$',
$endif$
$if(parallaxBackgroundImage)$

// Parallax background image
Expand Down
23 changes: 23 additions & 0 deletions tests/docs/smoke-all/revealjs/scroll-view/scroll-view-absent.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: "No scroll view"
format:
revealjs: default
_quarto:
tests:
revealjs:
ensureFileRegexMatches:
- []
-
- 'scrollActivationWidth'
- 'scrollSnap'
- 'scrollLayout'
- 'scrollProgress'
---

## test

Content

## test

Content
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: "Scroll view activation width"
format:
revealjs:
scroll-view:
activate: true
activation-width: 600
_quarto:
tests:
revealjs:
ensureFileRegexMatches:
-
- 'view: ''scroll'','
- 'scrollActivationWidth: 600,'
- []
---

## test

Content

## test

Content
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Jump to slide"
format:
title: "Scroll view config"
format:
revealjs:
scroll-view:
activate: true
Expand All @@ -12,12 +12,12 @@ _quarto:
tests:
revealjs:
ensureFileRegexMatches:
-
- '''view'': "scroll",'
- '''scrollProgress'': true,'
- '''scrollSnap'': false,'
- '''scrollLayout'': "compact",'
- '''scrollActivationWidth'': 0,'
-
- 'view: ''scroll'','
- 'scrollProgress: true,'
- 'scrollSnap: false,'
- 'scrollLayout: ''compact'','
- 'scrollActivationWidth: 0,'
- []
---

Expand Down
25 changes: 25 additions & 0 deletions tests/docs/smoke-all/revealjs/scroll-view/scroll-view-defaults.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: "Scroll view defaults"
format:
revealjs:
scroll-view: true
_quarto:
tests:
revealjs:
ensureFileRegexMatches:
-
- 'view: ''scroll'','
- 'scrollProgress: ''auto'','
- 'scrollSnap: ''mandatory'','
- 'scrollLayout: ''full'','
- 'scrollActivationWidth: 0,'
- []
---

## test

Content

## test

Content
Loading
Loading