Skip to content

Render diagrams in the markdown pipeline#5

Open
petrovo-as wants to merge 1 commit into
scos-lab:mainfrom
petrovo-as:fix/render-diagrams-in-pipeline
Open

Render diagrams in the markdown pipeline#5
petrovo-as wants to merge 1 commit into
scos-lab:mainfrom
petrovo-as:fix/render-diagrams-in-pipeline

Conversation

@petrovo-as

Copy link
Copy Markdown

Replaces #4, which patched the claim protocol without fixing the underlying race. This removes the need for the protocol instead.

The problem

Mermaid and Vega render by mutating the DOM after React has committed it: the pipeline emits an HTML string with plain code blocks, React writes it via dangerouslySetInnerHTML, and an effect in MarkdownView then walks that DOM replacing each <pre> with SVG.

That leaves the content with two writers. React reassigns innerHTML whenever the string changes and knows nothing about the SVG, so a re-render wipes the diagrams; the effect then has to put them back, cancelling whatever render is in flight. The data-*-claimed attributes and the isCancelled plumbing exist only to arbitrate between the two writers — and they don't manage it. A superseding pass skips the blocks the cancelled pass claimed, so the blocks stay as code with nothing left to render them.

The change

Render diagrams where KaTeX already renders them: in the pipeline, before the HTML reaches React. Blocks are substituted inside an inert <template>, so the SVG travels with the tab's renderedHTML instead of living only in the DOM.

The content has a single writer again. Switching tabs cannot lose a diagram, because nothing re-renders on a switch — the SVG is already in the string. The claim attributes, the cancellation plumbing and the revert-then-rerender path are all gone; mermaid.ts and vega.ts are now just "source in, SVG out". A theme switch re-runs the pipeline, the same path as a content change.

Net: -225 / +158, mostly deletions.

Vega needs a real element with a width to lay out against, so it renders into an offscreen host sized to .markdown-body's content box, removed again before returning. The live document never sees it.

Also in here

  • Search highlighting skips <svg> subtrees. Now that SVG is part of renderedHTML, a <mark> injected into one is a foreign element the renderer drops — taking the label with it.
  • setActiveTab() ignores clicks on the already-active tab. It wrote the store with identical values, re-rendering subscribers for nothing. This was a real bug: clicking the active tab reliably turned its diagrams back into code blocks.
  • Declare the devtools feature so the MARKVIEW_DEVTOOLS branch in setup() compiles. The cfg named a feature that was never declared, so the block was dead code (it also produces a build warning today).

Testing

Verified by hand in a release build on Linux: switching between tabs with diagrams, clicking the active tab, Ctrl+F highlighting, and theme switching, plus a document covering Vega-Lite (fixed and "width": "container"), plain Vega, Mermaid, and a deliberately malformed spec — a bad diagram renders an error block without costing the rest of the document.

🤖 Generated with Claude Code

Mermaid and Vega were rendered by mutating the DOM after React had
committed it: the pipeline produced an HTML string holding plain code
blocks, React wrote it via dangerouslySetInnerHTML, and an effect then
walked that DOM replacing each <pre> with SVG.

That left the content with two writers. React reassigns innerHTML whenever
the string changes and knows nothing about the SVG, so a re-render wiped
the diagrams; the effect then had to put them back, cancelling whatever
render was in flight. The data-*-claimed attributes and the isCancelled
plumbing existed only to arbitrate between the two writers, and they did
not manage it: a superseding pass skipped the blocks a cancelled pass had
claimed, leaving them as code with nothing left to render them.

Render diagrams where KaTeX already renders them — in the pipeline, before
the HTML reaches React. Blocks are substituted inside an inert <template>,
so the SVG travels with the tab's renderedHTML instead of living only in
the DOM. The content has a single writer again, and switching tabs cannot
lose a diagram because nothing re-renders on a switch.

This drops the claim attributes, the cancellation plumbing and the
revert-then-rerender path. A theme switch re-runs the pipeline, the same
path as a content change.

Also:

- Search highlighting skips <svg> subtrees. Now that SVG is part of
  renderedHTML, a <mark> injected into one is a foreign element the
  renderer drops, taking the label with it.
- setActiveTab() ignores clicks on the already-active tab. It wrote the
  store with identical values, re-rendering subscribers for nothing.
- Declare the devtools feature so the MARKVIEW_DEVTOOLS branch in setup()
  compiles. The cfg named a feature that was never declared, so the block
  was dead code.

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

1 participant