Skip to content

reveal.js: link shared assets (render) + preview/render parity; embedded-example iframes#271

Merged
cscheid merged 25 commits into
mainfrom
feature/revealjs-render-preview-convergence
Jun 10, 2026
Merged

reveal.js: link shared assets (render) + preview/render parity; embedded-example iframes#271
cscheid merged 25 commits into
mainfrom
feature/revealjs-render-preview-convergence

Conversation

@cscheid

@cscheid cscheid commented Jun 10, 2026

Copy link
Copy Markdown
Member

Bundles the reveal.js + embedded-example work accumulated off main (the
branch was rebuilt/merged onto current origin/main). Opening to run CI on
the combination.

Highlights

reveal.js linked shared assets — render side (bd-jij5gge2)

q2 render no longer inlines ~700 KB of reveal CSS/JS into every slides.html.
It registers the vendored resources/revealjs/* as Project-scoped artifacts
and links them, so a website with N decks shares one copy under
site_libs/revealjs/ — matching how format: html handles bootstrap/themes.
The "q2 single binary" property is unchanged (assets are still
include_str!-embedded; only the render output changed inline→linked).

q2 preview reveal converged with render (bd-ibqkf9ry)

The React preview path (RevealDeck.tsx + @revealjs/react) stays — it's
required for the collaborative-editing AST↔source mapping — but the drift
is removed:

  • One version, enforced. Pin reveal.js6.0.0 + @revealjs/react0.2.0
    exact, plus a test that fails if the vendored resources/revealjs/ ever drifts
    from the npm reveal.js/dist.
  • One CSS source. RevealDeck.tsx imports the vendored resources/revealjs/*
    (incl. the previously-missing reset.css), not the npm copy.
  • Same CSS environment. The q2-preview iframe no longer injects the
    HTML-theme/Bootstrap CSS into reveal decks. That single leak caused both
    visible bugs: a grey h2 { border-bottom } line under slide titles, and
    dead transitions (Bootstrap reboot's prefers-reduced-motion rule forced
    transition-duration: .01ms !important). Browser-verified parity with render.

embedded-example iframes + cross-referenceable Demos (bd-z1smhvuo, bd-t3cert81)

::: {.embed-example-iframe file="…"} → a live <iframe> to a staged example
deck; #demo-foo → auto-numbered, cross-referenceable "Demo N" via the normal
crossref machinery. (The preview inline-embed experiment was reverted; the
deck must be served, tracked under bd-kjrpya2d.)

Verification

Full cargo xtask verify (workspace build + tests + WASM + hub-client) was
green on the pre-merge branch tip; this push merges current origin/main and
regenerates package-lock.json (the only merge conflict). CI re-runs the full
suite on the combination.

Follow-ups (filed)

  • bd-kjrpya2d — serve embedded decks in preview (the inline approach was reverted).
  • bd-4b7f1hr7 — audit whether the HTML preview has the same CSS-codepath drift.
  • bd-teh4hbli — decouple sync-upload from resources: (publish ≠ upload trust boundary).

🤖 Generated with Claude Code

cscheid and others added 25 commits June 9, 2026 13:51
…ame (bd-z1smhvuo)

Phase 1 of the example-iframe embed feature. A built-in AST transform
rewrites `Div.embed-example-iframe[file=…]` placeholders into a live
`<iframe class="embed-example-iframe" src=…>` plus the author's source
link as a caption. Runs in the shared normalization pipeline, so render
and q2-preview, HTML and revealjs all get it.

Static-asset-only contract: `file=` must name a pre-rendered static
asset. A `.qmd` (or other source doc) is rejected with a diagnostic and
the placeholder degrades to just its fallback link — keeping the page
useful and dodging iframe self-recursion.

Structured error codes (project subsystem):
- Q-5-4 Example Embed Missing `file=`
- Q-5-5 Example Embed Target Is Not a Static Asset
Catalog entries, docs/errors/project pages + sidebar, and a
catalog-registration test included.

Verified end-to-end through `q2 render` (HTML and --to revealjs).
Workspace build + quarto-core + quarto-error-reporting suites green
(2314 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onvention findings) for bd-z1smhvuo

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e docs (bd-z1smhvuo)

Phase 2 of the example-iframe embed feature.

Staging (xtask + manifest):
- examples/manifest.yml: explicit allow-list of example projects to stage
  (no globs, so a stray dir is never rendered/published by accident).
- cargo xtask stage-doc-examples: renders each manifest project with q2 and
  copies its static output (*.html + *_files/) into docs/examples/<entry>/.
  Generated + gitignored (docs/.gitignore), regenerated, never committed.
- docs/_quarto.yml: project.resources: [examples] copies the staged tree into
  _site/examples/ so the iframes resolve on render.

Page-relative iframe src (resolve_static_resource_href):
- New helper in navigation_href.rs, sibling of resolve_doc_relative_href but
  for static assets: no ProjectIndex lookup, no .qmd diagnostic — normalize +
  page_url_for. ExampleEmbedTransform threads the page source + resolver and
  routes file= through it, so the emitted src is page-relative
  (../../examples/.../slides.html for a depth-2 page), NOT a host-absolute
  /examples/... that breaks under a deploy subpath. Mirrors how every other
  Quarto link is rewritten.

Docs migration: the 8 placeholders in docs/presentations/revealjs/index.qmd
now use .embed-example-iframe file="/examples/presentations/<name>/slides.html".

Verified end-to-end through `q2 render docs/` and a real browser: all 8 iframes
load real reveal decks via the ../../examples/... relative src; staged decks
land under _site/examples/. cargo xtask verify --skip-hub-build green
(2327 tests, fmt, -D warnings).

Preview-mode (q2 preview) verification is pending a WASM rebuild and tracked in
the plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-t3cert81)

Make `.embed-example-iframe` a first-class cross-referenceable Quarto type.
When the div carries a `#demo-…` id, the example is auto-numbered "Demo N"
and prose can `@demo-…`-reference it — resolved through the same crossref
index/resolve/render machinery as figures and theorems.

`demo`/"Demo" is a new built-in ref-type, deliberately distinct from the
theorem-like `exm`/"Example" (which `@exm-` would collide with).

Architecture (sugar → CustomNode → render, mirroring callouts/floats):
- ExampleEmbedTransform is now a SUGAR step (runs before the theorem/float
  sugar so a `#demo-…` div isn't claimed as a generic float): rewrites the
  placeholder into CustomNode("ExampleEmbed"), populating the crossref triple
  {ref_type,kind,identifier} only when `file=` validates AND the id is `demo-…`.
- The crossref core is unchanged — CrossrefIndex/Resolve/Render handle the node
  generically via the triple (CrossrefRender leaves the unknown type-name
  alone). Only the registry gains `("demo","Demo")`.
- New ExampleEmbedRenderTransform (finalization, after CrossrefRender): emits
  the iframe (page-relative src) + a "Demo N: caption" line when numbered +
  source link; container carries the `#demo-…` anchor. No id → plain embed
  (Phase-1 parity).

The Phase-1/2 static-only contract (Q-5-4/Q-5-5) and page-relative src carry
over unchanged. Backward compatible: unnumbered embeds render exactly as before.

Tests: 12 unit (sugar + render) + 3 crossref integration fixtures (numbering,
exm-vs-demo counter independence, unnumbered-not-indexed). Verified end-to-end
through `q2 render` (HTML and revealjs) and in the real docs site
(`#demo-fragments` + `@demo-fragments`). `cargo xtask verify --skip-hub-build`
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pya2d)

Revert the CLI-only disk approach (resolver vfs-branch + quarto-preview disk
route) — it worked for q2 preview but can't serve a hub-client/Automerge project
with no disk. Chosen instead: VFS-native fix (TS iframe post-processor falls back
to the VFS source path when the artifact path misses), avoiding per-render
Automerge duplication. New strand bd-kjrpya2d + plan; embed plan updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ath (bd-kjrpya2d)

Option B, part 1. Teach the TS iframe post-processor
(ts-packages/preview-renderer/src/utils/iframePostProcessor.ts) to inline
`<iframe>` elements whose src is artifact-rooted (`/.quarto/project-artifacts/X`)
via `srcdoc`, reading the bytes from the VFS — with a fallback to the source
path `X` when the artifact path misses.

Why the fallback: a static project asset (e.g. an `.embed-example-iframe` deck)
is referenced by its artifact-rooted URL (page_url_for roots output hrefs there
in VFS-root mode), but a static asset lives at its *source* path, not the
rendered-artifact path — nothing copies a `resources:` asset under the artifact
root in the WASM render. This is the VFS-native counterpart to how `q2 render`
copies the resource into `_site/`, and it avoids the rejected per-render
artifact-copy (which would re-duplicate the resource set into the VFS every
render — see bd-q3bxnq2e).

New helper `readArtifactOrSource`; 4 jsdom tests (source-path fallback, artifact
preferred, external src untouched, both-miss left alone). Existing post-processor
integration tests still green; package typecheck clean.

Part 2 (get the staged deck into the preview VFS — broaden the q2-preview VFS
sync beyond .qmd/config) is still needed for end-to-end; tracked on bd-kjrpya2d.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rt 2 scoped (bd-kjrpya2d)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion point; security strand bd-teh4hbli (bd-kjrpya2d)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rpya2d next)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…FS (bd-kjrpya2d)

Make `.embed-example-iframe` decks render under `q2 preview` (part 2).
The deck must be (a) present in the preview VFS source and (b) inlined
by the renderer, since the in-browser SPA has no server to answer the
iframe's `/examples/...` request.

VFS sync (Rust). Resolve the `resources:`-scoped `.html` set in
`quarto-preview` (which already depends on `quarto-core`), keeping
`quarto-hub` free of an engine dep:
- `config::resolve_project_resource_html` — `ProjectContext::discover`
  + `expand_patterns` (project scope), filtered to `.html`, returned
  as project-relative paths (best-effort; empty on any failure).
- `PreviewConfig.resource_html_files` → `HubConfig.resource_files` →
  `ProjectFiles::with_resource_files`, a new TEXT-synced discovery
  category (`.html` decks ride the text reconcile path so the SPA's
  `vfsReadFile` can read them). The bare discovery walk never sees
  `.html`; the caller-resolved set is injected.
Real-binary verified: `q2 preview docs/` logs `resource_count=8` and
`Reconciled ... count=163`.

Renderer (TS, q2-preview). Plan correction surfaced by the e2e run:
`q2 preview` does NOT use `iframePostProcessor.ts` (that is the
hub-client preview pane). q2-preview renders via `Q2PreviewIframe` +
React `<Ast>`; the embed is a `RawBlock(html)` and assets resolve
through the parent-side `assetWalker.ts` (previously Image-only):
- `assetWalker` also collects `.embed-example-iframe` srcs from
  `RawBlock(html)`, reads the deck text via `vfsReadFile`, and mints a
  `text/html` blob URL into the asset manifest.
- `blocks/RawBlock.tsx` rewrites the deck `<iframe src>` to that blob
  URL via `AssetManifestContext`.
- shared scan/rewrite helpers in `q2-preview/embedIframe.ts`.
`iframePostProcessor.ts` was also generalized (page-relative `/X` →
VFS source) — kept because it is correct for the hub-client pane.

E2E (Playwright vs live `q2 preview docs/`): all 8/8 `.embed-example-
iframe` decks resolve to `blob:` srcs; `#demo-fragments` loads the real
Fragments deck (reveal "FRAGMENTS" slide rendered); "Demo 1:" caption +
`@demo-fragments` xref work; zero console errors.

Tests: discovery (+3), config resolver (+4), embedIframe (+8),
assetWalker (+3 embed), RawBlock (+4), iframePostProcessor (+6); full
preview-renderer suite (202) green; `cargo xtask verify` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan for making reveal.js emit linked shared assets (site_libs) instead
of inlining ~700KB into every slides.html — mirroring how format:html
handles vendored deps. Records the decisions from the design discussion
(linked-only, no embed-resources opt-in yet, single theme for now,
render-side scope, keep the Rust VFS-sync) and a Phase R to revert the
inline-embedding behavior introduced by the embed-in-preview work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-jij5gge2 Phase R)

Restores the preview-renderer TS to its `main` state, undoing the
embed-in-preview work that inlined deck bytes into the preview HTML
(blob URL / srcdoc / data URI). That approach only worked because
reveal.js currently self-contains all assets into one HTML file;
once reveal emits linked shared assets (bd-jij5gge2), decks reference
site_libs/revealjs/... and must be SERVED, not inlined (revisited
under bd-kjrpya2d).

Reverted to main:
- utils/iframePostProcessor.ts (drop <iframe> srcdoc inlining)
- q2-preview/assetWalker.ts (Image-only; drop embed-deck blob URLs)
- q2-preview/blocks/RawBlock.tsx (plain dangerouslySetInnerHTML)
- q2-preview/assetWalker.test.ts
Deleted (branch-only):
- q2-preview/embedIframe.ts + test
- q2-preview/blocks/RawBlock.test.tsx
- utils/iframePostProcessor.embed.test.ts

KEPT (per Q-R1, reusable by the future served approach): the Rust
VFS-sync of resources-scoped .html (discovery resource_files category,
HubConfig/PreviewConfig wiring, resolve_project_resource_html). It does
not embed into HTML; it makes deck files available in the preview VFS.

The .embed-example-iframe transform (bd-z1smhvuo) and crossref Demo
blocks (bd-t3cert81) are untouched — they emit a plain <iframe src>
reference (the docs feature), not resource embedding.

preview-renderer: tsc clean, 181 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g (bd-jij5gge2)

reveal.js render no longer inlines ~700KB of reset/reveal/theme/quarto
CSS + reveal.js into every slides.html. Instead it registers them as
Project-scoped artifacts and links them, so a website with N decks
shares ONE copy under site_libs/revealjs/ — matching how format:html
handles bootstrap/themes. (The "q2 single binary" goal is unaffected:
the assets are still include_str!-embedded in the binary; only the
render-time *output* changes from inline to linked.)

- revealjs/assemble.rs:
  - render_revealjs_document(body, meta, css_urls, js_urls) emits
    <link>/<script src> in cascade order; only Reveal.initialize stays
    inline (per-document config).
  - reveal_assets(theme) + register_reveal_assets(store, theme): the
    ordered asset set as artifacts (keys css:revealjs:<order> /
    js:revealjs:reveal so collect_artifact_urls' sorted-key order ==
    CSS cascade; paths revealjs/<file>; Project scope). Theme name is
    resolved (unknown → white) so the key/filename stay stable for dedup.
- compile_theme_css.rs: the reveal branch (which already skips Bootstrap)
  now registers the reveal assets instead of just early-returning.
- apply_template.rs: the reveal branch collects css:revealjs: / js:revealjs:
  URLs via the resolver (never the Bootstrap css:theme:) and passes them
  to the assembler.

Verified end-to-end via the real binary (q2 render talk.qmd → 1016-byte
HTML, 4 <link> + reveal.js <script>, assets at talk_files/revealjs/*).

Tests: assemble unit tests (link emission, artifact registration,
theme fallback); revealjs_format integration (links-not-inlined +
on-disk flush); artifact_scoping_pipeline (two-deck website shares one
site_libs/revealjs/ copy). Full quarto-core suite: 2260 passed.

Remaining (Phase 4): re-stage docs examples + full cargo xtask verify
(incl. WASM); preview/WASM direct-deck path not yet exercised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-jij5gge2)

Phases 0-4 complete: render-side linking + dedup implemented, tested,
and browser-verified on a static server. Full cargo xtask verify (incl
WASM) green. Remaining: direct-deck preview ([Q-6]) and the served-iframe
embed rework (bd-kjrpya2d).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ep React path (bd-ibqkf9ry)

Converge q2 preview's reveal rendering with q2 render WITHOUT removing the
React-component path (required for the collaborative-editing AST<->source
mapping). Four prongs: (A) one enforced reveal version, (B) one CSS source
(vendored resources/revealjs/), (C) CSS isolation — gate the HTML-theme/
Bootstrap injection off for reveal decks (the grey-line root), (D) transitions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bqkf9ry Prong A)

q2 render embeds the vendored resources/revealjs/ copy (include_str!);
q2 preview renders via @revealjs/react, which peer-depends on the npm
reveal.js package. They must stay on the SAME version/bytes or the two
pipelines drift.

- Pin reveal.js -> 6.0.0 and @revealjs/react -> 0.2.0 (exact, drop ^) in
  the 3 package.json (root, hub-client, preview-renderer); regenerate lock.
- Add assemble.rs test `vendored_reveal_assets_match_npm_package`: asserts
  the include_str!'d constants (reset/reveal/reveal.js/theme-white) are
  byte-identical to node_modules/reveal.js/dist/*. Fails if the vendored
  copy drifts from a future npm bump (re-sync per resources/revealjs/
  README); skips gracefully when node_modules is absent.

@revealjs/react@0.2.0 has reveal.js as a peerDependency (no bundled copy),
so it uses the hoisted top-level reveal.js@6.0.0 — one engine for both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ibqkf9ry Prongs B+C)

Two render/preview reveal divergences, both from the deck drawing on a
different CSS source/environment than `q2 render`:

Prong B — one CSS source. RevealDeck.tsx now imports the SAME vendored
resources/revealjs/{reset,reveal,theme/white,quarto-reveal}.css that
render links (in cascade order), instead of the npm reveal.js/*.css.
Adds the previously-missing reset.css. @revealjs/react injects no CSS of
its own, so this is a clean import swap. Combined with Prong A's sync
check, resources/revealjs/ is the single source of truth for both pipelines.

Prong C — same CSS environment (the grey-line fix). The q2-preview iframe
injected the compiled HTML-theme (Bootstrap) CSS via a data-q2-theme <link>
for EVERY document, so Bootstrap's `h2 { border-bottom: 1px solid #dedede }`
leaked onto reveal slide headings (a faint grey line render never shows).
entry.tsx now reconciles the theme link against the active format: it
remembers the last theme URL + whether the doc is a slide deck (the two
arrive on separate UPDATE_THEME/UPDATE_AST messages) and attaches the
link ONLY for non-slide docs. A format switch re-applies/removes it.

Verified in a fresh preview (rebuilt SPA+q2) vs the live render server:
every slide h2 borderBottom is now `0px none` (was `1px solid #dedede`),
no data-q2-theme link present, deck renders the white theme cleanly —
matching render. preview-renderer: tsc clean, 181 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… by C (bd-ibqkf9ry)

Root cause for BOTH symptoms is the Bootstrap CSS leaking into the reveal
deck: h2 border-bottom (grey line) AND the prefers-reduced-motion reboot
override (transition-duration .01ms !important) that killed transitions
for reduced-motion users. Prong C (suppress the HTML-theme link for reveal
decks) fixes both. Browser-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…9ry)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er-preview-convergence

# Conflicts:
#	package-lock.json
…bd-kjrpya2d)

The `.embed-example-iframe` decks rendered blank in `q2 preview` with
module-script MIME errors: the iframe requests the deck at the artifact-
rooted path `/.quarto/project-artifacts/examples/.../slides.html`, but the
hub's SPA fallback returned `index.html` for every unmatched path — so the
iframe loaded the SPA shell, whose `<script type=module>` then 404'd→text/html.
Since the reveal linked-assets change (bd-jij5gge2) decks LINK their
`slides_files/…` rather than self-containing, so they must be SERVED (the
inline/srcdoc approach can't resolve the relative asset links and was reverted).

Add an axum route in the preview hub that serves the project's `resources:`-
declared files from disk at that artifact path:
- `config::resolve_project_resource_files` resolves the FULL resources: set
  (deck HTML + every `slides_files/…` sidecar) to (output-relative → absolute
  source) pairs.
- `RESOURCE_DISK_MAP` (OnceLock, populated once in `run()`) backs an
  `/.quarto/project-artifacts/{*rest}` handler: serves a declared resource from
  disk (correct content-type) or falls through to the SPA index — so it only
  ever serves files in the `resources:` publish set (the bd-teh4hbli boundary)
  and never shadows the existing VFS-served artifacts.

Scope: CLI/disk-only. Diskless hub-client embeds need the service-worker-over-
VFS (separate colleague workstream); tracked under bd-kjrpya2d.

Verified e2e: `q2 preview docs` → the #demo-fragments iframe loads & renders
the real Fragments deck (reveal initializes, nav arrow, "Demo 1" caption),
zero console errors; `curl` confirms slides.html + slides_files/revealjs/
reveal.js serve with correct MIME. config resolver unit-tested; full
quarto-preview suite (75) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(PR #271)

The merge-conflict resolution regenerated package-lock.json via rm+npm
install on macOS, which dropped the @rollup/rollup-linux-x64-gnu package
entry (npm optional-deps bug npm/cli#4828) — breaking the WASM build on
Linux CI. Restored origin/main's complete cross-platform lock and re-pinned
reveal.js->6.0.0 / @revealjs/react->0.2.0 (the only intended delta; also
realigns npm reveal.js with the vendored resources/revealjs 6.0.0 the sync
test checks). All 25 rollup platform binaries retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er-preview-convergence

# Conflicts:
#	ts-packages/preview-renderer/src/q2-preview/entry.tsx
@cscheid cscheid merged commit cc246ee into main Jun 10, 2026
4 of 5 checks passed
@cscheid cscheid deleted the feature/revealjs-render-preview-convergence branch June 10, 2026 17:08
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.

1 participant