Repository navigation
[Tracking] Mermaid diagrams in docs.plus through a headless Pele extension #521
Description
Summary
docs.plus gets Mermaid diagrams, drawn by Pele, and every Tiptap developer gets the same feature as a new headless package, @docs.plus/extension-mermaid.
- For writers. Type a
```mermaidfence, or pick "Diagram" from the slash menu or the desktop Code select. The source stays on top, and the live diagram draws under it (like Notion's split view). Errors show after about 1 s of idle. - For developers.
npm install @docs.plus/extension-mermaid, addMermaidtoextensions, and it works. Every visible part can be replaced through config (view,theme,limits,sanitize,onStateChange), like the other five@docs.plus/extension-*packages. - No new node. A diagram is the existing
codeBlockwithlanguage: 'mermaid'. Yjs merges the source per character, and removing the extension leaves plain code blocks. - Pele, not mermaid.js. Pele is the Mermaid renderer by Steph Ango (@kepano), CEO of Obsidian. MIT licence. mermaid.js is never a dependency.
Facts measured on 2026-10-18 (laptop, pele@1.6.0 from npm):
peleis MIT, ESM only, with zero dependencies. Its full bundle is 251,353 bytes gzipped.render()is synchronous, and it runs in Bun and Node with no DOM.- A small flowchart renders in 3.9 to 6.7 ms on the first call, and a sequence diagram in 1.3 ms after warm-up.
Claim tags in this set: measured means run on 2026-10-18. Read from code means read at commit 69ca1ef16, not run. Every file:line in these issues is read at that commit.
Maintainer rulings
Quoted words are the maintainer's own, lightly edited for spelling. D1 to D12 are the decisions ruled on 2026-10-18; R-numbers are the open decisions below. The newest ruling wins over any child body.
2026-10-18, first design round
- Pele only. "I want to use Pele, not mermaid, because it much more faster." mermaid.js is never a dependency.
- D2, history renders diagrams. "we need history for this part also"
- D4, chart colours. docs.plus keeps Pele's default series colours. "let developer extend it through the extension config"
- D5, the frame. "yeah explicit": the two-tone frame, a design-system catalog row, and the package tokens
--mermaid-diagram-borderand--mermaid-diagram-radiusare approved. - D7, the toolbar. "yes agree": "Diagram" is the last item in the desktop Code select.
- Headless, DX first. "the extension must be truly headless, and then developer can config it and connect it to their editor, like the other extension we have, so the DX is sooo important."
2026-10-18, second round
- Headless means the house pattern. "yes, house pattern." It works with zero config, every UI part is replaceable, and
styles.cssis optional. - D1, outside developers first. "it first must design for outside developer and release as OSS standard and follow tiptap and prosemirror conventions and principles and naming and typo and also abstraction layers."
- D12, the name and the credit. "I chose mermaid but promote the pele and we have to say how the pele good and how is working on it, you know we have to use the credit of the author of pele." The name is
@docs.plus/extension-mermaid: Mermaid is the language, Pele is the engine. Credit Steph Ango as Obsidian's CEO; Obsidian was co-founded by Shida Li and Erica Xu, so never call him its creator. - D3, D6, D8, D9 and D10 as recommended. "agree on all of them."
- D3: only the exact language
mermaiddraws a diagram. - D6: the PWA does not precache Pele chunks; offline diagrams show the load-error state.
- D8: the webapp sanitizes Pele's SVG with DOMPurify; the package exposes
sanitize. - D9: Pele
autoDirectionstays on. - D10: measure render plus sanitize on a mid-range phone; if p99 is over about 200 ms, lower the default
limits. No worker.
- D3: only the exact language
- Limits are configurable. "let's have a default limit character and let the developer to config this through the extension config also."
2026-10-18, on the plan
- R10 (D11), latest Pele, part of the extension. "yes use latest release of pele". Pele sits in the package
dependencies(~range), so npm installs it with the extension. It is never bundled intodist. This repo pins the exact version with a rootoverridesentry, because the webapp never importspeleitself. - R11, one load-error report per page. "accept." The webapp reports a
load-errorto observability once per page load. - No early error on caret leave. "drop it". An error shows only after about 1 s of idle (over-engineering cut).
2026-10-18, on the UI preview
- Fold the source, per viewer. "the first must be open everywhere and remember this fold and unfold and yes folded for read-only and visitors". Writers start open; read-only views (history included) and visitors start folded; each viewer's own choice is remembered. Nothing is stored in the document, so collaborators never see another viewer's fold.
- Insert an example, never an empty box. "when user click the button on the toolbar to create mermaid we must create a simple mermaid as an example". The toolbar and the slash menu insert the four-line starter (§Commands).
- A Syntax button on hover, as an option. "a button over the mermaid card when user mouseover the mermaid node … let the user to navigate to the mermaid document … this is an option … the default is show." The
helpLinkoption defaults to the Mermaid syntax reference;nullhides it. - A calm header, a Copy button and micro-animations. "this mermaid label must be at the right corner and when user mouse over there, the label will be hide and then the button will be show up, also we need another button to copy all mermaid markdown to the clipboard. also we must use micro-animation". Approved on preview version 4 ("approve all"): the
mermaidlabel owns the right corner and cross-fades in place with Syntax, Copy and Hide source; Copy is thecopyoption, on by default; motion follows the house tokens.
Peer behaviour. AGENTS.md:151 says behaviour follows Google Docs, Word and Notion. Google Docs and Word have no native Mermaid, so no peer rule exists for them. Editing follows Notion's split view. Storage and viewing follow the ```mermaid fence of GitHub, GitLab and Obsidian. This is recorded so no one "fixes" it toward a peer that has no such feature.
Child issues, in order
Each child depends on the one before it. Run them in this order.
- Measure Pele render time on a phone and set the default diagram limits #522 — Measure Pele render plus sanitize on a laptop, a 6x phone stand-in and a real phone, and set the default
limits(D10). The public API freezes at its checkpoint. - Scaffold @docs.plus/extension-mermaid with its types and the insertMermaid command #523 — The package skeleton, the public and internal types, the constants,
MermaidandinsertMermaid. The rootoverridespin for Pele. - Add the Mermaid renderer, default view, styles and node view #524 — The renderer (the only Pele seam), the default view,
styles.css, the node view and the plugin. - Add the extension-mermaid playground and clean-room Cypress suite #525 — The clean-room playground on port 5178, four Cypress specs, and every package sabotage proof.
- Write the extension-mermaid README, docs, assets and StackBlitz example #526 — The README with the Pele credit,
docs/api.md,docs/guide.md,docs/security.md,docs/pele.md, the gallery, the hero GIF and the StackBlitz example. - Register extension-mermaid as the sixth family package #527 — The package gates, the CJS gate, and the sixth-package wiring in CI, Docker and the family docs.
- Draw Mermaid diagrams in the pad, history and /editor #528 — The docs.plus wiring for the pad, history and
/editor. It covers the slash and Code select entries, styles, print, the TOC rail, the D6 PWA proof, observability and the help page. - Run the Mermaid browser pass and security probes #529 — The browser matrix in 7 themes, iOS and Android, history, print, collaboration, all 33 diagram types, and 9 hostile-input probes.
- Run the Mermaid quality pass and ship gates #530 —
code-janitor, the final layering sweep, the doc audit,check:ciand the Docker cold build. - Publish extension-mermaid with the 2.2.0 family release and draft the launch posts #531 — The 2.2.0 family release and the launch drafts (the maintainer publishes and posts).
Deadline: Friday 2026-10-23, milestone v2.2.0 (this whole board ships as v2.2.0), board project 6. Day plan: Mon 10-19 #522; Tue 10-20 #523 and #524; Wed 10-21 #525 and #526; Thu 10-22 #527 and #528; Fri 10-23 #529, #530 and #531. The board's Target date field holds each date. #522 needs the maintainer's phone confirmation on Monday, because every later child waits on it.
Close this tracker when every child's Acceptance criteria hold, the maintainer has confirmed the D10 phone numbers (#522 Step 8), and 2.2.0 is on npm. Together these are the production-ready flag.
Design
Vocabulary
| Name | Meaning |
|---|---|
| Mermaid | The text language a writer types (flowchart LR …). GitHub, GitLab, Notion and Obsidian read the same text. |
| Pele | The engine that turns Mermaid text into SVG. npm package pele. |
| Diagram | The one user-facing word (slash menu, toolbar, help page). |
mermaid-diagram |
The one code name: CSS classes and the CSS variable prefix. |
Mermaid |
The exported extension (Extension.create({ name: 'mermaid' })). |
Non-goals
- mermaid.js as a dependency, a fallback, or a benchmark dependency in the repo.
- A new document node, a stored view mode, or a stored SVG.
- Diagrams in chat messages, in the digest email, or as pictures in DOCX/ODT export.
- A visual (drag-and-drop) diagram editor.
Anti-goals
These would make the work a failure, even if it runs:
- A diagram error or a slow diagram reloads, freezes or blocks the pad or Restore.
- The source text stops merging per character under Yjs.
- Removing the extension breaks documents. It must leave plain code blocks.
- Pele types leak into our public
.d.ts, so a Pele minor release breaks our users. - Marketing claims that are not measured or not attributed.
Document model
A diagram is the existing codeBlock node with attrs.language === 'mermaid'. The Mermaid text is the node's text content. There is no new node and no new attribute.
- Yjs merges the source per character. An attribute would be last-writer-wins.
- Markdown, HTML, JSON, MCP, REST inject and DOCX/ODT export already round-trip this node.
- A client without the extension shows a plain code block. A new node type would trip
enableContentCheck(apps/webapp/src/components/TipTap/TipTap.tsx:273-282). - Nothing derived is written to the document: no SVG, size, id or view state.
Public API
export interface MermaidOptions {
/** Pele colour and font variables, set on each frame. Values are CSS, so `var()` and `light-dark()` work. */
theme: MermaidTheme | null // default null (Pele or styles.css defaults)
/** Replaces the built-in preview. Return the element Pele draws into. */
view: ((context: MermaidViewContext) => MermaidView) | null // default null (built-in view)
/** Host sanitizer for each SVG string before it enters the page. */
sanitize: ((svg: string) => Node) | null // default null
/** Remote images make every viewer of a shared document fetch the URL. */
images: boolean // default false
/** Pele zoom controls on fine pointers. Always off on coarse pointers. */
zoom: 'auto' | false // default 'auto'
/** Sources over these limits show a `limit` error and are never drawn. */
limits: MermaidLimits // default { characters: N, edges: M }
/** Extra attributes for the frame element. */
/** Lets each viewer hide the source. Nothing is stored in the document. `false` removes the button. */
fold: MermaidFold | false // default { initial: 'folded-when-read-only', remember: true }
/** The "Syntax" button on hover opens this guide. `null` hides it. */
helpLink: string | null // default 'https://mermaid.js.org/intro/syntax-reference.html'
/** The Copy button on hover copies the whole source. `false` hides it. */
copy: boolean // default true
HTMLAttributes: Record<string, string> // default {}
/** Called when a diagram's state changes. Errors wait for the idle delay while the source changes, never per keystroke.
* Also fires after each redraw of a ready diagram. */
onStateChange: ((event: MermaidStateEvent) => void) | null // default null
}
export interface MermaidFold {
/** Where a block starts. 'folded-when-read-only' folds it when the editor is not editable. */
initial?: 'open' | 'folded' | 'folded-when-read-only'
/** Remembers each viewer's own choice in localStorage, keyed by the source text. */
remember?: boolean
}
export interface MermaidTheme {
background?: string; foreground?: string; muted?: string; line?: string
surface?: string; surfaceAlt?: string; border?: string; accent?: string
critical?: string; fontFamily?: string; fontFamilyMono?: string; radius?: string
/** Chart and category colours. Pele repeats them after 8. */
series?: readonly string[]
}
export interface MermaidLimits {
/** Maximum source length in characters. */
characters?: number
/** Maximum arrows (edges) in one diagram. Pele counts edges in flowchart and agentflow only. */
edges?: number
}
export type MermaidState = 'empty' | 'loading' | 'ready' | 'error' | 'load-error' | 'skipped'
export interface MermaidError {
code: 'syntax' | 'semantic' | 'limit' | 'unsupported-diagram' | 'load'
message: string
line: number // 1-based, 0 if none
column: number // 1-based, 0 if none
}
export interface MermaidSnapshot {
state: MermaidState
error: MermaidError | null
diagramType: string | null // from Pele detectType, for example 'flowchart'
}
export interface MermaidViewContext {
editor: Editor
getPos: () => number | undefined
/** Loads the renderer again after a load-error. */
retry: () => void
}
export interface MermaidView {
dom: HTMLElement // placed under the source
diagram: HTMLElement // the extension mounts Pele here
update?: (snapshot: MermaidSnapshot) => void
destroy?: () => void
}
export interface MermaidStateEvent extends MermaidSnapshot {
editor: Editor
getPos: () => number | undefined
/** Render time (Pele plus sanitize) in ms on `ready` and `error`; null otherwise. */
durationMs: number | null
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
mermaid: { insertMermaid: (source?: string) => ReturnType }
}
}thememaps to--pele-*variables withstyle.setProperty, never astyleattribute string. CSSOM writes are not blocked by CSP.- No Pele type appears in this surface.
limitsmaps to PelelimitandmaxEdges. A missing field keeps its default. The defaults are tuned by the phone measure (D10).- Fixed internals, not options:
outputLimit1000000,links: false, noiconsresolver. Each one becomes an option only when a developer asks.
Commands
insertMermaid(source?) inserts a new codeBlock from JSON: { type: 'codeBlock', attrs: { language: 'mermaid' }, content }. An empty source gives empty content. With no argument, it inserts this starter:
flowchart TD
A[Start] --> B{Ready?}
B -- Yes --> C[Done]
B -- No --> A
The caret lands at the end of the source. The command returns false inside a code parent, or when the schema has no codeBlock. Hosts gate with editor.can().insertMermaid(). It never uses setCodeBlock or a source string, because both change other content.
Typing ```mermaid already works through the code block input rule.
Node view and the view slot
The extension adds one plugin with props.nodeViews.codeBlock. It returns a view for mermaid blocks only, and undefined for the rest, so ProseMirror falls back to toDOM. The full lifecycle contract is in #524 §Interfaces → Node view.
DOM order inside div.mermaid-diagram:
div.mermaid-diagram__head: themermaidlabel in the right corner. On hover or focus, it cross-fades with the Syntax link, Copy and the fold button. While folded, the head also shows the source summary (rulings 15, 17 and 18).div.mermaid-diagram__source > pre > code: the source, as contentDOM. The wrapper carries the fold tween; the source is hidden only while folded.view.dom: the built-in preview, or the developer'sviewresult.
The extension owns the drawing. The view owns everything around it. The extension mounts Pele into view.diagram, then calls view.update(snapshot) on each publish. A developer can portal a React or Vue component into view.dom, the same way the webapp portals into hyperlink popover hosts.
The built-in view shows the last good diagram, dimmed, while an error stands, and an error line with a Try again button on load-error. Its strings are in #524 §Interfaces → createDefaultView.
The placeholder hint "Write a Mermaid diagram" belongs to the host's placeholder extension. #528 Step 10 holds the position fix.
Another codeBlock node view wins silently (#526 caveat 3).
Edit flow
- Insert with slash "Diagram", the Code select, a
```mermaidfence, or a paste of a fence orpre > code.language-mermaid. - The source sits on top and the live diagram below, in one frame. There is no mode toggle.
- The diagram redraws after a trailing debounce of about 300 ms.
- Errors appear after about 1 s of idle.
- Exit with the existing code block keys.
- Read-only editors and the history view show the same frame (D2), starting folded (ruling 15).
- Hide source folds the source for that viewer only, and the choice is remembered. Moving the cursor into a folded block opens it again, without changing the remembered choice.
- At rest, only the
mermaidlabel shows in the corner. Hover or keyboard focus swaps it, in place, for Syntax · Copy · Hide source. On touch screens the buttons always show, at 44px. Copy puts the whole source on the clipboard, and its icon turns into a check for 2.2 s.
Pele promotion and credit
The release contract forbids per-package marketing taglines in READMEs. So the package states facts only (#526), and the louder launch happens after the npm publish (#531).
- The credit line, verbatim, in the README and in
docs/pele.md: "Diagrams are drawn by Pele, the Mermaid renderer by Steph Ango (@kepano), CEO of Obsidian. MIT licence." - No Obsidian logo or branding, and no wording that suggests Obsidian endorses us.
- Pele bug reports go upstream from the maintainer only.
Where the rest lives
- The look, the view and the node view: Add the Mermaid renderer, default view, styles and node view #524. Docs and the Pele credit in the package: Write the extension-mermaid README, docs, assets and StackBlitz example #526. The release: Register extension-mermaid as the sixth family package #527 and Publish extension-mermaid with the 2.2.0 family release and draft the launch posts #531.
- The docs.plus wiring: Draw Mermaid diagrams in the pad, history and /editor #528. The browser pass and the security probes: Run the Mermaid browser pass and security probes #529.
- Speed: a mid-range phone must render within p99 ≤ 200 ms (D10, Measure Pele render time on a phone and set the default diagram limits #522). The laptop p95 ≤ 16 ms target is open (R1).
- The renderer has an internal interface (§Shared contract → Internal types). A public
rendereroption comes only when a developer asks. - The package joins the family at the current version (Scaffold @docs.plus/extension-mermaid with its types and the insertMermaid command #523) and publishes with the family at 2.2.0 (Publish extension-mermaid with the 2.2.0 family release and draft the launch posts #531).
- Left to the builder: the
styles.cssliteral values (from the hyperlink palette), the screenshot and GIF scenes, and the docs prose.
Risks
- Pele is about two weeks old. One renderer folder, no Pele types in our API, a
~1.6.0range, and a clean-room run on each bump. - Pele has no time limit. Default
limits, the crash marker, and the D10 phone gate. History renders too, so the gate also protects Restore. - A Pele escaping bug becomes stored XSS, because webapp CSP
script-srcallows'unsafe-inline'. DOMPurify plus the security probes before ship.
Shared contract
Children point here instead of repeating these.
Global constraints
Every agent working on these issues writes in Simplified English. That means 25 words or fewer per sentence and one name per thing. Identifiers, paths, numbers and commands are copied verbatim. Restate this rule when you dispatch further agents.
Names used in every task
REPOis the absolute path of your docs.plus checkout (the folder that holdsbun.lock).PKGisREPO/extensions/extension-mermaid. Commands use absolute paths, because the shell resets its directory between calls.<scratchpad>is the executing session's own scratchpad directory.REPO/Notes/is gitignored local evidence on the maintainer laptop, so each child posts the results that matter on its issue.- Root checks: from
REPO,bun run lint,bun run lint:stylesandbun run format. Each exits 0 with no warnings. - Webapp checks: from
REPO,bun run --filter @docs.plus/webapp typecheck, thenNEXT_PUBLIC_SUPABASE_URL=http://localhost:54321 NEXT_PUBLIC_SUPABASE_ANON_KEY=dummy-key NEXT_PUBLIC_RESTAPI_URL=http://localhost:4000 bun run --filter @docs.plus/webapp test. The env stub matches CI, because some modules build a Supabase client at import time. - Review checkpoint: the maintainer posts the report on the issue as a comment, or asks an agent to. Run
git -C REPO status --short, list the touched files, report each check as pass or fail, name any check you could not run, and stop. Never rungit add, commit, push or stash.
Repo rules
- The package imports no React, Vue or Next.
- Bun only. Never run npm, npx, yarn or pnpm in repo commands.
noderuns only in the CJS gate (Register extension-mermaid as the sixth family package #527 Step 2) and in the Run the Mermaid browser pass and security probes #529offline-prodserver, which the webappstartscript runs. - Agents never post, open or comment anywhere (
gh issue create,gh pr create,gh api -X POST, Discussions or social posts) unless the maintainer asks in that session. Otherwise they hand the text to the maintainer. - No
(build):push between Scaffold @docs.plus/extension-mermaid with its types and the insertMermaid command #523 and Register extension-mermaid as the sixth family package #527. The Dockerfiles install with--frozen-lockfile, which needs every workspace manifest, and Register extension-mermaid as the sixth family package #527 adds the new one. - No family release (
bun run release:family) between Register extension-mermaid as the sixth family package #527 and Publish extension-mermaid with the 2.2.0 family release and draft the launch posts #531 unless the maintainer rules first. Once Register extension-mermaid as the sixth family package #527 adds the SSOT entry, a family release publishes this package too. - Never hand-edit generated files:
bun.lock,examples/vanilla/main.ts,examples/vanilla/index.html,apps/webapp/src/types/supabase.ts. - After any package source change: run
bun run buildinPKG, clearapps/webapp/.next/cache, restart dev, and hard-refresh. Restart a running playground after a build. - JSDoc and comments are 4 lines or less and say why. No "v1", "phase 1" or "MVP" labels.
- Every exported function has an explicit return type (
explicit-module-boundary-types,--max-warnings=0).
Pele
- Use the latest
pelerelease at the start of Measure Pele render time on a phone and set the default diagram limits #522 (D11). It is1.6.0on 2026-10-18, and every1.6.0in these issues means that version. If npm shows a newer one, use it everywhere, and recheck the Pele facts in Add the Mermaid renderer, default view, styles and node view #524 §Interfaces and in §Summary against itsdist/*.d.tsbefore Scaffold @docs.plus/extension-mermaid with its types and the insertMermaid command #523. - Pele is part of the extension: the package
dependencieshold~1.6.0, so npm installs it with the extension, and it is never bundled intodist. Rootpackage.jsonoverridespins the exact"pele": "1.6.0"for this repo (D11).bun.lockmust hold exactly one"pele@1.6.0". - The renderer's dynamic
import('pele/lazy')is the only runtime Pele import in the repo.import typefrompele/lazyis allowed insrc/renderer/only. Never import the mainpeleentry: it registers all 33 types and has a separatePeleErrorclass. - The d.ts greps (their home). Run from
PKGafterbun run build:grep -cE "\b(Mounted|MountOptions|PeleError|RenderResult|DiagramType|DiagramRenderer|RenderOutcome|RendererOptions)\b|from ['\"]pele" dist/index.d.ts dist/index.d.ctsprintsdist/index.d.ts:0anddist/index.d.cts:0.grep -c insertMermaid dist/index.d.ts dist/index.d.ctsprints 1 or more for each file.
Layering
Each layer imports only the ones listed.
mermaid.ts: nodeView, types, constants.- nodeView: renderer, view, types, constants.
- renderer:
pele/lazy, types, constants. - view: types only.
constants.tsandtypes.tsmake no runtime import.constants.tsmayimport typefromtypes.ts.
Behaviour
- Detection (D3): a diagram is a
codeBlockwithattrs.language === 'mermaid', exact and case-sensitive. The extension never writeslanguage. The node view never dispatches a transaction. One exception: the plugin key handler dispatches a selection, so ArrowUp or ArrowDown into a folded block opens it (ruling 15; maintainer ruling, 2026-10-19). DEFAULT_LIMITSholds the values the maintainer confirms at the Measure Pele render time on a phone and set the default diagram limits #522 checkpoint (D10).- Render debounce: trailing 300 ms after the last text change, local or remote. Error idle delay: 1000 ms.
- Error timing (A1, its home).
idlestarts true.- A text change, local or remote, sets
idlefalse, drops any pending error, and restarts the 1000 ms idle timer. - The idle timer sets
idletrue and publishes any pending error. - An
erroroutcome publishes at once whenidleis true. This covers a document that loads with a broken diagram. - Otherwise the error waits for the idle timer. A peer's broken middle state therefore never flashes either.
ready,empty,load-errorandskippedpublish at once. Every publish cancels a pending error.- An error that is already shown stays shown, dimmed, until the next publish.
- D8 config: DOMPurify
{ USE_PROFILES: { svg: true }, ADD_ATTR: ['role'], RETURN_DOM_FRAGMENT: true }. Draw Mermaid diagrams in the pad, history and /editor #528sanitizeDiagramSvgapplies it; Measure Pele render time on a phone and set the default diagram limits #522 measures with it. onStateChangeruns on every publish except the initial snapshot: on each state change, and after each completed draw, ready-to-ready included. It never runs per keystroke.durationMsis a number onreadyanderror, and null otherwise.emptymeanstextContent.trim() === ''. It never renders; the renderer is destroyed, so the diagram host is empty.loadingis the initial state only.- Visibility: one IntersectionObserver per
EditorView, withroot: nullandrootMargin: '800px 0px'. The first hit renders, then unobserves. Render timers do nothing before it. Inside a nested scroll container, such as the pad, the margin does not pre-mount (design choice; Run the Mermaid browser pass and security probes #529 Step 4 row Pop-in records the effect). - Crash marker: sessionStorage key
mermaid-diagram:rendering:<hash>. It wraps only the synchronous draw. Every storage access is in try/catch. - Async safety: each render takes a
runId. After everyawait, return when destroyed or stale. Catch everyimport()andload()rejection inside the renderer: an escaped rejection reloads the pad throughapps/webapp/src/utils/chunkLoadRecovery.ts. - Error text goes in only through
textContent. Nothing inside.ProseMirrorusesaria-live,role=status,role=alertoroutput. The source is neverdisplay:noneoraria-hiddenon screen. - Register
Mermaidonce, inbaseExtensionsofTipTap.tsx. That covers the pad, history and/editor(D2). Never register it in chat.
Sabotage form (house rule: prove a test by breaking the code). In one compound command: copy the file to <scratchpad>/sabotage/, set trap 'cp "<scratchpad>/sabotage/<name>" <file>' EXIT INT TERM, apply the change, and run the spec. The run must exit non-zero and name the expected case. Afterwards, cmp "<scratchpad>/sabotage/<name>" <file> exits 0. If a sabotaged run passes, the test proves nothing: record it, and fix the test, not the code.
Review focus
Each line names an input, the expected behaviour, and the tests that pin it.
- A failed Pele chunk import must not reload the pad. Offline or a dropped network shows
load-errorwith "Try again", and Try again draws once the network is back. Pins: Add the extension-mermaid playground and clean-room Cypress suite #525 case 5, sabotage S4 and S5; Run the Mermaid browser pass and security probes #529 rowsofflineandoffline-prod. - Errors must not flash while typing. Typing from a
readydiagram through a broken middle state keeps the last diagram at full opacity, with no error text, until A1 publishes. Pins: Add the extension-mermaid playground and clean-room Cypress suite #525 case 3, sabotage S2; Run the Mermaid browser pass and security probes #529 rowcollab. - A type change or a stale render must not win. Editing
flowchartintosequenceDiagramand back draws the current type. Pins: Add the extension-mermaid playground and clean-room Cypress suite #525 case 4, sabotage S3; Run the Mermaid browser pass and security probes #529 rowtype-race. - The Code select must not collide inside a diagram. Only "Diagram" is checked in a mermaid block; "Code block" turns it into a code block with
language: null; "Diagram" inside code does nothing. Pins: Draw Mermaid diagrams in the pad, history and /editor #528 Steps 7-8; Run the Mermaid browser pass and security probes #529 Code select rows. insertMermaidmust leave the caret at the end of the new source. This holds in an empty paragraph, mid-paragraph and at the start of a paragraph; inside code the command returns false. Pins: Add the extension-mermaid playground and clean-room Cypress suite #525 insert cases and sabotage S1 (mid-paragraph, start, inside code); the empty paragraph is the slash path (Draw Mermaid diagrams in the pad, history and /editor #528 Step 24, Run the Mermaid browser pass and security probes #529 Slash andmobilerows). The "block cannot fit" branch depends on the schema and is not pinned.- A custom view or an emptied block must not keep stale DOM. Emptying the text clears
view.diagramand publishesempty;destroy()clears onlyview.diagramand callsview.destroy. Pins: Add the extension-mermaid playground and clean-room Cypress suite #525 case 6. - Hidden source must never hold the caret. When the cursor enters a folded block (an arrow key, a search jump, an undo), the source opens. Pins: Add the extension-mermaid playground and clean-room Cypress suite #525
fold.cy.tscase 2; Run the Mermaid browser pass and security probes #529 row Source fold.
File map
Package root is PKG. One file, one job. Export names match file names.
| Path | Job | Issue |
|---|---|---|
package.json, tsconfig.json, eslint.config.js, .gitignore, tsup.config.ts |
family package shape | #523 |
src/index.ts |
named exports | #523 |
src/types.ts |
public types (#521 §Public API), Commands augmentation |
#523 |
src/renderer/types.ts |
internal renderer types | #530 |
src/constants.ts |
MERMAID_LANGUAGE, DEFAULT_LIMITS |
#523 |
src/mermaid.ts |
the extension, insertMermaid, the plugin |
#523, #524 |
src/renderer/createRenderer.ts |
the only Pele seam | #524 |
src/view/createDefaultView.ts, element.ts, icons.ts |
the built-in view and its DOM helpers | #524, #530 |
src/nodeView/createMermaidNodeView.ts |
the node view: ProseMirror lifecycle and painting | #524 |
src/nodeView/fold.ts |
the fold module: folded flag, start state, remembered choice, caret entry, head controls | #530 |
src/nodeView/renderLoop.ts |
the render loop: debounce, error idle delay, stale runs, crash marker | #530 |
src/nodeView/copy.ts, theme.ts, observeVisible.ts, crashMarker.ts, src/hashSource.ts |
node-view helpers | #524, #530 |
src/styles/mermaid.css → dist/styles.css |
the built-in look | #523, #524 |
test/playground/*, cypress.config.ts, cypress/** |
clean-room playground and specs | #525 |
README.md, CHANGELOG.md, CONTRIBUTING.md, docs/*.md, assets/*, examples/vanilla/*, cypress/docs/* |
package docs and assets | #526 |
| family lists, CI, Docker, repo docs | sixth-package wiring | #527 |
webapp, server, CONTEXT.md, design system, docs/diagrams.md |
docs.plus integration | #528 |
REPO/Notes/local-docs/mermaid-measure/, mermaid-ship-check/ (gitignored) |
measure and ship harnesses | #522, #528, #529, #530 |
Public surface (src/types.ts, re-exported by name from src/index.ts)
Copy the §Public API code block word for word, from export interface MermaidOptions to the end of the declare module '@tiptap/core' block, with every JSDoc line. Add import type { Editor } from '@tiptap/core' first. The block already carries the final JSDoc. Make one change: in the limits trailing comment, N and M become the values #522 confirms, with no issue reference.
No Pele type appears in any public type.
Internal types (src/renderer/types.ts, never exported from index.ts)
export type RendererOptions = Pick<MermaidOptions, 'images' | 'limits' | 'sanitize' | 'zoom'>
export interface RenderOutcome {
state: 'ready' | 'error'
error: MermaidError | null // null on 'ready'
diagramType: string | null // the type the last prepare() detected
durationMs: number // Pele plus sanitize, around the sync call
}
export interface DiagramRenderer {
/** Imports pele/lazy and the source's type chunk. Never rejects. */
prepare(source: string): Promise<MermaidError | null> // { code: 'load', ... } on a failed import
/** Synchronous Pele mount or update. Call only after prepare resolved null. */
draw(source: string): RenderOutcome
/** Destroys the Pele mount and clears the diagram host. */
destroy(): void
}src/constants.ts
import type { MermaidLimits } from './types'
export const MERMAID_LANGUAGE = 'mermaid'
export const DEFAULT_LIMITS: Required<MermaidLimits> = { characters: N, edges: M }CSS contract (public)
- Classes:
.mermaid-diagram,.mermaid-diagram__head,.mermaid-diagram__label,.mermaid-diagram__actions,.mermaid-diagram__help,.mermaid-diagram__copy,.mermaid-diagram__summary,.mermaid-diagram__source,.mermaid-diagram__fold,.mermaid-diagram__preview(root of the default view only),.mermaid-diagram__diagram,.mermaid-diagram__error,.mermaid-diagram__message,.mermaid-diagram__detail,.mermaid-diagram__retry. data-source(openorfolded) sits on the root.mermaid-diagram, per viewer.data-copiedsits on.mermaid-diagram__copyfor 2200 ms after a copy.data-statesits onview.domwithMermaidStatevalues. Select it as.mermaid-diagram > [data-state].- Tokens:
--mermaid-diagram-borderand--mermaid-diagram-radiusonly. - Pele overrides, which must beat Pele's inline styles:
.mermaid-diagram .pele-error { display: none },.mermaid-diagram svg.pele { opacity: 1 !important },.mermaid-diagram > [data-state='error'] svg.pele { opacity: 0.4 !important }.
Open decisions and owed items
Open decisions: none. R1 to R9 and the two contract exceptions were ruled on 2026-10-19 (see the Maintainer ruling, 2026-10-19 comment). 2.2.0 waits on the real phone run (#522 Step 8).
Owed after the children close (not passes)
- Real iOS Safari and Android Chrome typing in a diagram, plus any mobile row recorded as a harness limit.
- A Node 20.19 run of the CJS gate, when no Node 20 is installed.
make build-prod-cibefore the first(build):push, if the Docker daemon was down.A Discord badge for the package README, once Replace the expired Discord invite in the docs and the app #480 has a new invite.Done with the new invite (Replace the expired Discord invite in the docs and the app #480).
Rejected options
| Option | Why rejected |
|---|---|
A new mermaid node type |
Stale clients freeze on an unknown node, it needs server schema parity, and existing ```mermaid blocks would not render. |
An atom node with a source attribute (the Tiptap Mathematics shape) |
Last-writer-wins under Yjs, so two people editing one diagram lose text. |
| A widget decoration for the preview | It leaks out of heading folds, breaks the fold index walker, and is dropped on every remote change. |
| A Code / Preview / Split toggle | It needs a stored attribute, and one viewer's toggle would flip everyone's view. |
| The source in a popover, modal or panel | Tab traps, lost drafts, a third popover controller, and whole-string writes. |
| A render worker | It loses Pele's resize and zoom, and a blob: worker is blocked by CSP. Ruled out by D10. |
| mermaid.js | About 1.6 MB with 23 dependencies, and it needs a browser page to render. Ruled out by ruling 1. |
| An early error when the caret leaves the block | It saves at most 1 s and costs a selection listener and caret logic. Dropped by ruling 14. |
Activity
HMarzban commentedon Oct 18, 2026
Maintainer ruling, 2026-10-18
These rulings were made while the design and the plan were written. The tracker body quotes each one with its number. This comment is the dated record.
--mermaid-diagram-borderand--mermaid-diagram-radiusare approved.styles.cssis optional.@docs.plus/extension-mermaid. Promote Pele and credit its author, Steph Ango (@kepano), as CEO of Obsidian.mermaidonly; no PWA precache of Pele chunks; DOMPurify in the webapp and asanitizeoption;autoDirectionon; measure on a phone and lower the defaultlimitsif p99 is over about 200 ms, with no worker.dependencies. The repo pins the exact version with a rootoverridesentry.load-erroronce per page load.HMarzban commentedon Oct 18, 2026
Maintainer ruling update, 2026-10-18
Made on the interactive UI preview. The tracker body quotes each one as rulings 15 to 17.
fold(initial,remember, orfalse).helpLink: on by default (the Mermaid syntax reference), andnullhides it. On phones it is always visible.Updated: #521 (design, public API, rulings), #523 (options), #524 (node view and CSS), #525 (
fold.cy.tsand sabotage S6), #526 (docs), #528 (design-system row and tokens), #529 (Fold row).HMarzban commentedon Oct 18, 2026
Maintainer ruling update, 2026-10-18 — ruling 18
The maintainer approved preview version 4 ("approve all"). The issue bodies now carry it.
mermaidlabel shows, in the right corner. Hover or keyboard focus swaps it, in place, for Syntax, Copy and Hide source. On touch screens the buttons always show, at 44px.copyoption,trueby default. The button puts the whole source on the clipboard. Its icon turns into a check for 2200 ms, marked bydata-copied. It never dispatches a transaction.grid-template-rowstween on a newdiv.mermaid-diagram__sourcewrapper. Reduced motion turns all of it off.Changed bodies: #521 (ruling 18, public API, DOM, edit flow, CSS contract), #523 (defaults), #524 (head, Copy, CSS), #525 (
?bareand a new Copy case), #526 (README options), #528 (house skin), #529 (row renamed to Source fold).No action needed from implementers beyond the updated bodies.
HMarzban commentedon Oct 19, 2026
Maintainer ruling, 2026-10-19
Rulings on the open Mermaid decisions:
next.config.jsPWA no-precache predicate. R9 isdocs/diagrams.md, linked fromdocs/README.md.realPressinfold.cy.ts: kept, as a deliberate exception to Add the extension-mermaid playground and clean-room Cypress suite #525. Only a real key press proves the browser caret path.nodeView/fold.ts,nodeView/renderLoop.ts, the view helpers andsrc/renderer/types.ts.toc/utils/sectionPreviewClone.ts(Draw Mermaid diagrams in the pad, history and /editor #528): accepted.characters: 15000andedges: 500.lineOffsetlater.The work is in the maintainer's working tree and is not committed or pushed yet.
feat(extension-mermaid): add the Mermaid diagram extension
HMarzban commentedon Oct 19, 2026
Maintainer ruling, 2026-10-19 (ship gates)
These record exceptions to the contract above, which the #530 sweep found.
pele.apps/hocuspocus.server/src/modules/mcp/domain/checkDiagrams.tsrunsawait import('pele')to check a diagram before an AI app writes it. It is server-side, outside the package, and loads on demand. The "only import is insrc/renderer/" rule covers the extension and the webapp bundle; it does not cover this server path. Accepted.src/hashSource.tsis part of the accepted source split.nodeView/fold.tsandnodeView/crashMarker.tsimport it for the per-viewer fold key and the crash marker. §Layering did not list it. Accepted as part of the split ruling.src/styles/mermaid.css.:focus-visibleon the head buttons, plus.pele-zoom { display: none !important }andcontain: none !importantinside@media print. The print lines hide Pele's zoom controls on paper and undo the host's inlinecontain: paint, which would otherwise clip the printed diagram. Accepted.The limit test no longer imports the main
peleentry (1258a9d42). So inside the package, the renderer is still the only runtime import.test(extension-mermaid): load Pele through pele/lazy in the limit test
HMarzban commentedon Oct 21, 2026
Pele pin update, 2026-10-20
§Global constraints → Pele names
1.6.0. The pin is now1.7.0everywhere (aebd55866, deployed in3784c2c65). Read every1.6.0in these issues as1.7.0.bun.lockholds exactly onepele@1.7.0.The checks are on #531 Step 1. One fact in §Global constraints is not what the code does: the package
dependenciespin the exact version, not a~range.Records of past measurements keep
pele@1.6.0on purpose, because that was the version measured:docs/pele.md(the 0.90 ms starter) anddocs/security.md(the DOMPurify probe).HMarzban commentedon Oct 22, 2026
All work in this tracker shipped in docs.plus 2.2.0, and every child issue (#522 to #531) is closed.
mermaidcode block now draws a live diagram under its source, in the pad, in History and in/editor. Pele draws it.@docs.plus/extension-mermaidis on npm at 2.2.0, as the sixth family package.Follow-up: #572 (an empty H1 at the end of every document) is fixed and deployed, in the v2.3.0 milestone.