Skip to content

[Tracking] Mermaid diagrams in docs.plus through a headless Pele extension #521

Closed

Description

@HMarzban

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 ```mermaid fence, 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, add Mermaid to extensions, 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 codeBlock with language: '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):

  • pele is 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

  1. Pele only. "I want to use Pele, not mermaid, because it much more faster." mermaid.js is never a dependency.
  2. D2, history renders diagrams. "we need history for this part also"
  3. D4, chart colours. docs.plus keeps Pele's default series colours. "let developer extend it through the extension config"
  4. D5, the frame. "yeah explicit": the two-tone frame, a design-system catalog row, and the package tokens --mermaid-diagram-border and --mermaid-diagram-radius are approved.
  5. D7, the toolbar. "yes agree": "Diagram" is the last item in the desktop Code select.
  6. 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

  1. Headless means the house pattern. "yes, house pattern." It works with zero config, every UI part is replaceable, and styles.css is optional.
  2. 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."
  3. 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.
  4. D3, D6, D8, D9 and D10 as recommended. "agree on all of them."
    • D3: only the exact language mermaid draws 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 autoDirection stays on.
    • D10: measure render plus sanitize on a mid-range phone; if p99 is over about 200 ms, lower the default limits. No worker.
  5. 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

  1. 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 into dist. This repo pins the exact version with a root overrides entry, because the webapp never imports pele itself.
  2. R11, one load-error report per page. "accept." The webapp reports a load-error to observability once per page load.
  3. 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

  1. 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.
  2. 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).
  3. 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 helpLink option defaults to the Mermaid syntax reference; null hides it.
  4. 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 mermaid label owns the right corner and cross-fades in place with Syntax, Copy and Hide source; Copy is the copy option, 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.

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 }
  }
}
  • theme maps to --pele-* variables with style.setProperty, never a style attribute string. CSSOM writes are not blocked by CSP.
  • No Pele type appears in this surface.
  • limits maps to Pele limit and maxEdges. A missing field keeps its default. The defaults are tuned by the phone measure (D10).
  • Fixed internals, not options: outputLimit 1000000, links: false, no icons resolver. 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:

  1. div.mermaid-diagram__head: the mermaid label 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).
  2. div.mermaid-diagram__source > pre > code: the source, as contentDOM. The wrapper carries the fold tween; the source is hidden only while folded.
  3. view.dom: the built-in preview, or the developer's view result.

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

  1. Insert with slash "Diagram", the Code select, a ```mermaid fence, or a paste of a fence or pre > code.language-mermaid.
  2. The source sits on top and the live diagram below, in one frame. There is no mode toggle.
  3. The diagram redraws after a trailing debounce of about 300 ms.
  4. Errors appear after about 1 s of idle.
  5. Exit with the existing code block keys.
  6. Read-only editors and the history view show the same frame (D2), starting folded (ruling 15).
  7. 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.
  8. At rest, only the mermaid label 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

Risks

  • Pele is about two weeks old. One renderer folder, no Pele types in our API, a ~1.6.0 range, 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-src allows '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

  • REPO is the absolute path of your docs.plus checkout (the folder that holds bun.lock). PKG is REPO/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:styles and bun run format. Each exits 0 with no warnings.
  • Webapp checks: from REPO, bun run --filter @docs.plus/webapp typecheck, then NEXT_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 run git add, commit, push or stash.

Repo rules

Pele

  • Use the latest pele release at the start of Measure Pele render time on a phone and set the default diagram limits #522 (D11). It is 1.6.0 on 2026-10-18, and every 1.6.0 in 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 its dist/*.d.ts before Scaffold @docs.plus/extension-mermaid with its types and the insertMermaid command #523.
  • Pele is part of the extension: the package dependencies hold ~1.6.0, so npm installs it with the extension, and it is never bundled into dist. Root package.json overrides pins the exact "pele": "1.6.0" for this repo (D11). bun.lock must hold exactly one "pele@1.6.0".
  • The renderer's dynamic import('pele/lazy') is the only runtime Pele import in the repo. import type from pele/lazy is allowed in src/renderer/ only. Never import the main pele entry: it registers all 33 types and has a separate PeleError class.
  • The d.ts greps (their home). Run from PKG after bun run build:
    • grep -cE "\b(Mounted|MountOptions|PeleError|RenderResult|DiagramType|DiagramRenderer|RenderOutcome|RendererOptions)\b|from ['\"]pele" dist/index.d.ts dist/index.d.cts prints dist/index.d.ts:0 and dist/index.d.cts:0.
    • grep -c insertMermaid dist/index.d.ts dist/index.d.cts prints 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.ts and types.ts make no runtime import. constants.ts may import type from types.ts.

Behaviour

  • Detection (D3): a diagram is a codeBlock with attrs.language === 'mermaid', exact and case-sensitive. The extension never writes language. 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_LIMITS holds 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).
    • idle starts true.
    • A text change, local or remote, sets idle false, drops any pending error, and restarts the 1000 ms idle timer.
    • The idle timer sets idle true and publishes any pending error.
    • An error outcome publishes at once when idle is 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-error and skipped publish 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 #528 sanitizeDiagramSvg applies it; Measure Pele render time on a phone and set the default diagram limits #522 measures with it.
  • onStateChange runs 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. durationMs is a number on ready and error, and null otherwise.
  • empty means textContent.trim() === ''. It never renders; the renderer is destroyed, so the diagram host is empty. loading is the initial state only.
  • Visibility: one IntersectionObserver per EditorView, with root: null and rootMargin: '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 every await, return when destroyed or stale. Catch every import() and load() rejection inside the renderer: an escaped rejection reloads the pad through apps/webapp/src/utils/chunkLoadRecovery.ts.
  • Error text goes in only through textContent. Nothing inside .ProseMirror uses aria-live, role=status, role=alert or output. The source is never display:none or aria-hidden on screen.
  • Register Mermaid once, in baseExtensions of TipTap.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.

  1. A failed Pele chunk import must not reload the pad. Offline or a dropped network shows load-error with "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 rows offline and offline-prod.
  2. Errors must not flash while typing. Typing from a ready diagram 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 row collab.
  3. A type change or a stale render must not win. Editing flowchart into sequenceDiagram and 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 row type-race.
  4. 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.
  5. insertMermaid must 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 and mobile rows). The "block cannot fit" branch depends on the schema and is not pinned.
  6. A custom view or an emptied block must not keep stale DOM. Emptying the text clears view.diagram and publishes empty; destroy() clears only view.diagram and calls view.destroy. Pins: Add the extension-mermaid playground and clean-room Cypress suite #525 case 6.
  7. 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.ts case 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 (open or folded) sits on the root .mermaid-diagram, per viewer.
  • data-copied sits on .mermaid-diagram__copy for 2200 ms after a copy.
  • data-state sits on view.dom with MermaidState values. Select it as .mermaid-diagram > [data-state].
  • Tokens: --mermaid-diagram-border and --mermaid-diagram-radius only.
  • 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)

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

  1. HMarzban commented on Oct 18, 2026

    @HMarzban
    CollaboratorAuthor

    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.

    1. Pele only. mermaid.js is never a dependency.
    2. D2. The history view renders diagrams too.
    3. D4. docs.plus keeps Pele's default series colours. Developers set colours through the extension config.
    4. D5. The two-tone frame, a design-system catalog row, and the package tokens --mermaid-diagram-border and --mermaid-diagram-radius are approved.
    5. D7. "Diagram" is the last item in the desktop Code select.
    6. Headless, DX first. Developers configure the extension and connect it to their editor, like the other five packages.
    7. Headless means the house pattern. It works with zero config, every UI part is replaceable, and styles.css is optional.
    8. D1. Design for outside developers first. Release it as a standard OSS package that follows Tiptap and ProseMirror conventions, naming and layering.
    9. D12. The name is @docs.plus/extension-mermaid. Promote Pele and credit its author, Steph Ango (@kepano), as CEO of Obsidian.
    10. D3, D6, D8, D9 and D10 as recommended: exact mermaid only; no PWA precache of Pele chunks; DOMPurify in the webapp and a sanitize option; autoDirection on; measure on a phone and lower the default limits if p99 is over about 200 ms, with no worker.
    11. Limits are configurable, with defaults, through the extension config.
    12. R10 (D11). Use the latest Pele release. It is part of the extension's dependencies. The repo pins the exact version with a root overrides entry.
    13. R11. The webapp reports a load-error once per page load.
    14. No early error on caret leave. An error shows only after about 1 s of idle.
  2. HMarzban commented on Oct 18, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling update, 2026-10-18

    Made on the interactive UI preview. The tracker body quotes each one as rulings 15 to 17.

    1. Fold the source, per viewer. Writers start open. Read-only views (history included) and visitors start folded. Each viewer's own fold or unfold is remembered in that browser. Nothing is stored in the document, so collaborators never see another viewer's fold. The cursor entering a folded block opens it. The extension option is fold (initial, remember, or false).
    2. Insert an example, never an empty box. The toolbar and the slash menu insert the four-line starter diagram.
    3. A Syntax button on hover. It opens the Mermaid syntax guide in a new tab. It is the extension option helpLink: on by default (the Mermaid syntax reference), and null hides it. On phones it is always visible.

    Updated: #521 (design, public API, rulings), #523 (options), #524 (node view and CSS), #525 (fold.cy.ts and sabotage S6), #526 (docs), #528 (design-system row and tokens), #529 (Fold row).

  3. HMarzban commented on Oct 18, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling update, 2026-10-18 — ruling 18

    The maintainer approved preview version 4 ("approve all"). The issue bodies now carry it.

    • Head. At rest, only the mermaid label 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.
    • Copy. A new copy option, true by default. The button puts the whole source on the clipboard. Its icon turns into a check for 2200 ms, marked by data-copied. It never dispatches a transaction.
    • Motion. The literals match the house tokens: 120 ms in, 80 ms out, 200 ms for the fold, 2200 ms for the check. The fold is a grid-template-rows tween on a new div.mermaid-diagram__source wrapper. Reduced motion turns all of it off.
    • Loading and errors. Loading shows the inherited-ink spinner, never a shimmer. The error detail drops blank lines.

    Changed bodies: #521 (ruling 18, public API, DOM, edit flow, CSS contract), #523 (defaults), #524 (head, Copy, CSS), #525 (?bare and 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.

  4. HMarzban commented on Oct 19, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling, 2026-10-19

    Rulings on the open Mermaid decisions:

    • R1, R2, R3, R4, R5, R7 and R9: confirmed as built. R3 is the next.config.js PWA no-precache predicate. R9 is docs/diagrams.md, linked from docs/README.md.
    • Dispatch exception: accepted. The plugin key handler dispatches a selection, so ArrowUp or ArrowDown into a folded block opens it (ruling 15). The node view still never dispatches. This is a deliberate exception to §Shared contract → Behaviour.
    • realPress in fold.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.
    • Source split: accepted. §File map now lists nodeView/fold.ts, nodeView/renderLoop.ts, the view helpers and src/renderer/types.ts.
    • TOC preview clone move into toc/utils/sectionPreviewClone.ts (Draw Mermaid diagrams in the pad, history and /editor #528): accepted.
    • Default limits: do not ship 2.2.0 until the real phone run (Measure Pele render time on a phone and set the default diagram limits #522 Step 8) confirms or lowers characters: 15000 and edges: 500.
    • Label font: mono in the package CSS too, to match the approved design row.
    • Error line mapping: accept the known limits. The maintainer asks Pele upstream to expose lineOffset later.
    • Discord badge: the package README now carries the new invite from Replace the expired Discord invite in the docs and the app #480.

    The work is in the maintainer's working tree and is not committed or pushed yet.

  5. HMarzban commented on Oct 19, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling, 2026-10-19 (ship gates)

    These record exceptions to the contract above, which the #530 sweep found.

    1. The MCP server imports pele. apps/hocuspocus.server/src/modules/mcp/domain/checkDiagrams.ts runs await 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 in src/renderer/" rule covers the extension and the webapp bundle; it does not cover this server path. Accepted.
    2. src/hashSource.ts is part of the accepted source split. nodeView/fold.ts and nodeView/crashMarker.ts import it for the per-viewer fold key and the crash marker. §Layering did not list it. Accepted as part of the split ruling.
    3. Three CSS lines in src/styles/mermaid.css. :focus-visible on the head buttons, plus .pele-zoom { display: none !important } and contain: none !important inside @media print. The print lines hide Pele's zoom controls on paper and undo the host's inline contain: paint, which would otherwise clip the printed diagram. Accepted.

    The limit test no longer imports the main pele entry (1258a9d42). So inside the package, the renderer is still the only runtime import.

  6. HMarzban commented on Oct 21, 2026

    @HMarzban
    CollaboratorAuthor

    Pele pin update, 2026-10-20

    §Global constraints → Pele names 1.6.0. The pin is now 1.7.0 everywhere (aebd55866, deployed in 3784c2c65). Read every 1.6.0 in these issues as 1.7.0. bun.lock holds exactly one pele@1.7.0.

    The checks are on #531 Step 1. One fact in §Global constraints is not what the code does: the package dependencies pin the exact version, not a ~ range.

    Records of past measurements keep pele@1.6.0 on purpose, because that was the version measured: docs/pele.md (the 0.90 ms starter) and docs/security.md (the DOMPurify probe).

  7. HMarzban commented on Oct 22, 2026

    @HMarzban
    CollaboratorAuthor

    All work in this tracker shipped in docs.plus 2.2.0, and every child issue (#522 to #531) is closed.

    • A mermaid code block now draws a live diagram under its source, in the pad, in History and in /editor. Pele draws it.
    • The size limits are 10,000 characters and 250 edges. They were measured on a real iPhone and a real Android phone (Measure Pele render time on a phone and set the default diagram limits #522).
    • @docs.plus/extension-mermaid is 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions