T-VCFL-md-engine-mermaid
State: closed/done · Impact: high · Complexity: small
A ```mermaid fence renders a diagram in read mode but stays source text in live mode, and the
engine has no way to change an option (like the mermaid theme) after mount. This task adds the
mermaid plugin’s fence widget and the runtime-mutable optionsCompartment with
setOptions(view, partial).
| Location | Role today |
|---|---|
packages/ts/obsidian-editor/src/decorations.ts | FencedCode case: fence background and CodeLangWidget label; a mermaid fence stays source text |
packages/ts/obsidian-editor/src/state.ts | modeFacet, modeCompartment, optionsFacet; nothing reconfigurable at runtime |
packages/ts/obsidian-editor/src/types.ts#ObsidianEditorOptions | No mermaidTheme |
packages/ts/obsidian-editor/src/vue/index.ts#useObsidianEditor | No way to change options after mount |
packages/ts/obsidian-md/src/vue/components/MermaidBlock.ts | Read-mode mermaid component; T-RCWI-md-obsidian-md-sync-helpers extracts renderMermaid from it |
The plugin roster comes from T-4TE2-md-engine-plugin-contract; requestRedraw and the bench
from T-0GV4-md-engine-line-endings-corpus.
Proposed
Section titled “Proposed”The state module gains optionsCompartment and
setOptions(view: EditorView, partial: Partial<ObsidianEditorOptions>): void, which reconfigures
the compartment by merging partial over the current optionsFacet value. ObsidianEditorOptions
gains mermaidTheme?: string (default 'default').
packages/ts/obsidian-editor/src/plugins/mermaid.ts:
- Inactive
FencedCodewhose info word ismermaid→ block replace widget. The widget fills from a cache keyed bytheme + source; on a miss it shows the fence source, callsrenderMermaid(source, theme)(T-RCWI-md-obsidian-md-sync-helpers) and issuesrequestRedraw(view)on resolve. Because the cache key includes the theme,setOptions(view, { mermaidTheme })re-renders every diagram on the next rebuild. - Render failure → the widget carries
cm-mermaid-errorand shows the fence source; clicking it puts the cursor in the fence. Active fence → plain source, as any code fence.
useObsidianEditor returns setOptions(partial) bound to the view.
Approach
Section titled “Approach”- Write
packages/ts/obsidian-editor/test/plugins/mermaid.plugin.test.tswith a mockedrenderMermaid: inactive fence → widget; after resolve plusrequestRedrawthe widget DOM contains the mock SVG; active fence → no widget; rejection →cm-mermaid-errorwith the source text; two renders under one theme hit the cache once per source (spy). - Add
optionsCompartmentandsetOptionsto the state module; routeobsidianEditorExtensions’ options through the compartment. - Implement the plugin and add
mermaidtoBUILTIN_PLUGINS. - Test the theme swap:
setOptions(view, { mermaidTheme: 'dark' })→ the next rebuild callsrenderMermaidwith'dark'and the document (state.sliceDoc()) is byte-identical before and after. - Extend
useObsidianEditorand the Vue test; exportsetOptionsfrom the package entry. bun test,tsc --noEmit,moon run obsidian-editor:bench.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
packages/ts/obsidian-editor/src/plugins/mermaid.ts | new | mermaid plugin: fence widget, theme-keyed cache, error fallback |
packages/ts/obsidian-editor/src/plugins/index.ts | new | Created by T-4TE2-md-engine-plugin-contract; mermaid joins BUILTIN_PLUGINS |
packages/ts/obsidian-editor/src/state.ts | modify | optionsCompartment, setOptions |
packages/ts/obsidian-editor/src/types.ts | modify | mermaidTheme?: string |
packages/ts/obsidian-editor/src/vue/index.ts | modify | setOptions(partial) in the return |
packages/ts/obsidian-editor/src/index.ts | modify | Export setOptions |
packages/ts/obsidian-editor/test/plugins/mermaid.plugin.test.ts | new | Widget, cache, theme-swap and failure tests |
packages/ts/obsidian-editor/test/vue.test.ts | modify | setOptions returned and effective |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
bun test packages/ts/obsidian-editorpasses, including the new suite. - AC-2: an inactive
mermaidfence renders one block widget; after the mockedrenderMermaidresolves and arequestRedraw, the widget DOM contains the mock markup; with the cursor inside the fence the source is shown instead. - AC-3:
setOptions(view, { mermaidTheme: 'dark' })makes the next rebuild callrenderMermaidwith theme'dark', andstate.sliceDoc()is byte-identical across the call. - AC-4: a rejected render produces a widget with class
cm-mermaid-errorcontaining the fence source. - AC-5:
useObsidianEditorreturnssetOptions; the Vue test proves a theme change re-renders without touching the document. - AC-6:
moon run obsidian-editor:benchstays within 1.5× of the recorded baseline.
Out of scope
Section titled “Out of scope”- Read-mode mermaid and the theme-bound Vue wrapper (
MermaidBlockvia T-RCWI-md-obsidian-md-sync-helpers;MdMermaidBlockin T-A0HT-md-ui-layer-skeleton). - Wiring
mermaidThemeto the product dark-mode signal; the chrome layer callssetOptions({ mermaidTheme })from its theme watcher. - Any other runtime-mutable option;
setOptionsis generic but onlymermaidThemegets a consumer here.
Dependencies
Section titled “Dependencies”- T-RCWI-md-obsidian-md-sync-helpers for
renderMermaidand themermaiddependency on the editor manifest. - T-4TE2-md-engine-plugin-contract for the plugin contract;
T-0GV4-md-engine-line-endings-corpus for
requestRedrawand the bench. - D-7VMX: the fence and its info word come from the
FencedCodetree node.
Discovery context
Section titled “Discovery context”Member of milestone M-931L; engine step E8 of decision D-K3EM-markdown-editor-chrome-and-plugin-environment.
Depends on
Section titled “Depends on”T-RCWI-md-obsidian-md-sync-helpers