Skip to content

Commit 0fca0ee

Browse files
nperez0111matthewlipski
authored andcommitted
feat: diagram block
Add `@blocknote/diagram-block`: a Mermaid diagram block built on the source-with-preview primitives. Includes exporter mappings for HTML, markdown, docx, odt, pdf and email, an example, and documentation.
1 parent 2a34f7d commit 0fca0ee

75 files changed

Lines changed: 3657 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
title: Diagrams
3+
description: Mermaid diagram blocks for BlockNote — rendered as diagrams in the editor, and exportable to Markdown, PDF, DOCX, ODT, and email.
4+
---
5+
6+
# Diagrams
7+
8+
The `@blocknote/diagram-block` package adds a **diagram block**: authored as [Mermaid](https://mermaid.js.org/) source in a source popup, rendered as the diagram it describes — flowcharts, sequence diagrams, Gantt charts, and everything else Mermaid supports.
9+
10+
<Callout type="info">
11+
This block is only available in React (`@blocknote/react`).
12+
</Callout>
13+
14+
```bash
15+
npm install @blocknote/diagram-block
16+
```
17+
18+
## Adding to your editor
19+
20+
The package exports `createReactDiagramBlockSpec`. Add it to your schema's `blockSpecs`:
21+
22+
```tsx
23+
import { BlockNoteSchema } from "@blocknote/core";
24+
import { createReactDiagramBlockSpec } from "@blocknote/diagram-block";
25+
26+
const schema = BlockNoteSchema.create().extend({
27+
blockSpecs: {
28+
// Adds the Diagram block to the schema.
29+
diagram: createReactDiagramBlockSpec(),
30+
},
31+
});
32+
```
33+
34+
To highlight the Mermaid source in the popup, add the [syntax highlighting](/docs/features/blocks/code-blocks#syntax-highlighting) extension to your editor. The diagram block already declares its source language (`mermaid`), so no per-block configuration is needed:
35+
36+
```tsx
37+
import { syntaxHighlighter } from "@blocknote/code-block";
38+
39+
const editor = useCreateBlockNote({
40+
schema,
41+
extensions: [syntaxHighlighter],
42+
});
43+
```
44+
45+
## Menu items & localization
46+
47+
Because the diagram spec lives in an optional package, its editor integrations are opt-in too — the package exports everything needed:
48+
49+
```tsx
50+
import {
51+
getDiagramSlashMenuItems, // Slash Menu item for inserting a diagram
52+
getDiagramBlockTypeSelectItems, // Block Type Select item for the Formatting Toolbar
53+
locales as diagramLocales, // dictionary strings, merged under the `diagram` key
54+
} from "@blocknote/diagram-block";
55+
```
56+
57+
- `getDiagramSlashMenuItems(editor)` returns a [Slash Menu](/docs/react/components/suggestion-menus#slash-menu) item for inserting a diagram — combine it with the default items via `combineByGroup`.
58+
- `getDiagramBlockTypeSelectItems(editor)` returns a [Block Type Select](/docs/react/components/formatting-toolbar) item for turning a block into a diagram, to spread alongside the defaults.
59+
- `diagramLocales` translates the diagram strings — merge a locale into the editor's `dictionary` under the `diagram` key (see [Localization](/docs/features/localization)); without one, the bundled English strings are used.
60+
61+
The example below wires them all up.
62+
63+
## Example
64+
65+
<Example name="custom-schema/diagram-block" />
66+
67+
## Exporting
68+
69+
Diagrams export to every format BlockNote supports. [Markdown](/docs/features/export/markdown) works out of the box — diagrams export as ` ```mermaid ` fenced code blocks, their common Markdown notation.
70+
71+
The [PDF](/docs/features/export/pdf), [DOCX](/docs/features/export/docx), [ODT](/docs/features/export/odt), and [email](/docs/features/export/email) exporters embed the diagram as an image via their mappings — they live as subpaths of this package, and each exports a `createDiagramBlockMapping` factory to spread into the exporter's default mappings. The [DOCX exporter](/docs/features/export/docx) shown here; the [PDF](/docs/features/export/pdf), [ODT](/docs/features/export/odt), and [email](/docs/features/export/email) exporters work the same way with their respective subpaths:
72+
73+
```typescript
74+
import {
75+
DOCXExporter,
76+
docxDefaultSchemaMappings,
77+
} from "@blocknote/xl-docx-exporter";
78+
import { createDiagramBlockMapping } from "@blocknote/diagram-block/docx-exporter";
79+
// ...or "@blocknote/diagram-block/pdf-exporter",
80+
// "@blocknote/diagram-block/odt-exporter",
81+
// "@blocknote/diagram-block/email-exporter"
82+
83+
const exporter = new DOCXExporter(editor.schema, {
84+
...docxDefaultSchemaMappings,
85+
blockMapping: {
86+
...docxDefaultSchemaMappings.blockMapping,
87+
diagram: createDiagramBlockMapping(),
88+
},
89+
});
90+
```
91+
92+
The factory takes one option:
93+
94+
```typescript
95+
createDiagramBlockMapping(options?: {
96+
/**
97+
* Renders the Mermaid source to an image. Defaults to the built-in
98+
* Mermaid renderer, which only works in the browser - see "Exporting
99+
* server-side" below.
100+
*/
101+
renderDiagram?: RenderDiagram;
102+
});
103+
```
104+
105+
Invalid Mermaid sources render an error placeholder identifying the offending source, mirroring the editor.
106+
107+
The email subpath's factory additionally takes an `imageDelivery` option: some email clients don't display the default data URL images, and the generated images can be delivered as inline `cid:` attachments instead — see [image delivery](/docs/features/export/email#math--diagram-blocks) on the email page.
108+
109+
### Exporting server-side
110+
111+
Rendering Mermaid source to an image requires a browser, so the built-in renderer only works for client-side exports. When exporting server-side, pass a `renderDiagram` function to `createDiagramBlockMapping` — without one, a server-side export throws:
112+
113+
```typescript
114+
import { createDiagramBlockMapping } from "@blocknote/diagram-block/docx-exporter";
115+
import type { RenderDiagram } from "@blocknote/diagram-block/docx-exporter";
116+
117+
const renderDiagram: RenderDiagram = async (source) => {
118+
// Render the Mermaid source to an image with your renderer of choice.
119+
return {
120+
image: { data: pngBytes, mimeType: "image/png", width, height },
121+
};
122+
};
123+
124+
createDiagramBlockMapping({ renderDiagram });
125+
```
126+
127+
Common choices for the server-side renderer are [`@mermaid-js/mermaid-cli`](https://github.com/mermaid-js/mermaid-cli) (renders in a headless browser) or a [Kroki](https://kroki.io) server. Invalid Mermaid source is an expected failure — return it as `{ error }` rather than throwing, and the export renders the error placeholder for that block instead of failing.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"playground": true,
3+
"docs": true,
4+
"author": "yousefed",
5+
"tags": [
6+
"Intermediate",
7+
"Blocks",
8+
"Custom Schemas",
9+
"Suggestion Menus",
10+
"Slash Menu"
11+
],
12+
"dependencies": {
13+
"@blocknote/code-block": "latest",
14+
"@blocknote/diagram-block": "latest",
15+
"react-icons": "^5.5.0"
16+
}
17+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Diagram Block
2+
3+
In this example, we register the `@blocknote/diagram-block` block in a custom schema. The block renders diagrams from [Mermaid](https://mermaid.js.org/) source code, showing the rendered diagram in place of the source and revealing an editable source popup when selected - built from the same `SourceBlockWithPreview` component the math block uses, so the block itself is only a few dozen lines.
4+
5+
**Try it out:** Click a diagram to edit its Mermaid source!
6+
7+
**Relevant Docs:**
8+
9+
- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)
10+
- [Editor Setup](/docs/getting-started/editor-setup)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<html lang="en">
2+
<head>
3+
<meta charset="UTF-8" />
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
5+
<title>Diagram Block</title>
6+
<script>
7+
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
8+
</script>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="./main.tsx"></script>
13+
</body>
14+
</html>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
2+
import React from "react";
3+
import { createRoot } from "react-dom/client";
4+
import App from "./src/App.jsx";
5+
6+
const root = createRoot(document.getElementById("root")!);
7+
root.render(
8+
<React.StrictMode>
9+
<App />
10+
</React.StrictMode>,
11+
);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "@blocknote/example-custom-schema-diagram-block",
3+
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
4+
"type": "module",
5+
"private": true,
6+
"version": "0.12.4",
7+
"scripts": {
8+
"start": "vite",
9+
"dev": "vite",
10+
"build:prod": "tsc && vite build",
11+
"preview": "vite preview"
12+
},
13+
"dependencies": {
14+
"@blocknote/ariakit": "latest",
15+
"@blocknote/core": "latest",
16+
"@blocknote/mantine": "latest",
17+
"@blocknote/react": "latest",
18+
"@blocknote/shadcn": "latest",
19+
"@mantine/core": "^9.0.2",
20+
"@mantine/hooks": "^9.0.2",
21+
"react": "^19.2.3",
22+
"react-dom": "^19.2.3",
23+
"@blocknote/code-block": "latest",
24+
"@blocknote/diagram-block": "latest",
25+
"react-icons": "^5.5.0"
26+
},
27+
"devDependencies": {
28+
"@types/react": "^19.2.3",
29+
"@types/react-dom": "^19.2.3",
30+
"@vitejs/plugin-react": "^6.0.1",
31+
"vite": "^8.0.0"
32+
}
33+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { syntaxHighlighter } from "@blocknote/code-block";
2+
import { BlockNoteSchema, combineByGroup } from "@blocknote/core";
3+
import { filterSuggestionItems } from "@blocknote/core/extensions";
4+
import * as locales from "@blocknote/core/locales";
5+
import "@blocknote/core/fonts/inter.css";
6+
import { BlockNoteView } from "@blocknote/mantine";
7+
import "@blocknote/mantine/style.css";
8+
import {
9+
createReactDiagramBlockSpec,
10+
getDiagramBlockTypeSelectItems,
11+
getDiagramSlashMenuItems,
12+
locales as diagramLocales,
13+
} from "@blocknote/diagram-block";
14+
import {
15+
blockTypeSelectItems,
16+
FormattingToolbar,
17+
FormattingToolbarController,
18+
getDefaultReactSlashMenuItems,
19+
SuggestionMenuController,
20+
useCreateBlockNote,
21+
} from "@blocknote/react";
22+
23+
// Our schema with block specs, which contain the configs and implementations
24+
// for blocks that we want our editor to use.
25+
const schema = BlockNoteSchema.create().extend({
26+
blockSpecs: {
27+
// Creates an instance of the Diagram block and adds it to the schema.
28+
diagram: createReactDiagramBlockSpec(),
29+
},
30+
});
31+
32+
export default function App() {
33+
const editor = useCreateBlockNote({
34+
// The syntax highlighter extension highlights the Diagram block's Mermaid
35+
// source in its popup (the block declares `highlight: () => "mermaid"`).
36+
extensions: [syntaxHighlighter],
37+
schema,
38+
// Merges the default dictionary with the diagram dictionary, under the
39+
// `diagram` key the diagram block reads its strings from.
40+
dictionary: {
41+
...locales.en,
42+
diagram: diagramLocales.en,
43+
},
44+
initialContent: [
45+
{
46+
type: "paragraph",
47+
content: "Click a diagram to edit its Mermaid source:",
48+
},
49+
{
50+
type: "diagram",
51+
content: `graph TD
52+
A[Write docs] --> B{Diagram needed?}
53+
B -->|Yes| C[Type /diagram]
54+
B -->|No| D[Keep writing]
55+
C --> D`,
56+
},
57+
{
58+
type: "paragraph",
59+
content: "Press the '/' key to open the Slash Menu and add another",
60+
},
61+
],
62+
});
63+
64+
// Renders the editor instance using a React component.
65+
return (
66+
<BlockNoteView editor={editor} slashMenu={false} formattingToolbar={false}>
67+
{/* Replaces the default Formatting Toolbar, adding the Diagram block to
68+
the block type select so blocks can be converted to it. */}
69+
<FormattingToolbarController
70+
formattingToolbar={() => (
71+
<FormattingToolbar
72+
blockTypeSelectItems={[
73+
...blockTypeSelectItems(editor.dictionary),
74+
...getDiagramBlockTypeSelectItems(editor),
75+
]}
76+
/>
77+
)}
78+
/>
79+
{/* Replaces the default Slash Menu. */}
80+
<SuggestionMenuController
81+
triggerCharacter={"/"}
82+
getItems={async (query) => {
83+
// Gets the default slash menu items and adds the Diagram item at
84+
// the end of its group ("Advanced").
85+
const items = combineByGroup(
86+
getDefaultReactSlashMenuItems(editor),
87+
getDiagramSlashMenuItems(editor),
88+
);
89+
90+
// Returns filtered items based on the query.
91+
return filterSuggestionItems(items, query);
92+
}}
93+
/>
94+
</BlockNoteView>
95+
);
96+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
3+
"compilerOptions": {
4+
"target": "ESNext",
5+
"useDefineForClassFields": true,
6+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
7+
"allowJs": false,
8+
"skipLibCheck": true,
9+
"allowSyntheticDefaultImports": true,
10+
"strict": true,
11+
"forceConsistentCasingInFileNames": true,
12+
"module": "ESNext",
13+
"moduleResolution": "bundler",
14+
"resolveJsonModule": true,
15+
"isolatedModules": true,
16+
"noEmit": true,
17+
"jsx": "react-jsx",
18+
"composite": true,
19+
"paths": {
20+
"@shared/*": ["../../../shared/*"]
21+
}
22+
},
23+
"include": ["."],
24+
"__ADD_FOR_LOCAL_DEV_references": [
25+
{
26+
"path": "../../../packages/core/"
27+
},
28+
{
29+
"path": "../../../packages/react/"
30+
}
31+
]
32+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
2+
import react from "@vitejs/plugin-react";
3+
import * as fs from "fs";
4+
import * as path from "path";
5+
import { defineConfig } from "vite";
6+
// https://vitejs.dev/config/
7+
export default defineConfig(((conf: { command: string }) => ({
8+
plugins: [react()],
9+
optimizeDeps: {},
10+
build: {
11+
sourcemap: true,
12+
},
13+
resolve: {
14+
alias:
15+
conf.command === "build" ||
16+
!fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
17+
? {}
18+
: ({
19+
// The repo-wide alias for the shared test-utils directory (private,
20+
// so it only resolves inside the monorepo). Harmless for examples
21+
// that don't use it.
22+
"@shared": path.resolve(__dirname, "../../../shared/"),
23+
// Comment out the lines below to load a built version of blocknote
24+
// or, keep as is to load live from sources with live reload working
25+
"@blocknote/core": path.resolve(
26+
__dirname,
27+
"../../packages/core/src/",
28+
),
29+
"@blocknote/react": path.resolve(
30+
__dirname,
31+
"../../packages/react/src/",
32+
),
33+
} as any),
34+
},
35+
})) as Parameters<typeof defineConfig>[0]);

0 commit comments

Comments
 (0)