Skip to content

hub-client: q2-preview is the default renderer; format: q2-html-render opts into the full-DOM iframe (bd-kltzdhle) - #670

Open
cscheid wants to merge 17 commits into
mainfrom
feature/bd-kltzdhle-hub-client-default-q2-preview
Open

cscheid wants to merge 17 commits into
mainfrom
feature/bd-kltzdhle-hub-client-default-q2-preview

Conversation

@cscheid

@cscheid cscheid commented Sep 10, 2026

Copy link
Copy Markdown
Member

Summary

Brings q2 preview's renderer choice to hub-client. A document with no format: key, format: html, or a format: html: {…} map now previews through the q2-preview React renderer (Q2PreviewIframe), so comments, the Edit pill, Authors, and the printable version are available on every plain document. The old full-DOM MorphIframe renderer stays reachable with the new format: q2-html-render (a builtin pseudo-format that q2 render treats exactly like q2-debug: plain HTML output).

Plan and decision record: claude-notes/plans/2026-09-09-hub-client-default-q2-preview.md (strand bd-kltzdhle).

What changed

  • Rust (quarto-core, wasm-quarto-hub-client): register q2-html-render; render_page_in_project_with_attribution gains an optional prefer_preview_format argument that applies the q2 preview substitution (html → q2-preview, revealjs → q2-slides) while keeping RenderHost::HubClient (so Q-5-12 still fires — the reason the SPA's render_page_for_preview could not be reused). The response now reports the resolved format.
  • hub-client: getQ2Format maps html → q2-preview and q2-html-render → full-DOM; ReactPreview renders every preview-pipeline format through the one entry point with the knob, which also moves reveal decks off the SPA-only entry point (attribution now reaches decks; the Authors pill gate is a follow-up, bd-jx8b0ax9). Editor chrome gates key on the router-reported format and need no change.
  • q2 preview SPA: a successful render that came back as html (q2-html-render, pdf, …) shows a "no live preview in q2 preview … run q2 render" message naming the format instead of console.error + the boot screen.
  • e2e: the iframe kind names the renderer (q2-html-render | q2-debug | q2-preview, no default); smoke-all picks the kind from the fixture's own front matter; the CSS assertion helper reads the render string instead of the old iframe's DOM.
  • Phase 4b (partial): the draft-alert banner now renders in q2-preview (bd-3cpv7dah); the three title-banner fixtures pin placement on the render output instead of a body > selector the React root can never satisfy (bd-xiz1a2go). Each is its own commit.

Known: 6 fixtures keep their e2e DOM assertions skipped

DOM_ASSERTIONS_PENDING_PARITY (hub-client/e2e/helpers/smokeAllDiscovery.ts) lists fixtures whose ensureHtmlElements selectors do not yet hold in the q2-preview iframe. Only that assertion type is skipped — the fixtures still render in the default iframe with their regex / CSS / diagnostics assertions live — and each skip logs a [smoke-diag] dom-assertions-skipped strand=… line. Owners: bd-c3dtpe36 (mermaid), bd-b3oq2fsy (user css: links), bd-fandfn60 (repo actions in TOC), bd-bg0jze2i (callout body heading), bd-q2wqj24c (CommentBlock wraps every block in a positioned div, breaking parent > child theme rules — handoff note at claude-notes/plans/2026-09-10-commentblock-overlay-handoff.md), bd-47afd5ro (tabsets).

Verification

  • cargo xtask verify (full, with the hub build) green at the branch head; hub-client unit / integration / WASM tiers under the pinned Node 24; smoke-all e2e 156 passed, 1 pre-existing skip.
  • Real browser via q2 preview --ui editor (the embedded hub-client bundle): a plain document mounts the q2-preview iframe with Edit/Authors/Comments enabled and a comment added from the preview lands in the source as [>> …]; format: q2-html-render mounts the MorphIframe with the pills disabled; format: html: {toc: true} and a reveal deck route correctly. Screenshot in claude-notes/plans/bd-kltzdhle-plain-doc-q2-preview.png.
  • q2 preview on a q2-html-render document shows the new message; q2 render on it writes HTML.

Review notes

  • The changelog entries are in their own commits per the two-commit workflow.
  • The body > fixture edit (3a7653a) touches the P5 title-block parity corpus; the placement intent is preserved with ensureFileRegexMatches on the render.

🤖 Generated with Claude Code

https://claude.ai/code/session_0169cS5WjZuUvam43it8hdtX

cscheid and others added 11 commits September 9, 2026 15:52
…client WASM entry point (bd-kltzdhle, Phases 1-2)

Groundwork for making q2-preview hub-client's default renderer (plan:
claude-notes/plans/2026-09-09-hub-client-default-q2-preview.md).

Phase 1 - `q2-html-render` is a builtin pseudo-format (base `html`,
no preview pipeline_kind), the explicit opt-out into the full-DOM
MorphIframe renderer once html defaults to q2-preview. In the
pipeline it behaves exactly like `q2-debug`: `q2 render` writes an
ordinary HTML file for it (verified end-to-end; previously
"Unknown format: q2-html-render"). Registered in
`builtin_pseudo_format`, `lua_format_for`, and the WASM
`coerce_format_for_print`.

Phase 2 - `render_page_in_project_with_attribution` gains a trailing
`prefer_preview_format: Option<bool>`. When true it applies the same
default-format substitution `q2 preview` uses (html -> q2-preview,
revealjs -> q2-slides) while keeping `RenderHost::HubClient`, so the
host-dependent Q-5-12 render-scripts warning still fires - the reason
hub-client cannot reuse `render_page_for_preview`, which hard-codes
the native-preview host. Omitted/false is byte-identical to before.
The TS wrapper `renderPageInProjectWithAttribution` exposes it as an
optional fifth argument.

Tests: quarto-core `format::tests` (4 new/extended cases) and the new
hub-client WASM tier `previewFormatSubstitution.wasm.test.ts` (7 cases,
run red against the pre-knob WASM first). Full workspace nextest,
hub-client `test:wasm` (140), typecheck, and clippy green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169cS5WjZuUvam43it8hdtX
…r opts back into the full-DOM iframe (bd-kltzdhle, Phase 3)

A document with no `format:` key, `format: html`, or a `format: html:
{...}` map now previews through the React AST renderer
(Q2PreviewIframe) - the same choice `q2 preview` makes - so comments,
the Edit pill, Authors, and the printable-document affordance are
available on every plain document. `format: q2-html-render` is the
explicit opt-out into the full-DOM MorphIframe renderer, which is
otherwise only the fallback for non-html formats (pdf, docx,
extension formats).

Mechanism (plan D2/D3):
- `getQ2Format` maps `html` -> `q2-preview` and `q2-html-render` ->
  null (full-DOM); other `q2-*` and `revealjs` pass through. This is
  the JS half of the `map_format_for_preview` substitution in the WASM.
- `ReactPreview.doRender` renders every preview-pipeline format through
  `renderPageInProjectWithAttribution(..., preferPreviewFormat=true)`,
  so the WASM applies the same substitution after re-detecting the
  format from the file. The revealjs branch moves off the SPA-only
  `renderPageForPreview` onto this call, which gives decks attribution
  (bd-zvh2p) and the correct hub-client render host.
- `Editor.tsx`'s chrome gates already key on the router-reported
  format, so the pills enable for plain documents with no change there.

Tests (written first, red before the change): `getQ2Format.test.ts`
(6), new `PreviewRouter.integration.test.tsx` (3: no-format ->
ReactPreview as q2-preview; q2-html-render -> Preview; q2-debug pass
through), the capture and rerender ReactPreview tests now pin the fifth
argument and that `renderPageForPreview` is never called from
hub-client, and `pipelineKind.test.ts` pins q2-html-render as a non-
preview pipeline. hub-client unit (1102) / integration (128) / WASM
(140) tiers, typecheck, and `npm run build:all` green under Node 24.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169cS5WjZuUvam43it8hdtX
…no live preview (bd-kltzdhle, Phases 4-6)

Phase 4 - hub-client e2e. The iframe kind now names the renderer, not
the document format: `'html'` is renamed `'q2-html-render'`, and the
default kind is removed so every caller states which iframe it expects.
smoke-all picks the kind from the fixture's OWN front matter (new
`DiscoveredTest.documentFormat`): q2-html-render -> MorphIframe,
q2-debug -> Q2DebugIframe, everything else (including html-spec
fixtures with no format: key) -> Q2PreviewIframe. The three plain-
fixture specs and the extraction spec switch to the q2-preview iframe;
the click-to-editor-scroll control fixture declares q2-html-render
explicitly since it tests that renderer on purpose.

First run: 121/157 passed. 24 failures were one harness bug -
`getPreviewCss` read <link> tags from the MorphIframe DOM. It now parses
the render string `renderForAssertions` already produces, so the CSS
assertions are renderer-independent. The other 11 are DOM selectors on
page chrome / missing components in the q2-preview iframe; per plan D8
they are listed in `DOM_ASSERTIONS_PENDING_PARITY` with an owning
strand each (bd-3cpv7dah, bd-c3dtpe36, bd-b3oq2fsy, bd-fandfn60,
bd-bg0jze2i, bd-47afd5ro, and new bd-xiz1a2go / bd-q2wqj24c). Only the
DOM assertion type is skipped for those fixtures - they still render in
the default iframe and keep their regex/CSS/diagnostics assertions - and
each skip prints a `[smoke-diag] dom-assertions-skipped` line. Rerun:
156 passed, 1 skipped, 0 failed. Phase 4b works the list down.

Phase 5 - `q2 preview` has no full-DOM renderer, so a successful render
that came back as `html` (format: q2-html-render, pdf, ...) now shows a
"no live preview in q2 preview ... run `q2 render`" message naming the
format, instead of `console.error` + the boot screen. The format name
comes from a new `format` field on the WASM `RenderResponse` (the
resolved target_format after any preview substitution; omitted on
errors), pinned by `previewFormatSubstitution.wasm.test.ts`.

Phase 6 - notes in the two plans that called the html preview the
default; bd-zvh2p closed (attribution reaches reveal decks via D3).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169cS5WjZuUvam43it8hdtX
…ull verify)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169cS5WjZuUvam43it8hdtX
…cpv7dah; bd-kltzdhle Phase 4b)

`DraftAlertTransform` already runs in the q2-preview pipeline and
publishes the localized label at `meta.rendered.draft-alert-text`; only
the banner markup lived in the HTML template, which the React preview
never uses. Add `DraftAlertSlot` (chromeSlots.tsx) with Q1's exact
markup - `<div id="quarto-draft-alert" class="alert alert-warning">
<i class="bi bi-pencil-square"></i>{label}</div>` - and render it in
`PreviewDocument` before the site header, mirroring the template's
`$if(rendered.draft-alert-text)$` above `$quarto-header()$`.

Tests (written first, red): three PreviewDocument cases - markup and
position before `header#quarto-header`, a page with no header, and no
banner without the label. The two smoke-all fixtures
(drafts/draft-banner, localization/lang-es-draft-banner) leave the
e2e DOM-assertion skip-list and pass in the q2-preview iframe.
Verified in `q2 preview` on a `lang: es` draft: label "Borrador",
Bootstrap `.alert-warning` colours applied, banner before `main`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169cS5WjZuUvam43it8hdtX
…itle-banner items

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169cS5WjZuUvam43it8hdtX
…y > selector (bd-xiz1a2go; bd-kltzdhle Phase 4b)

The three title-block banner fixtures asserted `body > header#title-block-header ...`
as the P5 "banner renders above #quarto-content" check. The q2-preview
renderer emits the same header, banner, title/subtitle/date, and
`main.quarto-banner-title-block` (verified in the iframe, theme colour
applied), but React mounts into div#root, so nothing it renders can be a
direct child of body - the selector encoded page structure, not banner
parity. Drop the `body >` prefix from the DOM selectors and pin the
placement with an `ensureFileRegexMatches`
(`<body[^>]*>\s*<header id="title-block-header"`) on the render output,
which is where the placement is a property of the HTML writer.

Native smoke-all, the hub-client WASM smoke runner, and the e2e runner
(the three fixtures leave DOM_ASSERTIONS_PENDING_PARITY; 3/3 pass in the
q2-preview iframe) are green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169cS5WjZuUvam43it8hdtX
…r removal (bd-q2wqj24c)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169cS5WjZuUvam43it8hdtX
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169cS5WjZuUvam43it8hdtX
@posit-snyk-bot

posit-snyk-bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

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 6 commits September 10, 2026 13:02
…r-block wrapper

Re-verifies the 2026-09-10 handoff note at PR #670's branch HEAD and
narrows it: only five block components can carry comment chrome (not
14), reveal decks have no #quarto-content for a layer to live in, and
AttributionWrap is a second wrapper of the same class (out of scope).
Adds the theme direct-child selector inventory as an investigation
artifact. Verdict: ready to design; seven design questions for the user.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bB83744mRBrrQHrN1vDcm
…us; file bd-ijlb2yui for AttributionWrap

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bB83744mRBrrQHrN1vDcm
…per (bd-q2wqj24c)

CommentBlock wrapped every commentable block in a positioned <div> so the
bubble could be absolutely positioned against it. The theme CSS is written
for the native writer's DOM, where a block is a direct child of its
container, so every `parent > child` rule (`blockquote > h4`,
`.callout-body > :first-child`, `li > p:last-of-type`, `.tab-pane > p`,
`section > section`, …) stopped matching in the live preview — a visual
parity drift against `q2 render`, invisible to the parity harness (whose
read-only mount renders no chrome).

Now the block renders untouched. Its chrome (bubble + block glow) is
portalled into one lazily created body-level layer
(`[data-q2-comment-layer]`) and placed by the relayout pass from the
block's measured rect in layer coordinates (`top = block.top − 11`,
right-aligned 10px past `block.right`, `translate(-100%, nudge)`), on the
existing triggers plus a ResizeObserver on anchors and body. Hover is one
delegated document mousemove resolving the pointer to the deepest
registered anchor (right-half test) or bubble; the glow is an overlay
outline over the block's rect, never a style on the block.

How a bubble finds its block (commentAnchor.tsx): CommentBlock provides a
per-block `CommentAnchorContext`; the five chrome-eligible host
components (Para, Header, CodeBlock, MermaidCodeBlock, Div) spread the
ref it hands back when the context's node is theirs. `Plain` has no
element, so tight-list `<li>`s and definition `<dd>`s render through
`PlainHost` and provide themselves; a Plain with no host renders
passthrough with the comment spans left in the text. Overrides that do
not adopt the hook simply get no chrome.

Decks: the layer sits outside reveal's `.slides` transform, so the
measured-scale / counter-scale / DECK_BUBBLE_FUDGE machinery is gone;
`.present`-slide gating and the `q2-reveal-scale` trigger stay. The chrome
adopts its block's computed font-family on every pass (it no longer
inherits the deck theme's font) and deck chrome is scaled ×1.2.

Tests (red first): CommentBlock.structure (DOM shape with/without
comments and hover, layer, glow, Plain-in-li, no-host passthrough, and a
read-only-vs-live parity guard the harness lacks) and
CommentBlock.geometry (placement in layer coords, overlap nudge);
resolveLast/defensive/bubbleText helpers rewritten for the layer. The
test setup's ResizeObserver/IntersectionObserver stubs are now
constructible classes (vitest 4 will not `new` an arrow-function mock).
The `div-heading-becomes-section` fixture leaves the e2e DOM-assertion
skip-list; its `blockquote > h4#quoted` now runs live (smoke-all 156
passed / 1 pre-existing skip; comment specs 4/4). Full `cargo xtask
verify` green.

Plan + browser verification record:
claude-notes/plans/2026-09-10-commentblock-wrapper-removal.md.
Follow-ups filed: bd-ijlb2yui (AttributionWrap is the same defect
class), bd-5lwrvjp6 (tree-sitter shared-cache hermeticity in verify),
bd-1nuiqmoe (quarto-nav.js observer teardown noise in jsdom).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bB83744mRBrrQHrN1vDcm
); bd-q2wqj24c plan closes out

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bB83744mRBrrQHrN1vDcm
…ts out via q2-html-render (bd-kltzdhle)

The spec arrived from main in the merge (5b5baab) and pinned the old
default: a document with no `format:` key had no printable version, so
the header held only New and Upload. Since bd-kltzdhle a plain document
renders through q2-preview, which is printable, and `.print-file-btn`
appears — CI failed deterministically at files-header.spec.ts:61 on the
merged branch (4/4 attempts).

Swap the fixtures rather than the gate: the two-button case now declares
`format: q2-html-render` (the full-DOM opt-out, no printable), and the
three-button case is the plain document that exercises the new default.
Both pass locally; the full default e2e suite is 75/75 on the merged
tree.

Co-Authored-By: Claude Fable 5.1 <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.

2 participants