Process mermaid %%| cell options: captions, cross-references, alt text (bd-mermaid-cell-options-9wo3crl0) - #499
Merged
Conversation
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 --> 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>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Merged
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Processes mermaid
%%|cell options, sofig-cap/fig-alton a```mermaidfence reach the output instead of sitting verbatim in thediagram 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```mermaidblock with#| label:+#| fig-cap:rendered a fullquarto-floatfigure with numbering andaria-describedby; only the%%|spelling was unrecognized.
cell_options::comment_syntax_forhad no mermaidentry, because Q1 keeps mermaid's
%%on the language handler and inconstants.luarather than inkLangCommentChars, which is what that tablewas ported from.
The architectural constraint: this has to happen in
PreEngineSugaringStage,not in
transforms/mermaid.rs. That transform is onQ2_PREVIEW_TRANSFORM_EXCLUDED(so preview would diverge) and runs aftercrossref-render(so alabel:could never be numbered). Doing it pre-enginegives preview/render parity for free.
What changed, commit by commit
cell_optionslearns mermaid's%%%cell_optionsfig-capparsed as markdown inlinesfig-cap→Block::Figurefig-scap→Caption::shortfig-alt→ mermaid'saccDescr:Three pre-existing defects in the shared
#|shorthand are fixed along theway — all reproduced on a plain
```pythoncell first, so they werenever mermaid-specific:
fig-cap: "A caption."rendered asFigure 1: "A caption."with literal quote characters.
fig-cap: A *strong* claimrendered the asterisksliterally.
fig-altandfig-scapwere consumed (stripped from thebody) but never read back out; the text vanished with no warning. Authors who
wrote
fig-altbelieved 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#|isuniversal), but it is a visible break:
#is not a mermaid comment, soleftover 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 essentiallyevery real document. Q1 accepts
themeandmermaid-formaton mermaid cellsand q2 does not yet, so those warn today —
DIAGRAM_OPTION_KEYScarries a noteto add
themewhen the mermaid theming strands land.fig-alton a non-diagram cell is now left in the body for the engineinstead of being consumed. Strictly better than the previous silent drop, and
for jupyter/knitr
fig-altis a real engine option.Verification
cargo xtask verify(full, including the hub/WASM leg) passes on the rebasedbase. No snapshot files changed anywhere in the workspace.
End-to-end through the binary, output inspected — e.g. a
%%| fig-cap+%%| fig-altdiagram with no label now emits: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'saria-describedbypointing at it, and a negative control confirms mermaidrejects
accDescr:placed before the diagram-type line — which is why thedirective is inserted after the first declaration line rather than at the top.
Not verified: presentation in an actual screen reader, and the
q2 previewUIin a live browser session.
Note for reviewers
mainclaimed Q-2-42 … Q-2-46 while this branch was in flight (#497), so thetwo 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
qmd-syntax-helperrule 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
%%|.because
mermaid-renderis excluded from preview,CodeBlockRenderTransformwraps mermaid blocks in copy-button chrome there that
q2 rendersuppressesby transform ordering.
🤖 Generated with Claude Code