Skip to content

Printing/PDF export from the quarto-hub preview (issue #315)#379

Merged
cscheid merged 14 commits into
mainfrom
feature/bd-vhdknrvl-preview-printing
Jul 7, 2026
Merged

Printing/PDF export from the quarto-hub preview (issue #315)#379
cscheid merged 14 commits into
mainfrom
feature/bd-vhdknrvl-preview-printing

Conversation

@cscheid

@cscheid cscheid commented Jul 7, 2026

Copy link
Copy Markdown
Member

Fixes #315 (bd-vhdknrvl).

Problem

Printing/PDF from the live preview is broken: the React preview formats render DOM inside a sandboxed iframe with no standalone document, so the browser's "Print Frame" gives Firefox a clipped single page and Chrome no affordance at all.

Approach — "Open printable version"

A 🖨 button in the preview renders a standalone, self-contained copy of the current document on demand and opens it in a new top-level tab, where the browser paginates natively and @media print applies (⌘P → clean multi-page PDF).

Pipeline:

  1. render_printable(path) — new path-aware WASM export. Coerces the preview pseudo-format to its HTML output (q2-preview→html, q2-slides→revealjs, inverse of map_format_for_preview) and renders through the HTML pipeline so relative image srcs resolve against the doc dir.
  2. makeSelfContainedHtml — inlines every VFS-backed asset: CSS/JS <link>/<script> → inline <style>/<script>, images + CSS url() fonts → data: URIs. Strips a leading BOM so inlined theme CSS actually applies (see below).
  3. forceRevealPrintMode — injects view:"print" into a deck's Reveal.initialize config (blob URLs don't expose ?print-pdf via location.search).
  4. injectPrintStylesheet — conservative @media print baseline for documents (heading break-avoidance, orphans/widows, break-inside for figures/tables/code, @page margins).
  5. UI button in the preview pane, format-aware, with pop-up-block detection.

Notable bug found & fixed

The compiled Bootstrap/reveal theme CSS artifact carries a leading UTF-8 BOM. A <link> load strips it, but inlining into a <style> prefixed the first selector (:root,[data-bs-theme=light]) — one invalid selector in a comma list drops the whole rule, so all --bs-* variables went undefined and the doc rendered unstyled. Fix: strip a leading BOM from inlined CSS/JS.

Verification

  • Real-WASM end-to-end test (printableDocument.wasm.test.ts) drives the actual export + full glue; a themed regression fails without the BOM fix.
  • Browser-verified (Chrome via CDP + user on Firefox/Chrome): q2-preview doc renders themed/self-contained/paginated; revealjs deck (solarized) renders themed, self-contained, in reveal print-pdf layout.
  • Green locally: hub-client test:ci (678+76+123), cargo nextest -p quarto-core (2450), preview-renderer (480+526), WASM build, strict hub-client production build.

Plan: claude-notes/plans/2026-07-06-issue-315-preview-printing.md.

🤖 Generated with Claude Code

cscheid and others added 11 commits July 6, 2026 16:29
… (issue #315)

Phase 1 of bd-vhdknrvl. Adds a pure, DI-based transform that turns the
WASM HTML-pipeline output (standalone document, but with /.quarto/…
<link>/<script> refs and relative <img> srcs) into a single
self-contained document suitable for opening as a bare top-level tab:

- CSS <link> → inline <style> (with url() font/image refs → data: URIs)
- <script src> → executing inline <script> (reveal.js must run)
- <img src> (relative + /.quarto) → base64 data: URIs

VFS access is injected (SelfContainedReaders) so it is decoupled from the
WASM singleton; production binding is deferred to the Phase 3 glue. Adds
web-font MIME types to the shared vfsPaths.guessMimeType.

Tested via 9 jsdom cases; full preview-renderer suite stays green.

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

Phase 2 of bd-vhdknrvl. Adds a path-aware WASM render that produces the
standalone printable form of an in-scope preview document:

- coerce_format_for_print: inverse of map_format_for_preview
  (q2-preview→html, q2-slides→revealjs, q2-debug/q2-raw→html, else
  passthrough) so the HTML pipeline (and reveal deck assembler) runs.
- render_single_doc_to_response gains a format_override param (existing
  4 callers pass None); render_printable(path) reads the VFS file,
  coerces the detected format, and renders through the HTML branch with
  the real path so relative image srcs resolve against the doc dir.
- renderPrintable wrapper + type decls in preview-runtime.

The output still links /.quarto/ artifacts; the JS makeSelfContainedHtml
inliner (Phase 1) embeds them before the doc opens in a top-level tab.

Load-bearing render contract pinned natively in a new quarto-core
integration test (the WASM crate is cdylib-only): a q2-preview-frontmatter
doc renders to full HTML with its relative image src preserved, and a
revealjs doc renders to a standalone deck. WASM rebuilt and verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lockfile catch-up materialized by the WASM build; unrelated to the
render_printable change.

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

Phases 3-4 of bd-vhdknrvl. Wires the printable-export end to end and
exposes it in the preview UI:

- forceRevealPrintMode (preview-renderer): inject view:"print" into a
  deck's own Reveal.initialize config, so reveal enters its paginated
  PDF layout when opened from a blob: URL (whose query string is not
  reliably exposed via location.search).
- printableDocument service (hub-client): renderPrintable →
  makeSelfContainedHtml (readers bound to the WASM VFS) →
  forceRevealPrintMode for slides → Blob URL → window.open in a new
  top-level tab, with pop-up-block detection and delayed revoke.
- Floating 🖨 button in the preview pane (Editor.tsx/.css), shown for
  q2-preview / q2-slides / revealjs, with a dismissible error toast.

Pure logic unit-tested (revealPrintMode, isPrintableSlidesFormat,
buildPrintableHtml throw path). hub-client production build (tsc -b &&
vite build) passes. Browser E2E still pending.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drives the real render_printable WASM export against a VFS project and
runs makeSelfContainedHtml + forceRevealPrintMode over the output — the
production pipeline minus window.open. Verifies a q2-preview doc renders
to full HTML (image src preserved) and inlines to a self-contained
document (no /.quarto refs, image as data URI), and a revealjs doc
renders to a standalone deck forced into reveal print layout.

Both artifacts were also opened in Chrome and visually confirmed: the
doc renders themed/self-contained; the deck shows reveal's print-pdf
stacked-slide layout.

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

Phase 5 of bd-vhdknrvl. q2's HTML template inlines only the theme's
@media print rules and omits pandoc's default print partial, so a
printable document lacked heading break-avoidance, orphan/widow control,
and figure/code/table break-inside protection.

injectPrintStylesheet appends a conservative @media print block (before
</head>) covering those basics plus @page margins and a white-background
reset. Wired into buildPrintableHtml's document branch; reveal decks skip
it (they ship their own precise per-slide print CSS). Injected
printable-only in the JS glue, so q2 render and the live preview are
untouched.

The E2E wasm test now drives the full buildPrintableHtml glue and asserts
data-q2-print present for docs / absent for decks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…able export (issue #315)

Field bug: the printable document opened with no theme styling in both
Firefox and Chrome. Root cause found by reproducing the exact browser
pipeline: the compiled Bootstrap theme CSS artifact carries a leading
UTF-8 BOM (U+FEFF). A <link> load strips it from the byte stream, but
makeSelfContainedHtml injected the bytes verbatim into a <style>, where
the BOM prefixes the first selector (`:root,[data-bs-theme=light]`)
— an invalid selector in a comma list drops the WHOLE rule, so
Bootstrap's `--bs-*` variables were never defined and the theme silently
fell back to unstyled serif defaults.

Fix: strip a leading BOM from inlined CSS and JS text. Verified
end-to-end in a real browser (Chrome via CDP): stripping the BOM makes
`--bs-body-font-family` resolve and the cosmo theme render correctly.

Tests: 2 unit cases (BOM strip for CSS + JS) in makeSelfContainedHtml,
and a themed real-WASM regression in printableDocument.wasm.test.ts that
fails without the fix (asserts no inlined <style> begins with a BOM).

This was missed originally because the first E2E fixture used a minimal
themeless doc with no BOM-prefixed theme CSS to inline.

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

posit-snyk-bot commented Jul 7, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

cscheid and others added 3 commits July 7, 2026 08:32
…es panel (issue #315)

Relocates the 🖨 print affordance out of the floating preview-pane overlay
into the Files sidebar header (next to New / Upload), per user request.
The entire printable concern (button, in-flight + error state, handler)
now lives in FileSidebar, driven by a new optional `currentFormat` prop;
Editor just threads that prop through. Removes the preview-pane button,
error toast, and associated CSS.

Adds FileSidebar integration tests: the button is shown only for
printable formats (q2-preview / q2-slides / revealjs), hidden otherwise,
and clicking opens the printable document for the current file.

Verified in an isolated browser mount: "🖨 Print" renders in the Files
header matching the New/Upload button style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adding the Print button made three buttons compete for the narrow
sidebar header; flex-shrink squeezed New/Upload below their content
width so their labels wrapped to a second line, making them taller than
the single-line Print button.

Give all header buttons white-space:nowrap + flex-shrink:0 and a fixed
line-height (so an emoji's taller line box doesn't inflate one button),
and let the header flex-wrap so whole buttons drop to a second row when
the width is too small — instead of one button going two-line. All three
are now the same height (28px); verified at 250px (one row) and 190px
(Upload wraps as a whole button).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cscheid cscheid merged commit 29e4ace into main Jul 7, 2026
1 of 4 checks passed
@cscheid cscheid deleted the feature/bd-vhdknrvl-preview-printing branch July 7, 2026 13:46
cscheid added a commit that referenced this pull request Jul 7, 2026
…reaking CI

The workbox `maximumFileSizeToCacheInBytes` ceiling was 35MB while the
`wasm_quarto_hub_client_bg.wasm` artifact had crept to ~36.7MB. When a
globbed precache asset exceeds the limit, workbox emits a warning that
vite-plugin-pwa's `logWorkboxResult` throws as a fatal build error,
failing `vite build` (and thus the TS Test Suite's Build WASM step).

The boundary was razor-thin: the local/ubuntu build landed just under
35MiB while the macOS CI build produced ~990 bytes over, so main went
red only on macOS after #379 (render_printable + self-contained
inliner) grew the WASM.

Raise the ceiling to 64MB to restore generous headroom for continued
WASM growth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

print to pdf is super broken in our react preview formats

2 participants