Pasting Mermaid code as shapes

Paste mermaid diagrams!

import { useEffect } from 'react'
import { Tldraw } from 'tldraw'
import { defaultHandleExternalTextContent, useEditor, useToasts } from 'tldraw'

export default function MermaidDiagrams() {
	return (
		<div className="tldraw__editor">
			<Tldraw>
				<SneakyMermaidHandler />
			</Tldraw>
		</div>
	)
}

export function SneakyMermaidHandler() {
	const editor = useEditor()
	const { addToast } = useToasts()

	useEffect(() => {
		editor.registerExternalContentHandler('text', async (content) => {
			// when pasting html, the derived text is stripped and loses line breaks
			// which make evaluating mermaid diagrams impossible. We look into the
			// sources and see if there's some plain text alongside the html and if
			// there are, we can use this to evaluate mermaid diagrams.

			const plainTextSource = content.sources?.find(
				(s) => s.type === 'text' && s.subtype === 'text'
			)
			const plainText = plainTextSource?.data ?? content.text
			const textToTest = simpleMermaidStringTest(plainText) ? plainText : content.text

			if (!simpleMermaidStringTest(textToTest)) {
				await defaultHandleExternalTextContent(editor, content)
				return
			}
			const mermaidText = stripMarkdownMermaidFence(textToTest)
			const { createMermaidDiagram } = await import('@tldraw/mermaid')
			const shapesBefore = new Set(editor.getCurrentPageShapeIds())

			const selectNewShapes = () => {
				const newShapeIds = [...editor.getCurrentPageShapeIds()].filter(
					(id) => !shapesBefore.has(id)
				)
				if (newShapeIds.length) {
					editor.setSelectedShapes(newShapeIds)
				}
			}

			try {
				const onUnsupportedDiagram = async (svgString: string) => {
					await editor.putExternalContent({
						type: 'svg-text',
						text: svgString,
						point: content.point,
						sources: content.sources,
					})
					addToast({
						id: 'unsupported-mermaid-diagram',
						title: 'Unsupported mermaid diagram',
						description: 'This diagram is not supported yet',
						severity: 'warning',
					})
				}

				await createMermaidDiagram(editor, mermaidText, { onUnsupportedDiagram })
				selectNewShapes()
			} catch (e) {
				console.error(e)
				await defaultHandleExternalTextContent(editor, content)
			}
		})
	}, [editor, addToast])

	return null
}

/**
 * Lightweight mermaid detection replicating mermaid's own detectType preprocessing
 * (from mermaid v11.12.2 src/diagram-api/detectType.ts and src/diagram-api/regexes.ts).
 *
 * https://github.com/mermaid-js/mermaid/blob/277c4967f97405e9bb172c0a2f67f462a672b162/packages/mermaid/src/diagram-api/detectType.ts
 * https://github.com/mermaid-js/mermaid/blob/277c4967f97405e9bb172c0a2f67f462a672b162/packages/mermaid/src/diagram-api/regexes.ts
 *
 * Strips YAML frontmatter, %%{...}%% directives, and %% comments, then tests for
 * a known diagram keyword at the start of the cleaned text.
 *
 * This file intentionally has zero imports so it can be loaded statically without
 * pulling in the heavy mermaid library.
 */
const FRONTMATTER_REGEX = /^-{3}\s*[\n\r]([\s\S]*?)[\n\r]-{3}\s*[\n\r]+/
const DIAGRAM_KEYWORD_REGEX =
	/^\s*(flowchart|graph|sequenceDiagram|classDiagram|stateDiagram|erDiagram|journey|gantt|pie|gitGraph|mindmap|timeline|sankey|xychart|block|quadrantChart|requirement|C4Context|C4Container|C4Component|C4Dynamic|C4Deployment|packet|kanban|architecture|treemap|radar|info)/

/**
 * Leading ```mermaid (or longer run) fence, closed by the first line that ends
 * the same run length (CommonMark-style: inner shorter ``` lines do not close a
 * longer fence). Trailing markdown after the block is allowed so multi-block
 * pastes do not pull in later fences. Group 1 = fence run, group 2 = diagram body.
 */
const MARKDOWN_MERMAID_FENCE_REGEX =
	/^\s*(```+)\s*mermaid\s*\r?\n([\s\S]*?)\r?\n\s*\1\s*(?:[\s\S]*)$/

/**
 * Strip mermaid boilerplate (frontmatter, directives, comments) so only the
 * diagram body remains. The two global regexes are created as fresh literals
 * each call to avoid the stateful-lastIndex footgun of module-level /g regexes.
 */
function stripMermaidBoilerplate(text: string): string {
	return text
		.replace(FRONTMATTER_REGEX, '')
		.replace(/%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi, '')
		.replace(/\s*%%.*\n/gm, '\n')
}

export function stripMarkdownMermaidFence(text: string): string {
	const match = text.match(MARKDOWN_MERMAID_FENCE_REGEX)
	return match ? match[2] : text
}

export function simpleMermaidStringTest(text: string): boolean {
	return DIAGRAM_KEYWORD_REGEX.test(stripMermaidBoilerplate(stripMarkdownMermaidFence(text)))
}

This example shows how to handle the pasting of mermaid diagrams into the canvas.

Supported diagrams:

  • Flowcharts
  • State
  • Sequence
  • Mindmap

Example:

graph TD
    A[Start] --> B{Is it correct LALA?}
    B -- Yes --> C[Display Diagram]
    B -- No --> D[Edit Code]
    D --> B
    C --> E[End]
Is this page helpful?
Prev
Hundreds of Mermaid Diagrams
Next
Snowstorm