diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index dc270902..a4314fd9 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -18,6 +18,7 @@ {"id":"bd-12fpz","title":"D4/D5: HTML writer emits odd/even and header row classes","description":"## What\n\n`write_table_row` in `crates/pampa/src/writers/html.rs:1490` emits bare `` for every row. Pandoc's reference HTML writer (which Q1 inherits) adds:\n\n- `` on header (thead) rows.\n- `` / `` alternating on body rows.\n\n## Fix\n\nPass row index + is_header from the table-writing loop into `write_table_row` (it already gets `is_header`); add the class accordingly.\n\n## Tests (TDD)\n\nSnapshot test on a 3-row table (1 header + 2 body) showing:\n- header row: ``\n- first body row: ``\n- second body row: ``\n\n## Plan\n\nclaude-notes/plans/2026-05-20-table-default-rendering-parity.md (D4, D5 sections — bundled because they share the same writer code path)","status":"closed","priority":3,"issue_type":"bug","created_at":"2026-05-20T20:55:24.955808Z","created_by":"cscheid","updated_at":"2026-05-20T21:31:14.339774Z","closed_at":"2026-05-20T21:31:14.339617Z","close_reason":"Implemented in this commit: row classes (header/odd/even) emitted.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-12fpz","depends_on_id":"bd-hir7j","type":"parent-child","created_at":"2026-05-20T20:55:24.955808Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-12vrr","title":"Callout default-title synthesizer + popup-menu callout-type switching (Plan 6 §B)","description":"Plan 6's audit identified crates/quarto-core/src/transforms/callout_resolve.rs:267 (default callout title 'Note'/'Tip'/'Warning'/...) as an AST synthesizer that today emits SourceInfo::default(). To bring it into the Generated shape Plan 6 establishes, it needs: (a) a new By::callout() constructor in quarto-source-map, (b) an atomicity decision (is_atomic_kind), (c) the fix at callout_resolve.rs:267, (d) a per-transform test. Deferred from Plan 6 because it was not enumerated in the plan body and requires the new By constructor + atomicity decision.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-22T16:19:01.484116Z","created_by":"gordon","updated_at":"2026-05-22T16:45:31.449690Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-12vrr","depends_on_id":"bd-129m3","type":"related","created_at":"2026-05-22T16:19:13.342003Z","created_by":"gordon","metadata":"{}","thread_id":""}],"comments":[{"id":27,"issue_id":"bd-12vrr","author":"Gordon Woodhull","text":"Why this matters beyond the audit fix: the React preview needs a UI affordance to **switch callout type** (Note → Tip → Warning → Important → Caution) via a popup menu on the callout block. For that to round-trip cleanly through Plan 7's writer, the default-title text (\"Note\", \"Tip\", …) must NOT be treated as user-editable Original content — otherwise typing-as-edit semantics would force the writer to materialize \"Note\" as literal text into source, even when the user only wanted to swap the type via the menu.\n\nThe `Generated { by: callout(), … }` stamping + the atomicity decision determine:\n\n1. **React-side**: whether the title region is gated read-only via Plan 2A's atomic check (so the user can't accidentally type into it; the menu is the only path to mutation).\n2. **Writer-side**: how a menu-driven type change serializes. The menu rewrites the Callout CustomNode's `plain_data` (e.g. `{\"type\": \"tip\"}`) and leaves the title-Generated either re-synthesized on next pipeline run or replaced with a fresh Generated whose `by.data` reflects the new type. Plan 7's let-user-win path handles the CustomNode rewrite naturally; the title region soft-drops on direct edits (Q-3-42-style).\n\nRecommend `is_atomic_kind = true` for `By::callout()`'s title-synthesizer use so the title is non-editable text and only menu-driven type changes mutate it. The popup-menu component lives in hub-client's React framework registry (post-Plan 2B); this beads owns the Rust-side provenance support that makes the round-trip behave.\n","created_at":"2026-05-22T16:45:31Z"}]} {"id":"bd-140x","title":"Contribute samod fix upstream with pure-rust reproduction test","description":"Cherry-pick the NotFound race condition fix (431333f) from quarto branch to main on cscheid/samod fork, and write a pure-rust integration test in samod-core/tests/ that reproduces the bug independently of quarto-hub. The test should demonstrate that a document synced from a client to a DontAnnounce server is dropped during handle_load when pending_sync_messages are ignored. PR target: alexjg/samod.","status":"open","priority":1,"issue_type":"task","created_at":"2026-03-04T00:58:02.303737Z","created_by":"cscheid","updated_at":"2026-03-04T00:58:02.303737Z","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-14rer","title":"ExecuteResult.filters set by Knitr engine is never consumed downstream","description":"Workspace-wide grep for `result.filters` / `ExecuteResult.*filters` returns hits in tests, the Knitr write site, extension parsing, and a comment in capture-splice — but no production consumer.\n\nConcretely:\n- crates/quarto-core/src/engine/knitr/mod.rs:215 sets `filters: result.filters` from the R subprocess output (test fixture asserts `vec![\"rmarkdown/pagebreak.lua\"]`, knitr/types.rs:281).\n- crates/quarto-core/src/filter_resolve.rs only resolves filters from YAML `filters:` document metadata, not from ExecuteResult.filters.\n\nSo Knitr's pagebreak filter is silently dropped today. This also blocks any future engine (like a mermaid Lua-filter approach) that wants to declare filters dynamically.\n\nDecision needed: wire ExecuteResult.filters into the resolver, or remove the field if the architectural direction is different (e.g. all engine-side filters should be expressed as AST transforms instead).\n\nDiscovered during mermaid engine design — see claude-notes/plans/2026-05-28-mermaidjs-engine-design.md gap G1.","status":"open","priority":2,"issue_type":"bug","created_at":"2026-05-28T13:45:45.986759Z","created_by":"cscheid","updated_at":"2026-05-28T13:45:45.986759Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-14rer","depends_on_id":"bd-c6h96","type":"discovered-from","created_at":"2026-05-28T13:45:45.986759Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-15dw","title":"Navbar icon-only item enrichment tie-break","description":"Phase 3 enrichment fills navbar item.text from profile title only when text is None. An item that supplies icon but no text (common for social links) currently stays text-less. Confirmed intentional. Revisit if users ask for auto-text from titles even for icon-only items. See 2026-04-24-websites-phase-3.md §Follow-up beads.","status":"open","priority":4,"issue_type":"task","created_at":"2026-04-24T19:43:02.249186Z","created_by":"cscheid","updated_at":"2026-04-24T19:43:02.249186Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-15dw","depends_on_id":"bd-fqyg","type":"discovered-from","created_at":"2026-04-24T19:43:02.249186Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-18wn","title":"Fix file-locking test failures in quarto-hub on Windows","description":"2 storage tests fail because Windows uses mandatory file locking vs Unix advisory. File::create fails with sharing violation before try_lock_exclusive runs. Fix: use OpenOptions::new().write(true).create(true) instead of File::create, or map sharing violation error to HubAlreadyRunning. Affects test_storage_manager_prevents_double_lock and test_storage_manager_standalone_prevents_double_lock.","status":"open","priority":2,"issue_type":"bug","created_at":"2026-03-20T13:36:06.424470600Z","created_by":"cderv","updated_at":"2026-03-20T13:36:06.424470600Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-195t","title":"Lua attribute-mutation: proxy tables so cb.attr.attributes[k]=v persists","description":"Quarto 2's Lua bridge returns fresh copies of `cb.attr` (and of `cb.attr.attributes`) on every read. In-place mutation like `cb.attr.attributes[\"k\"] = v` — the idiomatic Pandoc-Lua pattern — silently hits an ephemeral copy and is discarded. Surfaced while exercising Phase 3.5's filter-authored spans fixture (04-filter-authored-spans); the workaround there rebuilds the whole Attr with pandoc.Attr(...) and assigns as one value. Before we document the Lua-filter path to syntax highlighting as a user-facing feature, the idiomatic pattern must persist. See claude-notes/plans/2026-04-20-syntax-highlighting-phase-3.5.md 'Follow-up task: Lua attribute-mutation proxy'. Plan: claude-notes/plans/2026-04-21-lua-attr-mutation-proxy.md","status":"open","priority":1,"issue_type":"bug","created_at":"2026-04-21T17:19:19.121444Z","created_by":"cscheid","updated_at":"2026-04-21T17:19:19.121444Z","source_repo":".","compaction_level":0,"original_size":0} @@ -109,6 +110,7 @@ {"id":"bd-55n0g","title":"JSON-OUTPUT-SCHEMA.md describes a stale Q-1-XX numbering scheme that does not match the catalog or src/error.rs","description":"crates/quarto-yaml-validation/JSON-OUTPUT-SCHEMA.md lines 134-152 list Q-1-13 = 'String too short', Q-1-14 = 'String too long', Q-1-16 = 'Number out of range', etc. — but the actual mapping in src/error.rs::ValidationErrorKind::error_code and the catalog assigns those numbers to other kinds (Q-1-13 = ArrayLengthInvalid, Q-1-16 = ObjectPropertyCountInvalid). The file is broadly stale and would mislead anyone reading it. Needs a wholesale rewrite to match the current catalog.\n\nDiscovered while fixing bd-gdzlq (Q-1-20 double-allocation). Only the Q-1-20 → Q-1-29 line was updated in that fix; the rest of the file stays stale until this issue is taken.","status":"open","priority":3,"issue_type":"bug","created_at":"2026-05-22T20:26:45.950001Z","created_by":"cscheid","updated_at":"2026-05-22T20:26:45.950001Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["documentation","error-reporting","yaml"],"dependencies":[{"issue_id":"bd-55n0g","depends_on_id":"bd-gdzlq","type":"discovered-from","created_at":"2026-05-22T20:26:45.950001Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-56b0","title":"Cross-doc dependency channel audit for q2 preview","description":"Enumerate every Quarto feature that creates a cross-document dependency the preview pipeline needs to react to: include shortcodes, listing content globs, bibliography/csl paths, theme SCSS imports, project-scoped resources, _extensions/ Lua filters, and anything else. For each: which DocumentProfile channel encodes it today (if any), which need to be added, which stay manual-refresh-only. Drives Phase B (channels already on DocumentProfile) and informs Phase D (new channels). Until this lands, the always-visible manual force-refresh button (Phase A.6) is the user escape hatch. See claude-notes/plans/2026-05-11-q2-preview-epic.md §Recommended next steps item 3 + Risk 2.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-11T15:40:54.896280Z","created_by":"cscheid","updated_at":"2026-05-11T15:40:54.896280Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-56b0","depends_on_id":"bd-kw93","type":"related","created_at":"2026-05-11T15:40:54.896280Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-57y4","title":"Vendor and integrate quarto-listing.scss with theme-CSS pipeline","description":"Per L3 D5 (decided 2026-05-06), the listing JS pair (list.min.js + quarto-listing.js) shipped via the artifact store as Project-scoped js: artifacts in the L3 phase 7 commit. The third asset, quarto-listing.scss, was deferred because it requires Bootstrap variable wiring + media-breakpoint mixins from the existing theme-CSS pipeline (CompileThemeCssStage / quarto_sass::SassLayer). That's a larger design task than the static JS bundling.\n\nThis issue: integrate the listing SCSS as a SassLayer that gets composed with the website's theme bundle. The Q1 source file lives at external-sources/quarto-cli/src/resources/projects/website/listing/quarto-listing.scss; copy to resources/listing/quarto-listing.scss per the External Sources Policy and add it to the theme bundle when at least one listing is rendered.\n\nWhile the SCSS isn't compiled, listings render with default browser styling — the markup and JS are unchanged. The list / grid / table layouts work but lack the polished card / table styling Q1 ships.\n\nDiscovered while shipping L3 phase 7 (bd-ml8z); see D5 in claude-notes/plans/2026-05-06-listings-L3-resolve-transform.md.\n\nL5 (bd-5vsr) lands the markup that consumes this SCSS; merging bd-57y4 restores Q1 visual parity for category sidebars and per-item category chips.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-06T20:52:15.894830Z","created_by":"cscheid","updated_at":"2026-05-07T13:46:18.157025Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-57y4","depends_on_id":"bd-ml8z","type":"discovered-from","created_at":"2026-05-06T20:52:15.894830Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-5ijtt","title":"User-facing docs for mermaid diagrams","description":"Add a user-facing docs page under docs/ explaining `{mermaid}` code blocks: syntax, what gets rendered, browser-runtime caveats (requires network access to jsdelivr; diagram appears after page load), known limitations (HTML output only in first cut). Render with cargo run --bin q2 -- render docs/ (Q2, not Q1) per CLAUDE.md.\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-28T13:45:22.896182Z","created_by":"cscheid","updated_at":"2026-05-28T13:45:38.414661Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5ijtt","depends_on_id":"bd-gwfdo","type":"blocks","created_at":"2026-05-28T13:45:38.414195Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-5ijtt","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:45:22.896182Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-5qnj","title":"Manage trace size for use as replay/regression-test fixtures","description":"Spun out of bd-45yw (replay engine). Once traces double as replay fixtures (and as user-attached bug-report artifacts), trace size becomes a real constraint — they will be checked into the repo as regression fixtures and posted by users in issues.\n\nCurrent quarto-trace output captures per-stage pipeline state (see crates/quarto-trace/src/lib.rs and crates/quarto-core/src/stage/trace.rs::JsonTraceObserver), which is already on the heavy side for routine diagnostic use.\n\nInvestigate:\n- Where the bulk lives in current traces (per-stage AST snapshots? supporting_files content? something else?)\n- Which content is actually load-bearing for replay vs. diagnostics, and whether the two roles can share one artifact (the design preference noted in bd-45yw's plan)\n- Compression on disk; lazy loading on read\n- Whether per-stage AST snapshots can be elided/diffed when not needed\n- Size budgets — what is reasonable for (a) checked-in CI fixtures and (b) user-attached bug reports\n\nGoal: make 'one trace serves both diagnostic and replay roles' practical. If size cannot be bounded, fall back to two separate artifacts and document why.\n\nReferences:\n- crates/quarto-trace/src/lib.rs (TraceDocument, TraceEntry)\n- crates/quarto-core/src/stage/trace.rs (JsonTraceObserver)\n- claude-notes/plans/2026-05-03-replay-engine.md (open subquestion in Phase 1)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-03T21:11:19.859036Z","created_by":"cscheid","updated_at":"2026-05-03T23:29:05.405253Z","closed_at":"2026-05-03T23:29:05.405068Z","close_reason":"Phases 1, 2, 3, and 5 complete. End-to-end verified on bd-5qnj fixtures: big.qmd 16.3 MB pretty -> 62 KB gzipped+deduped (~265x). Unified-artifact promise realized post-merge with bd-45yw. Real-engine supporting_files re-measurement tracked as bd-sr73.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5qnj","depends_on_id":"bd-45yw","type":"related","created_at":"2026-05-03T21:11:19.859036Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-5vsr","title":"L5 — Categories sidebar","description":"Right-margin category list emitted from L3's resolved item set, grouped by category. Three Q1-parity styles: category-default, category-unnumbered, category-cloud. Templates embedded as doctemplate via MemoryResolver. Click-to-filter interactivity scoped out of v1. See claude-notes/plans/2026-05-05-listings-epic.md §L5.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-05-05T19:53:32.333630Z","created_by":"cscheid","updated_at":"2026-05-07T16:00:41.250061Z","closed_at":"2026-05-07T16:00:41.249924Z","close_reason":"L5 — categories sidebar landed on feature/listings via merge 9e8afa0d (impl 2750546b + follow-on 67a985f4). Per-item chips + right-margin sidebar with three modes (default/unnumbered/cloud); Q-12-11 and Q-12-12 diagnostics; aggregate across multi-listing pages. End-to-end CLI verification recorded in plan §End-to-end CLI verification record. cargo xtask verify clean (Rust + hub-client + WASM). Test count 8570 → 8621 (+51). Discovered-from follow-ups filed: bd-99ru (localize labels), bd-754f (review encoding scheme), bd-ra5j (hub-client browser smoke deferred), bd-nwyp (PandocInlines parser audit).","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5vsr","depends_on_id":"bd-61cd","type":"parent-child","created_at":"2026-05-05T19:53:32.333630Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-5vsr","depends_on_id":"bd-ml8z","type":"blocks","created_at":"2026-05-05T19:53:32.333630Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-5yff4","title":"Sequential multi-engine execution (engine: [a, b, ...])","description":"Investigate and design support for running multiple Quarto 2 execution engines in sequence for a single document.\n\nTwo coupled changes:\n\n1. YAML config: allow 'engine:' to be an ordered array (e.g. engine: [knitr, mermaidjs]) in addition to the current singular string/map forms. Distinct engines only; order is significant (engine N may emit code cells consumed by engine N+1). Array merge across config layers uses the existing default (!concat); no engine-specific tag default for now.\n\n2. Pipeline: thread N engines through EngineExecutionStage. The AST->text->engine->text->AST->reconcile loop already type-checks end-to-end; each subsequent engine starts from the AST after the prior engine's results were reconciled in. Generalize the FileId/intermediate-file slot handling (currently 2 slots: .qmd + one .rmarkdown) to N+1 slots.\n\nTracing/replay/preview redesign: TraceDocument.engine_capture (single Option slot, name-keyed replay) becomes per-engine. Capture one AST snapshot + one EngineCapture per engine invocation within the stage, mirroring the existing transform: sub-entry pattern. Preview record/replay (record_capture, with_replay, preview cache) generalize from one capture to an ordered list.\n\nValidation/testing uses a simple file-backed test engine (reads a results file and splices per-cell outputs in order) so multi-engine sequencing can be exercised deterministically without R/Python/Jupyter, and without committing to a real second engine (e.g. mermaidjs) yet.\n\nPlan: claude-notes/plans/2026-05-27-multi-engine-execution.md\n\nStatus: design/investigation. Implementation gated on user go-ahead after plan review.","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-05-27T14:07:37.941942Z","created_by":"cscheid","updated_at":"2026-05-27T14:33:39.118501Z","source_repo":".","compaction_level":0,"original_size":0} @@ -194,10 +196,12 @@ {"id":"bd-c263g","title":"Author error-docs pages for lua subsystem (1 codes)","description":"Author stub-quality pages for all 1 lua subsystem error codes under docs/errors/lua/. Follow template in docs/errors/yaml/Q-1-10.qmd. See claude-notes/plans/2026-05-22-error-docs-content.md.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-24T21:07:58.103168Z","created_by":"cscheid","updated_at":"2026-05-24T21:15:23.074966Z","closed_at":"2026-05-24T21:15:23.074602Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-c3jh","title":"Phase 9 follow-up: GC stale VFS artifacts at session end","description":"When the hub-client preview re-renders, the WASM Pass-2 produces new artifact paths (theme-css fingerprints change when content changes) but the *old* artifacts under /.quarto/project-artifacts/... linger in VFS storage. The new HTML never references them — so they don't poison the page — but they do leak.\n\nGC pass at session-end (or periodically): walk /.quarto/project-artifacts/, drop any entry whose path doesn't appear in a 'live' set (the union of artifact paths from the most-recent project render).\n\nPhase 9 plan §Risks: 'Add a follow-up to GC /.quarto/project-artifacts/... entries with no live references at session end. Not a Phase-9 blocker.'","status":"open","priority":4,"issue_type":"task","created_at":"2026-04-29T00:32:31.194561Z","created_by":"cscheid","updated_at":"2026-04-29T00:32:31.194561Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-c3jh","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-29T00:32:31.194561Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-c3jh","depends_on_id":"bd-ayj6","type":"discovered-from","created_at":"2026-04-29T00:32:31.194561Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-c5u2g","title":"Cache engine-discovery so we don't re-spawn per document","description":"Replace JupyterEngine::find_executable's sh -c subprocess spawn with which::which (parity with KnitrEngine), and memoize both find_jupyter and find_rscript with OnceLock>. Per-doc engine-registry construction is 37% of main-thread CPU on quarto-web (bd-9eltv profile).\n\nPlan: claude-notes/plans/2026-05-22-engine-discovery-cache.md\nProfile: claude-notes/research/2026-05-21-quarto-web-render-profile.md","status":"closed","priority":1,"issue_type":"task","created_at":"2026-05-22T01:07:27.123486Z","created_by":"cscheid","updated_at":"2026-05-22T01:26:59.991346Z","closed_at":"2026-05-22T01:26:59.991170Z","close_reason":"Implemented and verified — per-process engine-discovery cache. quarto-web wall time 3.28s -> 2.30s (-30%); perf.engine-discover gauge added as regression tripwire.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-c5u2g","depends_on_id":"bd-9eltv","type":"discovered-from","created_at":"2026-05-22T01:07:27.123486Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-c6h96","title":"Mermaid engine design session: resolve architectural questions","description":"Design session to resolve the open questions in claude-notes/plans/2026-05-28-mermaidjs-engine-design.md.\n\n**v2 (post PR #238 review):**\n\n- **Q-A — RESOLVED.** PR #238 lands engine: [knitr, mermaidjs] sequencing. Mermaid is a first-class ExecutionEngine impl. Cell-class ownership disjoint (`{r}` vs `{mermaid}`) so the two compose cleanly. The v1 'AST transform' framing is obsolete.\n\n- **Q-B — sharper.** With engines as first-class citizens, the format-agnostic vs HTML-specific split becomes: does the engine emit RawBlock(HTML) (B1, format-locked), a marker Div + HTML writer special-case (B2a, ugly coupling), a marker Div + dedicated MermaidHtmlEmitStage in the canonical pipeline (B2c, recommended for first ship), or a marker Div + engine-declared per-format AST pass (B2e, requires bd-mqk49)?\n\n- **Q-C — unchanged.** Recommend C1 (inline RawBlock(HTML) for the jsdelivr script tag at end of body, encoded into the engine's markdown output). Survives bd-cp3em (capture-splice aux drop).\n\n- **Q-D — redirected.** PR #238 documents bd-iq0hp ('no browser E2E of multi-engine preview was possible'). Mermaid + knitr is the first real-engine pair with disjoint cell ownership; our preview-e2e task (bd-my0o5) effectively closes bd-iq0hp.\n\nOutput: plan doc has v2 with these decisions recorded. User confirmation of B2c recommendation needed before impl.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-28T13:44:52.754334Z","created_by":"cscheid","updated_at":"2026-05-28T17:09:57.153827Z","closed_at":"2026-05-28T17:09:57.153681Z","close_reason":"Design ratified by user (2026-05-28). All four questions resolved in plan v2.1: Q-A=A1 (MermaidEngine as ExecutionEngine in sequence per PR #238), Q-B=B1 (direct RawBlock HTML emission with bd-mqk49 TODO comment), Q-C=C1 (inline script RawBlock in engine markdown output), Q-D=close PR #238's bd-iq0hp via bd-my0o5 preview e2e. Plan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-c6h96","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:44:52.754334Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-cfl67","title":"q2 render truncates source images referenced in qmd documents","description":"ResourceCollectorTransform stores image source paths as artifacts with empty content; write_artifacts then opens the source path for writing and truncates it to 0 bytes. Confirmed via 'q2 render docs/authoring/markdown/index.qmd' which truncates docs/authoring/markdown/elephant.png from 126124 bytes to 0. Plan doc: claude-notes/plans/2026-05-20-render-truncates-source-images.md","status":"open","priority":0,"issue_type":"bug","created_at":"2026-05-21T00:53:50.747194Z","created_by":"cscheid","updated_at":"2026-05-21T00:53:50.747194Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["bug","data-loss"]} {"id":"bd-cjxlb","title":"Keyring error messages must not leak credential blob","description":"Plan §Phase 5 calls this out as a specifically-flagged risk: when CredentialStore.write/read/clear surfaces a backend error, the error message must NOT contain the credential blob (or any token-shaped substring).\n\nTest name: keyring_error_does_not_leak_blob_in_message in ts-packages/quarto-hub-mcp/test/auth/credential-store.test.ts.\n\nImplementation: wrap keyring errors via redact(err.message) at every site that touches a blob.\n\nPlan §Phase 5: claude-notes/plans/2026-05-05-hub-mcp-device-flow-implementation.md","status":"open","priority":1,"issue_type":"bug","created_at":"2026-05-20T14:27:50.747041Z","created_by":"shikokuchuo","updated_at":"2026-05-20T14:27:50.747041Z","source_repo":"kyoto","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-cjxlb","depends_on_id":"bd-sfr5l","type":"parent-child","created_at":"2026-05-20T14:27:50.747041Z","created_by":"shikokuchuo","metadata":"{}","thread_id":""}]} {"id":"bd-cmp48","title":"hub-mcp Google device-flow auth (Design C')","description":"Implements Design C' (Google as device-flow AS) for hub-mcp ↔ quarto-hub authentication over WebSocket. Hub-mcp persists Google ID + refresh tokens locally (OS keyring); hub keeps existing JWKS validator with audience allowlist.\n\nPlan: claude-notes/plans/2026-05-05-hub-mcp-device-flow-implementation.md\n\nPer the plan's Beads section: one issue per phase (1-10) parent-child to this epic, plus separate bug-tagged issues for the dual-credential CVE test (Phase 2), keyring-leak test (Phase 5), and canonical-URL constant test (Phase 7).","status":"open","priority":1,"issue_type":"epic","created_at":"2026-05-20T14:26:43.587791Z","created_by":"shikokuchuo","updated_at":"2026-05-20T14:26:43.587791Z","source_repo":"kyoto","compaction_level":0,"original_size":0} {"id":"bd-coffj","title":"q2 preview: Div with class='section' emits
, not
— breaks Quarto's :has(+ section) margin rules","description":"The native HTML writer (crates/pampa/src/writers/html.rs:1129-1142) emits
...
for Pandoc Div blocks whose class list contains 'section'; the React Div component in ts-packages/preview-renderer/src/q2-preview/blocks/Div.tsx always emits
. Consequence: Quarto theme CSS rules keyed on the
tag (e.g. 'main.content > p:has(+ section) { margin-bottom: 2rem }') don't apply to preview, causing visible spacing drift. Concrete repro: the 'This is an extremely basic website' paragraph in the fixture has 17px bottom margin in preview vs 34px in render. Fix: mirror the native writer — emit
when classes contains SECTION ('section').","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-05-18T17:53:40.764259Z","created_by":"cscheid","updated_at":"2026-05-18T17:58:07.422417Z","closed_at":"2026-05-18T17:58:07.422276Z","close_reason":"Implementation complete: Pandoc Divs with class='section' now render as
in q2 preview, matching the native HTML writer. Visible result: 'This is an extremely basic website' paragraph margin-bottom now 34px (was 17px), matching q2 render exactly. Quarto theme's main.content > p:has(+ section) selector now matches in preview. 2 new SPA tests; 152/152 green; cargo xtask verify 12/12 green.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-coffj","depends_on_id":"bd-kw93","type":"parent-child","created_at":"2026-05-18T17:53:40.764259Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-cp3em","title":"CaptureSpliceStage drops includes/filters/supporting_files from recorded ExecuteResult","description":"crates/quarto-core/src/stage/stages/capture_splice.rs explicitly only consumes `result.markdown` from the captured ExecuteResult JSON. Comment in the code:\n\n> We don't need the rest of the ExecuteResult shape here (filters, includes, supporting_files) — those are engine-side concerns the splice doesn't reproduce in the AST.\n\n**Verified still present in PR #238 (`feature/multi-engine` branch)** — the new fold-over-Vec path drops aux fields per-iteration, same comment. So this is a multi-engine bug as well as a single-engine bug.\n\nEngines emitting side effects via `ExecuteResult.includes` (e.g. CSS/JS injection) work on `q2 render` but silently fail on `q2 preview`. Same shape as the CodeHighlightStage incident in CLAUDE.md.\n\nFor the mermaid first ship (bd-gwfdo), the plan v2 routes the jsdelivr ') at end of body (C1). Serialize back to QMD; return as ExecuteResult { markdown, ..Default }.\n- **Add a source-code comment** at the RawBlock-emission site pointing at bd-mqk49: 'when engines can declare per-format AST passes, route this through a format-conditional transform instead of emitting HTML inline. Today Quarto 2 only renders HTML so the format-locked emission is acceptable.'\n- Register in native + WASM EngineRegistry.\n\nTests: unit (engine on fixture) + pipeline (full HTML render) + end-to-end per CLAUDE.md verification policy (cargo run --bin q2 -- render fixture.qmd; grep output; record in plan).\n\nThe B2c/B2e (dedicated HTML-emit stage / engine-declared per-format pass) path was rejected for the first ship — Quarto 2 is HTML-only today, so format-agnostic decomposition would be hypothetical correctness against a future cost. The follow-up lives on bd-mqk49.\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","notes":"Topic branch: beads/bd-gwfdo-implement-mermaidengine-b1-direct (commit 93418945). Working tree clean. cargo nextest run --workspace passes (9496 tests). End-to-end verified via cargo run --bin q2 -- render. cargo xtask verify --skip-hub-build in flight (background).","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","updated_at":"2026-05-28T17:43:58.846739Z","closed_at":"2026-05-28T17:43:58.846606Z","close_reason":"Implementation complete (2026-05-28).\n\nMermaidEngine landed on feature/mermaid-engine via merge commit da29062f (topic 93418945).\n\nWhat shipped:\n- crates/quarto-core/src/engine/mermaid.rs — text-level fence scanner, B1 direct HTML emission via ```{=html} raw-block fence, HTML-escaped source, once-per-doc jsdelivr script include.\n- Registered in EngineRegistry::new always-block (native + WASM).\n- 'mermaidjs' added to KNOWN_ENGINES.\n- 13 unit tests in mermaid.rs.\n- 4 integration tests in tests/mermaid_pipeline.rs (single-doc, no-cells, multi-cell, array engine form).\n- bd-mqk49 follow-up comment in code at the HTML emission site.\n\nVerification:\n- cargo nextest run --workspace: 9501 tests pass.\n- cargo xtask verify (full, with WASM): all 12 steps green after npm install. Hub-client build + 79 tests + 61 smoke-all WASM tests pass.\n- End-to-end via real binary: 'cargo run --bin q2 -- render /tmp/mermaid-e2e/test.qmd' produces
 wrappers + jsdelivr script tag in rendered HTML. Recorded in plan.\n\nPhase 2 surprise documented in plan: bare 
/")`
+  at the end of the body, inline in its `markdown` output. Survives
+  the capture-splice path because `result.markdown` is the only
+  field the splice preserves.
+- **C2.** The engine populates `ExecuteResult.includes`
+  (`include_after_body`). Cleaner on native but **silently lost on
+  q2 preview replay** until bd-cp3em is fixed.
+- **C3.** Engine returns the include in `ExecuteResult.includes`
+  *and* we fix bd-cp3em as part of this work. Cleanest. Adds scope.
+
+**Recommendation for the first ship:** C1. Zero new infrastructure;
+correct on both `q2 render` and `q2 preview` from day one;
+debuggable in the AST. C2/C3 are a follow-up after bd-cp3em.
+
+### Q-D. WASM replay coverage
+
+PR #238's bd-iq0hp documents that a browser E2E of *multi-engine*
+preview was not possible at PR time — the default registry uses real
+engines, `FixtureEngine` is test-only, and knitr/jupyter don't compose
+cleanly. **Our preview-e2e task (bd-my0o5) closes bd-iq0hp**: mermaid
++ knitr is the first real-engine pair that composes cleanly because
+cell-class ownership doesn't overlap.
+
+Test matrix:
+
+1. Document with mermaid block only — `q2 preview` renders the
+   diagram.
+2. Document with `engine: [knitr, mermaidjs]`, knitr block + mermaid
+   block, with recorded captures — both render in preview.
+3. Edit the mermaid block — preview updates without re-running
+   knitr's capture (capture invariance on engine 1's input).
+
+## Proposed phased work
+
+### Phase 0 — design ratification (this session)
+
+- [x] v1 plan written
+- [x] PR #238 surfaced; v2 revision applied
+- [x] Decisions locked: A1 (engine impl), B1 (direct RawBlock HTML
+      emission with bd-mqk49 follow-up TODO), C1 (inline script
+      RawBlock), D=bd-iq0hp closure
+- [x] User ratified plan v2.1 (2026-05-28); bd-c6h96 closed
+
+### Phase 1 — multi-engine current-state audit
+
+- [x] Read `claude-notes/plans/2026-05-27-multi-engine-execution.md`
+      (on `feature/multi-engine`) and confirm:
+  - [x] Where `MermaidEngine` would register
+  - [x] The per-engine FileId provenance scheme and mermaid's
+        intermediate name
+  - [x] That `result.markdown` is QMD-text re-parsed for the next
+        engine
+- [x] Re-verify `capture_splice.rs` drops aux fields — confirmed
+      against `feature/multi-engine` (bd-cp3em remains valid)
+- [ ] If PR #238's review surfaces design changes that affect mermaid,
+      reflect them here (deferred until #238 merges or stabilizes)
+
+#### Phase 1 findings (2026-05-28)
+
+Pulled from `feature/multi-engine` (PR #238 head `ed9cbbfe`) and the
+multi-engine plan `claude-notes/plans/2026-05-27-multi-engine-execution.md`.
+
+**F1. Registration site is `EngineRegistry::new`
+(`crates/quarto-core/src/engine/registry.rs:48-66`).** The current
+shape is "always-register markdown; native-only register knitr +
+jupyter":
+
+```rust
+registry.register(Arc::new(MarkdownEngine::new()));
+
+#[cfg(not(target_arch = "wasm32"))]
+{
+    registry.register(Arc::new(KnitrEngine::new()));
+    registry.register(Arc::new(JupyterEngine::new()));
+}
+```
+
+**Mermaid registers in the always-block alongside `MarkdownEngine`.**
+Mermaid is text-only (no subprocess, no R/Python runtime), so the
+same code can run in native + WASM. This is **strictly better than
+the trace-replay route** for q2 preview: the WASM build executes
+mermaid live in-browser, no recorded capture needed, and bd-cp3em's
+aux-field-drop never bites mermaid because mermaid never executes
+on the native side to need replaying. (Native render still uses the
+local engine the same way; both paths converge on the same engine
+code.)
+
+**F2. `KNOWN_ENGINES` const
+(`crates/quarto-core/src/engine/detection.rs:31`)** is
+`&["markdown", "knitr", "jupyter"]`. Used to gate the top-level
+config shortcut (`jupyter: { kernel: python3 }` instead of
+`engine: jupyter`). **Add `"mermaidjs"` to this list** so a
+top-level `mermaidjs: { ... }` is recognized when no `engine:` is
+declared. The `engine: [..., mermaidjs, ...]` path works regardless;
+this is the nice-to-have for the bare top-level form.
+
+**F3. Per-engine FileId provenance is per-loop-iteration in the
+stage, not per-engine.** `EngineExecutionStage::run` walks each
+engine in `to_run` and appends one intermediate slot per executed
+engine to `merged_context`. The intermediate filename is
+`..rmarkdown` (per the multi-engine plan §2). **Mermaid
+gets `.mermaidjs.rmarkdown` automatically** — no engine-side
+participation required. `MermaidEngine::execute` only needs to
+return `ExecuteResult { markdown, ..Default }`; the loop does the
+rest.
+
+**F4. `result.markdown` is QMD text re-parsed for the next engine.**
+Confirmed at `engine_execution.rs:255` (multi-engine version):
+
+```rust
+let (qmd, qmd_source_info) = serialize_ast_to_qmd(&ast)?;
+// ...
+let mut result = engine.execute(&qmd, &exec_context)?;
+// later: parse result.markdown as the next iteration's input
+```
+
+So mermaid emits QMD text. Literal HTML in QMD (e.g.
+`
` on its own lines, blank-separated) +parses as `RawBlock(HTML, …)` via pampa's QMD reader (Pandoc +convention). No need to emit `\`\`\`{=html}` raw-block fences +unless we hit an edge case during impl. + +**F5. In-process engine convention is *text-level* fence scanning, +not AST parse-walk-serialize.** The biggest finding of the audit. +`FixtureEngine` (`crates/quarto-core/src/engine/fixture.rs:120-250`, +new in PR #238) is the only pure-Rust engine on the multi-engine +branch and it works text-level: a hand-rolled fence scanner finds +`{name}` cells and splices replacement text in. **Mermaid should +mirror this** rather than go through pampa's parser: + +- Simpler. ~100 lines of text-walking vs. AST manipulation + + `serialize_ast_to_qmd` (which is private to `engine_execution.rs`). +- Cheaper. No round-trip through the parser, no AST allocation. +- Less coupled. The mermaid engine never touches pampa internals or + Pandoc AST types; only `ExecuteResult`/`ExecutionContext`/`ExecutionError`. +- Matches the multi-engine plan's design pattern. Future graphviz/ + plantuml/dot engines would all follow the same template. + +The cell shape mermaid matches is exactly the FixtureEngine pattern: +opening fence `` ```{mermaid} `` ... source text ... closing fence +`` ``` ``. Replacement text is the literal HTML for the `
`
+wrapper, plus (once per document) the jsdelivr `
+```
+
+This output was inspected at the file system; the rendered HTML is
+exactly the markup the user's hub-client browser session would
+receive, and the mermaid runtime would pick up both diagrams at
+page load.
+
+### Phase 3 — q2-preview verification (closes bd-iq0hp)
+
+- [x] Single-doc `engine: mermaidjs` fixture renders both diagrams as
+      `` in the preview iframe (2026-05-29 via Chrome DevTools
+      MCP). `data-processed="true"` on both `pre.mermaid`,
+      `svgInsidePre=2`, console clean. Screenshots:
+      - `claude-notes/plans/bd-my0o5-preview-no-render.png` (before
+        the fixes — diagrams appear as raw source text)
+      - `claude-notes/plans/bd-my0o5-preview-renders.png` (after the
+        fixes — diagrams render as SVG)
+- [ ] Multi-engine `engine: [knitr, mermaidjs]` in preview — deferred
+      (knitr requires R runtime; uses the same code path; the two
+      parity fixes below are sufficient for either engine's scripts).
+
+#### Phase 3 surprise: two compounding parity gaps
+
+The first browser run made the diagrams not render. Investigation
+surfaced two distinct gaps between static `q2 render` and `q2 preview`
+that both had to be fixed:
+
+**Gap P1 — React's `dangerouslySetInnerHTML` does not execute inline
+`
`.
+
+## Beads issues
+
+Created 2026-05-28 (v1), revised 2026-05-28 (v2 after PR #238 review):
+
+- **`bd-je48v`** (epic, P2) — "Add mermaid diagram engine to Quarto 2."
+- **`bd-c6h96`** (task, P2, child of epic) — "Mermaid engine design
+  session." v2: Q-A resolved (engine sequence via PR #238); Q-B
+  sharper (B1 vs B2a/B2c/B2e); Q-C unchanged (C1 recommended); Q-D
+  redirected at closing bd-iq0hp.
+- **`bd-fztki`** (task, P2, child of epic, blocks impl) — "Audit
+  current multi-engine state." v2: retargeted at PR #238's plan +
+  code instead of a generic multi-engine investigation.
+- **`bd-gwfdo`** (task, P2, child of epic, blocked-by `bd-c6h96` +
+  `bd-fztki`, gated on PR #238 merge) — "Implement `MermaidEngine`."
+  v2: revised from "AST transform" to "ExecutionEngine impl emitting
+  RawBlock HTML directly (B1)." A bd-mqk49 follow-up will refactor
+  to format-conditional emission once the engine→stage extension
+  API exists.
+- **`bd-my0o5`** (task, P2, child of epic, blocked-by `bd-gwfdo`) —
+  "q2 preview end-to-end verification for mermaid blocks." v2:
+  explicitly closes PR #238's bd-iq0hp.
+- **`bd-5ijtt`** (task, P3, child of epic, blocked-by `bd-gwfdo`) —
+  "User-facing docs for mermaid diagrams."
+
+Follow-up issues (discovered-from `bd-c6h96`):
+
+- **`bd-14rer`** (bug, P2) — Knitr's `filters` silently dropped.
+- **`bd-s8llm`** (feature, P3) — VFS publication API.
+- **`bd-mqk49`** (feature, P3) — Engine→stage extension. **More
+  relevant after PR #238**; the natural mechanism for Q-B's full
+  B2e form. The mermaid engine ships under B1 (direct
+  `RawBlock(HTML, …)` emission); when bd-mqk49 lands, the mermaid
+  engine should be refactored to declare a per-format AST pass
+  instead. A source-code comment in `engine/mermaid/` flags the
+  TODO.
+- **`bd-cp3em`** (bug, P2) — Capture-splice aux-field drop;
+  verified still present in `feature/multi-engine`.
+
+## References
+
+- ExecutionEngine trait: `crates/quarto-core/src/engine/traits.rs:56-127`
+- ExecuteResult: `crates/quarto-core/src/engine/context.rs:127-206`
+- EngineExecutionStage (single-engine, on `main`):
+  `crates/quarto-core/src/stage/stages/engine_execution.rs:150-401`
+- EngineExecutionStage (multi-engine, on `feature/multi-engine`):
+  per PR #238 file list (+625/-232)
+- Capture-splice aux-field drop (on `feature/multi-engine`):
+  `crates/quarto-core/src/stage/stages/capture_splice.rs` —
+  comment unchanged from `main` version
+- Pipeline stages: `crates/quarto-core/src/pipeline.rs`
+- Document profile contract: `claude-notes/designs/document-profile-contract.md`
+- Filter resolution: `crates/quarto-core/src/filter_resolve.rs`
+- VFS: `crates/quarto-system-runtime/src/wasm.rs:227-261`
+- q2-preview epic: `claude-notes/plans/2026-05-11-q2-preview-epic.md`
+- Multi-engine plan (on `feature/multi-engine`):
+  `claude-notes/plans/2026-05-27-multi-engine-execution.md`
+- Multi-engine PR: https://github.com/quarto-dev/q2/pull/238
diff --git a/claude-notes/plans/bd-my0o5-preview-no-render.png b/claude-notes/plans/bd-my0o5-preview-no-render.png
new file mode 100644
index 00000000..731f3d21
Binary files /dev/null and b/claude-notes/plans/bd-my0o5-preview-no-render.png differ
diff --git a/claude-notes/plans/bd-my0o5-preview-renders.png b/claude-notes/plans/bd-my0o5-preview-renders.png
new file mode 100644
index 00000000..d7d002e2
Binary files /dev/null and b/claude-notes/plans/bd-my0o5-preview-renders.png differ
diff --git a/crates/quarto-core/src/engine/detection.rs b/crates/quarto-core/src/engine/detection.rs
index b62f2188..00ad434f 100644
--- a/crates/quarto-core/src/engine/detection.rs
+++ b/crates/quarto-core/src/engine/detection.rs
@@ -28,7 +28,7 @@
 use quarto_pandoc_types::ConfigValue;
 
 /// Known execution engine names.
-pub const KNOWN_ENGINES: &[&str] = &["markdown", "knitr", "jupyter"];
+pub const KNOWN_ENGINES: &[&str] = &["markdown", "mermaidjs", "knitr", "jupyter"];
 
 /// Result of engine detection.
 ///
diff --git a/crates/quarto-core/src/engine/mermaid.rs b/crates/quarto-core/src/engine/mermaid.rs
new file mode 100644
index 00000000..bc758101
--- /dev/null
+++ b/crates/quarto-core/src/engine/mermaid.rs
@@ -0,0 +1,507 @@
+/*
+ * engine/mermaid.rs
+ * Copyright (c) 2026 Posit, PBC
+ *
+ * Mermaid diagram engine: rewrites `{mermaid}` code cells into the
+ * `
` markup that the mermaid.js browser runtime
+ * picks up at page load.
+ */
+
+//! Mermaid diagram engine — direct HTML emission (B1).
+//!
+//! [`MermaidEngine`] is a pure-Rust, in-process [`ExecutionEngine`]
+//! that rewrites Quarto's `{mermaid}` code cells into raw HTML the
+//! browser-side `mermaid.js` runtime can pick up. It does **no** server-
+//! side diagram rendering: no SVG generation, no PNG export, no
+//! subprocess. The actual diagram drawing happens in the browser when
+//! `mermaid.initialize({ startOnLoad: true })` runs.
+//!
+//! # What it does
+//!
+//! 1. Scans the input QMD for executable code cells whose info string
+//!    is exactly `{mermaid}` — i.e. fences of the form
+//!    ```` ```{mermaid} ````. (pampa keeps the braces in the class
+//!    name, matching the `{r}` / `{python}` / `{fixture-…}` shape.)
+//! 2. Rewrites each matched cell as a raw HTML block
+//!    `
…HTML-escaped source…
`. The browser +//! then reads the `textContent` of the element (entity-decoding the +//! escapes) and hands it to `mermaid.render`. +//! 3. If at least one mermaid cell was found, appends a single +//! ` +```"; + +/// Mermaid diagram engine — emits browser-runtime markup for `{mermaid}` +/// code cells. See module docs. +/// +/// # Characteristics +/// +/// - Always available (no external dependencies). +/// - Available in both native and WASM builds (no subprocess work). +/// - Does not support freeze/thaw (output is a deterministic function +/// of input — caching the trace via `engine: replay` would be silly). +/// - Produces no intermediate files. +#[derive(Debug, Clone, Default)] +pub struct MermaidEngine; + +impl MermaidEngine { + /// Create a new mermaid engine instance. + pub fn new() -> Self { + Self + } +} + +impl ExecutionEngine for MermaidEngine { + fn name(&self) -> &str { + "mermaidjs" + } + + fn execute( + &self, + input: &str, + _ctx: &ExecutionContext, + ) -> Result { + let output = render_mermaid_cells(input) + .map_err(|msg| ExecutionError::execution_failed(self.name(), msg))?; + Ok(ExecuteResult::new(output)) + } + + fn is_available(&self) -> bool { + true + } +} + +/// Rewrite each ```` ```{mermaid} ```` executable cell in `input` as a +/// raw HTML `
` block. If any cell was +/// matched, append the once-per-doc jsdelivr `'], + }; + + const { container } = render(renderBlock(rawBlock, 0, '', () => {})); + + const scripts = container.querySelectorAll('script'); + expect(scripts.length).toBe(1); + // The text body and id attribute are copied onto the + // replacement script; the replacement is what's in the DOM + // now (the original was removed by `replaceWith`). + expect(scripts[0].getAttribute('id')).toBe('orig-marker'); + expect(scripts[0].textContent).toContain('inline body'); + }); + + it('copies script attributes (e.g. type="module") onto the recreated tag', () => { + // We cannot easily exercise an actual ES-module load in jsdom, + // but we can assert the recreated script keeps the type + // attribute — which is what makes the browser parse the source + // as a module rather than a classic script. + const rawBlock: RawBlock = { + t: 'RawBlock', + c: ['html', ''], + }; + + const { container } = render(renderBlock(rawBlock, 0, '', () => {})); + + const script = container.querySelector('script'); + expect(script).not.toBeNull(); + expect(script!.getAttribute('type')).toBe('module'); + expect(script!.hasAttribute('data-mermaid-init')).toBe(true); + }); + + it('passes through non-html raw blocks as null (no behaviour change)', () => { + const rawBlock: RawBlock = { t: 'RawBlock', c: ['latex', '\\section{Foo}'] }; + const result = renderBlock(rawBlock, 0, '', () => {}); + expect(result).toBeNull(); + }); +}); diff --git a/hub-client/src/components/render/ReactAstSlideRenderer.tsx b/hub-client/src/components/render/ReactAstSlideRenderer.tsx index 59cb29fd..d0bddd24 100644 --- a/hub-client/src/components/render/ReactAstSlideRenderer.tsx +++ b/hub-client/src/components/render/ReactAstSlideRenderer.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { AspectRatioScaler } from '../render/AspectRatioScaler'; import katex from 'katex'; import 'katex/dist/katex.min.css'; @@ -484,6 +484,73 @@ function parseStyleString(styleString: string): React.CSSProperties { // BlockNode Rendering // ============================================================================ +/** + * Render a Pandoc `RawBlock(html, …)` so embedded `'); + const { container } = render(); + + const scripts = container.querySelectorAll('script'); + expect(scripts.length).toBe(1); + // Attributes and body were copied onto the replacement. + expect(scripts[0].getAttribute('id')).toBe('orig-marker'); + expect(scripts[0].getAttribute('type')).toBe('module'); + expect(scripts[0].textContent).toContain('module body'); + }); + + it('preserves the mermaid engine\'s emission shape end-to-end through the renderer', () => { + // Verbatim of what MermaidEngine emits after a render cycle: + // a
 for each cell and one
+        // ',
+        ].join('\n');
+        const node = rb('html', html);
+        const { container } = render();
+
+        expect(container.querySelectorAll('pre.mermaid').length).toBe(1);
+        const script = container.querySelector('script[type="module"]');
+        expect(script).not.toBeNull();
+        // The recreated script must still carry the import; this is
+        // what a real browser would execute and that would set
+        // `window.mermaid` and run the diagram render.
+        expect(script!.textContent).toContain('mermaid.esm.min.mjs');
+        expect(script!.textContent).toContain('startOnLoad: true');
+    });
+
+    it('passes through non-html raw blocks as 
', () => {
+        const node = rb('latex', '\\section{Foo}');
+        const { container } = render();
+        const pre = container.querySelector('pre');
+        expect(pre).not.toBeNull();
+        expect(pre!.textContent).toContain('\\section{Foo}');
+    });
+});
diff --git a/ts-packages/preview-renderer/src/q2-preview/blocks/RawBlock.tsx b/ts-packages/preview-renderer/src/q2-preview/blocks/RawBlock.tsx
index f361e96a..c603ee59 100644
--- a/ts-packages/preview-renderer/src/q2-preview/blocks/RawBlock.tsx
+++ b/ts-packages/preview-renderer/src/q2-preview/blocks/RawBlock.tsx
@@ -1,9 +1,12 @@
+import { useEffect, useRef } from 'react';
 import type { NodeArgs, RawBlock as RawBlockType } from '../../framework';
 
 /**
  * RawBlock semantics:
  *  - format === 'html' (or 'html5'): inject raw HTML via
- *    `dangerouslySetInnerHTML` so users can embed exact markup.
+ *    `dangerouslySetInnerHTML` so users can embed exact markup. After
+ *    mount we re-execute any `