Skip to content

fix(preview): a theme change re-draws the diagrams it has never re-drawn (#741), retargeted to master - #743

Merged
PathGao merged 2 commits into
masterfrom
fix/theme-recolours-onto-master
Sep 1, 2026
Merged

fix(preview): a theme change re-draws the diagrams it has never re-drawn (#741), retargeted to master#743
PathGao merged 2 commits into
masterfrom
fix/theme-recolours-onto-master

Conversation

@PathGao

@PathGao PathGao commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Base is master. Administrative: this is #741, already reviewed and merged,
but into perf/rich-content-anchor-drift rather than into master, because that
branch was its base while #740 was open. #740 was squash-merged to master,
which closed the branch's own PR and left this sitting where nothing points at
it. Cherry-picked onto master; the commit below is the same one. Same thing
happened to #732-#735 earlier in this series (#737).

Rebasing the branch instead does not work: #740's own commit conflicts against
its squash on master, which is the same content under a different SHA.

What is in here

A theme change never re-drew a Mermaid diagram, in any mode, for any theme, and
had not since #632.

renderRichContent finds diagrams with selfAndDescendants(root, 'pre code'),
and a diagram that has been drawn once has no <pre> left — that same function
replaced it with <div class="mermaid-diagram">. While the preview was
{@html sanitizedHtml} this was invisible, because every render put the whole
document back as source and the diagram was rebuilt from scratch. blockPatch.ts
keeps the enriched nodes instead, so from #632 on the theme change reached
nothing. Mermaid bakes its colours into the SVG, so a dark diagram stayed dark on
a light page indefinitely.

The source was already on the container — mermaidPrint.ts keeps it there so the
PDF path can rebuild the diagram light — so the theme it was drawn for is kept
beside it, and renderRichContent now collects the containers whose theme no
longer matches and re-draws them through a drawDiagram both paths share. The
collection runs before the pre code loop so the containers that loop is about
to build are not scanned as stale.

The viewer's theme effect also stopped depending on isEditing. It was a tracked
read, so every ⌘E and every mode-changing tab switch re-ran the whole theme block
dataset writes, clearVscodeTheme, saveStartupAppearance,
monaco.editor.setTheme, and for a vscode: theme an invoke('read_vscode_theme')
round trip plus a re-parse — to draw no diagram at all. git log -S puts that
guard at 06380bc, where enrichment was a whole-document pass on every render and
the preview was hidden in edit mode: it was protecting something real then. Its
sibling lost the guard when the patch effect was built; this copy was never
revisited.

Measured in Chromium over the real pipeline, one whole-article pass with the
other memos warm, two runs:

                             diagrams   theme moved   theme unchanged
samples/katex-stress.md            0    8.6-10.6ms       11.9-12.2ms
samples/markdown-syntax.md         4   61.2-76.1ms         2.6-3.0ms
samples/stress-test.md             3  107.5-121.2ms      12.5-12.8ms

The diagrams are the whole cost; katex-stress has none and its two columns agree.
The right-hand column is what a mode toggle used to pay to draw nothing.

Tests

scripts/diagramThemeRefresh.spec.ts drives renderRichContent twice against a
recording Mermaid stand-in — a behaviour test, not a source assertion. Reverting
the re-draw with the tests kept turns 4 of its 5 red.

Two assertions from #740 went red on this change and were updated rather than
relaxed: mermaidPrintTheme.test.ts pinned rememberDiagramSource's arity and
previewAnchorRestore.test.ts pinned a whole guard line when what it is for is
the untrack. That is the "pins today's spelling" weakness #740's own body
called out, arriving one PR later.

Verification

npm audit            0 vulnerabilities
npm run check        828 files, 0 errors, 0 warnings
npm test             1022 pass, 0 fail
npm run test:vitest  434 pass, 0 fail
cargo test           164 pass, 0 fail

Re-run after the cherry-pick. The app cannot be run here: worth toggling
light/dark with a document containing a Mermaid diagram open before this lands.

…awn (#741)

Mermaid bakes fills, strokes and label colours into the SVG, so a theme change
has to draw the diagrams again. The theme effect called `renderRichContent()`
to do exactly that, and it has not worked since #632: that function finds
diagrams by `pre code`, and a diagram that has been drawn once has no `<pre>`
left -- `renderRichContent` itself replaced it with the `.mermaid-diagram`
container. While the preview was `{@html sanitizedHtml}` every render put the
whole document back as source and the selector matched; `blockPatch.ts` keeps
enriched nodes across renders instead, and from then on a theme change reached
no diagram at all, in any mode, for any theme.

Nothing else covers it. Re-activating a tab patches to nothing, so
`patch.inserted` is empty; typing does not help either, because the diagram's
markup is unchanged and the patch keeps the node.

`mermaidPrint.ts` already keeps the source on the container so the PDF path can
rebuild a diagram with a light theme. The theme it was drawn with is now kept
beside it, and `renderRichContent` re-draws the containers whose theme no longer
matches the one it was asked for -- through the same cache, sanitizer and id
factory as a fresh diagram, which is a `drawDiagram` the two paths share.

The guard on the call was the second half. `if (markdownBody && !isEditing)`
read `isEditing`, so the whole effect re-ran on every mode toggle and on every
tab switch that changed the mode: the dataset writes, `monaco.editor.setTheme`,
and for a `vscode:` theme an `invoke('read_vscode_theme')` and a re-parse, with
nothing about the theme having moved. Nothing in the body wants a mode
dependency -- Editor.svelte themes Monaco itself from its own `theme` prop. And
the guard excluded from the re-colour every tab in edit mode, including a split
tab entered from edit mode, whose preview is on screen beside the editor.

The call is also made per branch rather than once at the end: the `vscode:`
branch does not know its own appearance until `parseAndApplyVscodeTheme` has
published `dataset.themeType`, which is what `currentMermaidTheme` reads, so the
old placement would have re-drawn every diagram in the theme being replaced.

Measured in Chromium as one whole-article pass with the other memos warm, two
runs:

                                 diagrams   theme moved   theme unchanged
    samples/katex-stress.md            0    8.6-10.6ms       11.9-12.2ms
    samples/markdown-syntax.md         4   61.2-76.1ms         2.6-3.0ms
    samples/stress-test.md             3  107.5-121.2ms      12.5-12.8ms

The right-hand column is what a mode toggle used to pay to draw nothing.

`untrack` stays, for the reason #740 established: a tracked call makes this
effect re-run when the rich-content libraries land and duplicate the cold-start
enrichment the patch effect owns.
…742)

`monaco.editor.setTheme` is global to the page, and the viewer's theme
effect set Monaco's stock `vs`/`vs-dark` over the `app-theme-*` the editor
had just asked for — a child's effects run before its parent's, so the
viewer's write landed last and stripped the editor of every markdown and
semantic token rule on each theme change. The name is now decided once, in
`editorTheme.ts`, and only `Editor.svelte` asks for it.
@PathGao
PathGao merged commit 28963d1 into master Sep 1, 2026
1 check passed
@PathGao
PathGao deleted the fix/theme-recolours-onto-master branch September 1, 2026 18:12
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