Skip to content

Process mermaid %%| cell options: captions, cross-references, alt text (bd-mermaid-cell-options-9wo3crl0) - #499

Merged
cscheid merged 10 commits into
mainfrom
feature/bd-mermaid-cell-options-9wo3crl0
Aug 10, 2026
Merged

Process mermaid %%| cell options: captions, cross-references, alt text (bd-mermaid-cell-options-9wo3crl0)#499
cscheid merged 10 commits into
mainfrom
feature/bd-mermaid-cell-options-9wo3crl0

Conversation

@cscheid

@cscheid cscheid commented Aug 10, 2026

Copy link
Copy Markdown
Member

Processes mermaid %%| cell options, so fig-cap / fig-alt on a
```mermaid fence reach the output instead of sitting verbatim in the
diagram source (bd-mermaid-cell-options-9wo3crl0).

Plan, design rationale and full verification record:
claude-notes/plans/2026-08-10-mermaid-cell-options.md.

The finding that shaped this

The caption / figure / crossref machinery already worked for a mermaid
fence — it just keyed off the #| marker. At the previous HEAD, a
```mermaid block with #| label: + #| fig-cap: rendered a full
quarto-float figure with numbering and aria-describedby; only the %%|
spelling was unrecognized. cell_options::comment_syntax_for had no mermaid
entry, because Q1 keeps mermaid's %% on the language handler and in
constants.lua rather than in kLangCommentChars, which is what that table
was ported from.

The architectural constraint: this has to happen in PreEngineSugaringStage,
not in transforms/mermaid.rs. That transform is on
Q2_PREVIEW_TRANSFORM_EXCLUDED (so preview would diverge) and runs after
crossref-render (so a label: could never be numbered). Doing it pre-engine
gives preview/render parity for free.

What changed, commit by commit

cell_options learns mermaid's %% with a cross-talk test against matlab/tikz's single %
shorthand migrated onto cell_options language-aware marker + real YAML parsing, replacing a private `#
fig-cap parsed as markdown inlines via the same entry point document metadata uses
unlabelled fig-capBlock::Figure fig-scapCaption::short
fig-alt → mermaid's accDescr: and stop consuming options we cannot route
Q-2-47 / Q-2-48 diagnostics source-mapped to the offending key
preview-parity test, docs, worked example

Three pre-existing defects in the shared #| shorthand are fixed along the
way — all reproduced on a plain ```python cell first, so they were
never mermaid-specific:

  • bd-5jcmmj1ffig-cap: "A caption." rendered as Figure 1: "A caption."
    with literal quote characters.
  • bd-sdpp9rw4fig-cap: A *strong* claim rendered the asterisks
    literally.
  • bd-il6pxq4ffig-alt and fig-scap were consumed (stripped from the
    body) but never read back out; the text vanished with no warning. Authors who
    wrote fig-alt believed they had an accessible description and did not.

Behavior changes worth reviewing

#| in a mermaid fence is no longer a cell-option marker. Deliberate
(Q1 parity; q2 is in 0.* and accepting both would teach that #| is
universal), but it is a visible break: # is not a mermaid comment, so
leftover lines become diagram source and mermaid fails on them. Q-2-48 reports
this at the exact source line rather than leaving a silently broken diagram.

A diagram cell warns about options it cannot act on (Q-2-47), scoped to
diagram cells only. On an executable cell an unrecognized key is normally an
engine option (echo, eval, …), so warning there would fire on essentially
every real document. Q1 accepts theme and mermaid-format on mermaid cells
and q2 does not yet, so those warn today — DIAGRAM_OPTION_KEYS carries a note
to add theme when the mermaid theming strands land.

fig-alt on a non-diagram cell is now left in the body for the engine
instead of being consumed. Strictly better than the previous silent drop, and
for jupyter/knitr fig-alt is a real engine option.

Verification

cargo xtask verify (full, including the hub/WASM leg) passes on the rebased
base. No snapshot files changed anywhere in the workspace.

End-to-end through the binary, output inspected — e.g. a %%| fig-cap +
%%| fig-alt diagram with no label now emits:

<div class="quarto-figure quarto-figure-center">
<figure>
<pre class="mermaid">
flowchart LR
  accDescr: Two nodes connected by an arrow.
  E --&gt; F
</pre>
<figcaption>
A tiny flowchart.
</figcaption>
</figure>
</div>

The accessibility claim was verified against real mermaid 11.12.0 (the
pinned version) under jsdom, because both browser routes were unavailable in
the session: the injected line becomes <desc> inside the SVG with the SVG's
aria-describedby pointing at it, and a negative control confirms mermaid
rejects accDescr: placed before the diagram-type line — which is why the
directive is inserted after the first declaration line rather than at the top.

Not verified: presentation in an actual screen reader, and the q2 preview UI
in a live browser session.

Note for reviewers

main claimed Q-2-42 … Q-2-46 while this branch was in flight (#497), so the
two codes here were renumbered to Q-2-47 / Q-2-48 during the rebase. The
Phase 6 commit message still names the pre-rebase numbers; code, catalog,
tests and plan all use the final ones.

Follow-ups filed

  • bd-vg8p5yojqmd-syntax-helper rule rewriting ```{mermaid}
    ```mermaid. This is the other half of the Connect-docs story:
    all 33 diagrams there are brace-form and still render as source text until
    it lands. Cell options need no rewrite — Q1 documents already write %%|.
  • bd-e3m3rkik — pre-existing, surfaced while reading the preview AST:
    because mermaid-render is excluded from preview, CodeBlockRenderTransform
    wraps mermaid blocks in copy-button chrome there that q2 render suppresses
    by transform ordering.

🤖 Generated with Claude Code

cscheid and others added 10 commits August 10, 2026 17:51
The caption/figure/crossref path already works for mermaid fences — it
just keys off `#|` instead of mermaid's `%%|`. Probes at HEAD show a
```mermaid block with `#| label:` + `#| fig-cap:` renders a full
quarto-float figure with figcaption and aria-describedby, while the
`%%|` spelling passes through verbatim.

Also records three pre-existing defects in the shared `#|` shorthand
(quoted YAML values keep their quotes, markdown captions aren't parsed,
`fig-alt`/`fig-scap` are consumed and dropped), reproduced on a plain
python cell so they're established as general, not mermaid-specific.

No source changes — plan skeleton plus probe fixtures only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six questions answered: Block::Figure for unlabelled fig-cap (possible
because we work on the AST, unlike Q1's textual handling); accDescr: for
fig-alt, with a note that an alt-carrying element becomes the right
answer once diagrams are rendered server-side for PDF; D1/D2/D3 fixed
here one commit at a time; unknown keys warn with a source-mapped
diagnostic, scoped to diagram cells so engine options keep passing
through; %%| only for mermaid, with a diagnostic when #| appears.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-cell-options-9wo3crl0)

comment_syntax_for had no mermaid entry, so mermaid cells fell through
to the `#` default and `%%|` option lines were never recognized. Q1
keeps mermaid's `%%` on the language handler and in constants.lua
rather than in kLangCommentChars, which is why the port of that table
missed it.

Adds a cross-talk test pinning mermaid's `%%` against matlab/tikz's
single `%`: neither marker half-parses under the other's syntax.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… (bd-mermaid-cell-options-9wo3crl0)

Migrates codeblock_shorthand off its private `#|`-only, split_once(':')
matcher onto the shared cell_options facility, which selects the marker
from the cell's language and parses the block as real YAML.

Two consequences:

- ```mermaid now takes `%%|`, so a labelled diagram desugars into the
  crossref Div scaffold and picks up the figure/figcaption/numbering
  path that already existed. `#|` in a mermaid fence is deliberately no
  longer a cell-option marker (decision 5 in the plan) — `#` is not a
  mermaid comment, so accepting it would teach that `#|` is universal.

- Quoted YAML values arrive unquoted, fixing bd-5jcmmj1f: `fig-cap: "A
  caption."` no longer renders as `Figure 1: "A caption."` with literal
  quote characters. This affects every `#|` cell, not just mermaid.

Consumed lines are still stripped textually rather than re-serialized,
so unconsumed options reach the engine byte-for-byte as written.

desugar_blocks now takes the document's SourceContext, used to anchor
each cell body inside its fenced block. CodeBlock::source_info spans the
whole fence, so the body is located within it by search; when no
contiguous match exists (a fence inside a blockquote has its `> `
continuations stripped from `text`) it falls back to the block's own
span rather than binding an assumed offset to real content.

No snapshot files changed. Full workspace suite green: 11343 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
caption_paragraph built the caption as a single Inline::Str, so
`fig-cap: A *strong* claim, see [the docs](url)` reached the figcaption
with literal asterisks and brackets. Captions are markdown — this
routes them through parse_config_string_as_markdown, the same entry
point document metadata uses, anchored at the option value's own span
so inline positions resolve into the option line.

desugar_blocks now takes a diagnostics sink rather than discarding the
Q-1-20 warning that entry point emits when a caption doesn't parse.

No snapshot files changed. Full workspace suite green: 11344 passed.

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

A `fig-cap` with no `label:` used to do nothing at all — the desugar
returned early when no crossref label was present. It now builds a
plain Block::Figure, which the HTML writer renders as
<figure>…<figcaption> with no number and no float scaffolding.

Constructing the node directly is the AST-level answer Q1 could not
reach: its cell handling was textual, so it emitted markdown and let a
filter rebuild the structure.

`fig-scap` now populates Caption::short instead of being consumed and
dropped — part of bd-il6pxq4f. Only fig-cap/fig-scap are honoured on
the unlabelled path: with no label there is no ref-type to derive a
category from.

A `label:` that is not a crossref still leaves the block untouched (the
author is naming the cell for the engine).

End-to-end, `q2 render claude-notes/plans/mermaid-cell-options-investigation/probe.qmd`,
output inspected:

  B1 (```mermaid + `%%| fig-cap`, no label) now emits
      <div class="quarto-figure quarto-figure-center"><figure>
      <pre class="mermaid">…</pre>
      <figcaption>A tiny flowchart.</figcaption></figure></div>

  B2 (```mermaid + `%%| label: fig-diagram` + `%%| fig-cap`) emits the
      numbered float — <figcaption id="fig-diagram-caption">Figure 1: A
      labelled flowchart.</figcaption> — and `@fig-diagram` resolves to
      a live "Figure 1" link instead of the previous `?fig-diagram?`
      unresolved-crossref warning.

`%%| fig-alt` is still passed through into the diagram source; Phase 5
routes it.

No snapshot files changed. Full workspace suite green: 11347 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… can't route (bd-il6pxq4f)

partition_options classified `<reftype>-alt` and `<reftype>-scap` as
consumed — stripping them from the body — but only `<reftype>-cap` was
ever read back out. The text vanished with no warning. That is an
accessibility defect: authors who wrote fig-alt believed they had an
accessible description and did not.

Two changes:

- On a mermaid cell, `fig-alt` is injected as mermaid's own `accDescr:`
  directive, placed after the diagram-type line (mermaid rejects it
  before). mermaid.js swaps the <pre> for an inline <svg> at runtime, so
  an attribute on the <pre> would not reach assistive tech; accDescr
  becomes the SVG's description and survives. The text is folded onto
  one line — a newline terminates the single-line form, and the
  multi-line `accDescr { … }` form has no escape for a `}`.

- Everywhere else, an option q2 cannot route is left in the body for the
  engine rather than consumed. For a jupyter/knitr cell that is strictly
  better than before: fig-alt is a real engine option there.

The consumed/wrapper decision is now computed once, so fig-alt composes
with both caption paths and a lone fig-alt on a diagram is itself reason
to rewrite the cell.

Recorded in the code for the future: once diagrams are rendered
server-side for PDF/print there is a real image element, and the
accessible name belongs in its `alt` rather than in the diagram source.

End-to-end, `q2 render …/probe.qmd`, output inspected — section B1
(```mermaid with `%%| fig-cap` + `%%| fig-alt`, no label) now emits:

  <div class="quarto-figure quarto-figure-center">
  <figure>
  <pre class="mermaid">
  flowchart LR
    accDescr: Two nodes connected by an arrow.
    E --&gt; F
  </pre>
  <figcaption>
  A tiny flowchart.
  </figcaption>
  </figure>
  </div>

No snapshot files changed. Full workspace suite green: 11351 passed.
(One quarto-hub admin-lifecycle test flaked on an intermediate run and
passed both in isolation and on a clean re-run; unrelated to this work.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…id-cell-options-9wo3crl0)

Two new catalog codes:

- Q-2-42, "Cell option ignored on a diagram cell". A diagram cell has no
  execution engine to hand leftover options to, so an option nothing
  acted on is a mistake. Scoped to diagram cells on purpose: on an
  executable cell an unrecognized key is normally an engine option
  (echo, eval, warning), and warning there would fire on essentially
  every real document. The message distinguishes an unknown key from a
  recognized key that has nowhere to go in this position (`fig-scap` on
  a numbered float).

- Q-2-43, "Wrong cell-option marker for a diagram cell". `#|` used to
  work in a mermaid fence and deliberately no longer does; since `#` is
  not a mermaid comment, the leftover lines become diagram source and
  mermaid fails to parse them. Without this the author would see a
  broken diagram and no explanation.

Going through cell_options' YAML parse is what makes these precise: each
option carries its key's span, so the caret lands on the key.

End-to-end, `q2 render …/probe4.qmd`, output inspected:

  Warning: [Q-2-42] Cell option ignored on a diagram cell
     ╭─[ …/probe4.qmd:9:5 ]
   9 │ %%| echo: false
     │     ──┬─
     │       ╰─── `echo` is not a cell option a diagram cell understands.
  ℹ Diagram cells accept `label`, `fig-cap`, `fig-scap`, and `fig-alt`. …

  …:20:5 flags `fig-scap` as recognized-but-ineffective, and the
  ```{python} cell in the same document emits nothing.

`q2 render …/probe.qmd` reports Q-2-43 at probe.qmd:28:1 — the exact
line of the `#|` run — confirming the body-source derivation resolves
against the real .qmd, not just synthetic test spans.

Q1 accepts `theme` and `mermaid-format` on mermaid cells and q2 does
not, so they warn today; DIAGRAM_OPTION_KEYS carries a note to add
`theme` when the mermaid theming strands land.

Full workspace suite green: 11356 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l-options-9wo3crl0)

Preview parity is pinned by a new test on render_qmd_to_preview_ast:
PreEngineSugaringStage is a *stage* while Q2_PREVIEW_TRANSFORM_EXCLUDED
only filters *transforms*, so the preview AST carries the same Figure
wrapper, caption inlines, injected accDescr and stripped options that
`q2 render` emits — while the diagram still arrives as a CodeBlock for
MermaidCodeBlock.tsx. That last part is the intended difference between
the surfaces; the test pins it so neither list can drift silently.

Docs gain a "Captions, cross-references, and alt text" section, and the
"Differences from Quarto 1" list drops captions/labels/cross-references
(now supported) while keeping the brace-form note, with migration
guidance. New worked example examples/diagrams/03-mermaid-captions,
registered in examples/manifest.yml.

Also removes a .quarto/render-manifest.json that an earlier `git add -A`
committed under the investigation directory, and gitignores probe render
output there.

Filed while verifying:
- bd-vg8p5yoj — qmd-syntax-helper rule to rewrite ```{mermaid} to
  ```mermaid (the migration half of this strand).
- bd-e3m3rkik — pre-existing: q2 preview gives mermaid blocks copy-button
  chrome that q2 render suppresses by transform ordering.

The accessibility claim was verified against real mermaid 11.12.0 under
jsdom (both browser routes were unavailable this session): the injected
line becomes <desc> inside the SVG with aria-describedby pointing at it,
and a negative control confirms mermaid rejects accDescr placed before
the diagram-type line. Not verified: presentation in an actual screen
reader, and the q2 preview UI in a live browser. Full record in the plan.

`cargo xtask verify` (full, including the hub/WASM leg): all steps passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main claimed Q-2-42 through Q-2-46 while this branch was in flight (PR
#497: conditional-content attributes, callout titles, reference-style
links). The catalog conflict was resolved by keeping main's entries and
appending the renumbered pair; the plan records the collision and what
it costs to chase a renumber through call sites, tests and prose.

The Phase 6 commit message still names the pre-rebase numbers. Code,
catalog, tests and plan all use the final ones.

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

posit-snyk-bot commented Aug 10, 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
cscheid merged commit a429bec into main Aug 10, 2026
8 checks passed
@cscheid
cscheid deleted the feature/bd-mermaid-cell-options-9wo3crl0 branch August 10, 2026 23:14
@cscheid cscheid mentioned this pull request Aug 11, 2026
cscheid added a commit that referenced this pull request Aug 11, 2026
The diagnostics added in PR #499 set docs_url values pointing at
quarto.org/docs/errors/markdown/Q-2-47 and /Q-2-48, but the pages were
never written — every emitted warning linked to a 404.

Q-2-47 explains both halves of the message (an option Quarto does not
know versus a real option that does not apply in this position) and why
executable cells are deliberately not warned about. Q-2-48 explains that
the option marker tracks the cell language, and that `#|` in a mermaid
fence used to be accepted and no longer is.

Nothing enforces this coverage — main's Q-2-42 is missing a page for the
same reason — so it is worth a lint rule. Filed as bd-u2qj4y29.

`cargo xtask verify` (full) passes; `q2 render docs/` renders 201 of 201
files with both pages' cross-links resolving.

Co-Authored-By: Claude Opus 5 (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.

2 participants