ISMS stores document bodies and register free-text fields as Markdown. The web application has two deliberately separate pipelines:
- Display rendering turns Markdown into sanitized HTML and renders Mermaid diagrams for read-only views.
- Editor conversion turns Markdown into TipTap HTML and back without executing diagrams or losing fenced-code metadata.
Keeping these pipelines separate prevents display-only markup such as copy buttons and rendered SVG from leaking into stored Markdown.
The first fenced-code info token selects Mermaid, case-insensitively:
```mermaid
flowchart LR
Draft --> Review --> Approved
```Additional info tokens are retained through editor round-trips. Other fenced code blocks continue to use syntax highlighting, line numbers, wrapping, and the copy action.
web/src/composables/useRenderMd.js is the canonical Markdown display
renderer. A Mermaid fence becomes an inert placeholder containing escaped
source text:
<div class="mermaid-diagram" data-mermaid-pending="true">...</div>The complete read path is:
- Marked parses Markdown.
- The custom code renderer emits either a Mermaid placeholder or the existing highlighted code-block markup.
- DOMPurify sanitizes the generated display HTML. Diff and review callers that
require extra tags use
parseMd()and apply their own DOMPurify boundary. - Vue inserts the sanitized HTML through
v-html. - The globally registered
v-mermaiddirective scans that root for pending placeholders. web/src/composables/useMermaid.jslazy-loads Mermaid, renders each diagram, sanitizes the returned SVG, and inserts it into its original placeholder.
The directive is attached to document, review, diff, track-changes, and register-field Markdown roots. Comment and mention renderers are intentionally excluded; they have their own constrained rendering path and do not execute diagram source.
The directive runs on both Vue mounted and updated. Pending placeholders are
claimed synchronously before the Mermaid module is loaded. This prevents an
overlapping update from scheduling the same DOM node twice while the lazy import
is still pending.
Every render receives a unique isms-mermaid-N identifier. Diagrams within a
root are processed sequentially; Mermaid also serializes its internal render
queue because it uses temporary document-level elements. Before inserting an
asynchronous result, the renderer verifies that the target still belongs to the
original root; results for replaced content are discarded.
If content is replaced, its new placeholders carry
data-mermaid-pending="true" and the next directive update renders them.
Diagram rendering is client-side only. No Mermaid-generated SVG is stored in git or sent back to the server.
The boundary has four layers:
- Markdown source enters the placeholder as escaped text, not executable HTML.
- Mermaid is initialized with
startOnLoad: false,securityLevel: "strict", andsuppressErrorRendering: true. htmlLabels: falsekeeps labels as SVG text instead offoreignObjectHTML.- The returned SVG passes through DOMPurify's
svgandsvgFiltersprofiles before insertion. Mermaid binding functions run only against that sanitized result.
The normal display-HTML DOMPurify pass remains in place before Mermaid runs, so
diagram support does not weaken the existing v-html boundary.
A syntax or loading failure affects only its diagram. The placeholder becomes
an accessible role="alert" state with the message
Diagram could not be rendered and an inert preformatted copy of the source.
This keeps invalid or diff-marked diagrams inspectable without introducing a
new HTML sink; the source is assigned through textContent.
Mermaid error SVGs and temporary wrapper or sandbox elements are removed. The original fenced source remains available in the editor for correction.
web/src/composables/useMarkdownConvert.js converts between stored Markdown and
TipTap HTML. Mermaid stays a plain editable code block; the editor never loads
or executes Mermaid.
The surrounding <pre> carries two internal attributes:
data-info-stringretains the complete original fence info string.data-wrappedretains the editor's per-block wrapping choice.
web/src/components/codeBlockAttributes.js defines the corresponding TipTap
attributes shared by DocumentEditor and MarkdownField. When a user changes
the language or wrapping controls, only those tokens are updated; unrelated
info tokens remain intact. Mermaid trailing blank source lines are preserved.
Global styles in web/src/style.css provide the slate container, loading and
error states, horizontal overflow, and responsive SVG sizing. Mermaid uses its
configurable base theme so the slate palette reaches the generated SVG. Print
styles avoid splitting a diagram across pages, remove the application container
background, and invert the complete dark SVG palette to preserve text/shape
contrast on white paper.
For a new field that uses the canonical free-text Markdown model:
- Render it with
renderMarkdown(), or useparseMd()followed by an explicit DOMPurify configuration when additional safe tags are required. - Put
v-mermaidon the same element that owns the resultingv-html. - Do not add the directive to comment, mention, or other constrained rich-text renderers without first extending their security model.
- Add a focused test when the surface has custom sanitization or update behavior.
| File | What it covers |
|---|---|
web/test/useRenderMd.test.js |
Mermaid placeholder detection and unchanged ordinary code rendering |
web/test/useMermaid.test.js |
Safe configuration, real syntax errors, SVG sanitization, unique IDs, rerendering, and overlapping lifecycle calls |
web/test/mermaidDirective.test.js |
Vue mounted/updated directive hooks |
web/test/useMarkdownConvert.test.js |
Fence source, info-token, language, wrapping, and trailing-line round-trips |
web/test/codeBlockMetadata.test.js |
TipTap preservation of Mermaid metadata |
web/test/codeLanguages.test.js |
Mermaid availability in the editor language selector |
web/test/editorLowlight.test.js |
Plain Mermaid source without lowlight auto-detection |
web/test/mermaidPrintStyles.test.js |
Print palette inversion for text/shape contrast |