Skip to content

Commit 2a34f7d

Browse files
nperez0111matthewlipski
authored andcommitted
feat: math block
Add `@blocknote/math-block`: a LaTeX math block and inline math content type rendered with KaTeX, built on the source-with-preview primitives. Includes exporter mappings for HTML, markdown, docx, odt, pdf and email, an example, and documentation.
1 parent 503c796 commit 2a34f7d

103 files changed

Lines changed: 6118 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
---
2+
title: Math & Equations
3+
description: LaTeX math blocks and inline math for BlockNote — rendered as formulas in the editor, and exportable to Markdown, PDF, DOCX, ODT, and email.
4+
---
5+
6+
# Math & Equations
7+
8+
The `@blocknote/math-block` package adds mathematical notation to your documents: a **math block** for standalone equations and **inline math** that flows with the surrounding text. Both are authored as LaTeX in a source popup and rendered as formulas — [KaTeX](https://katex.org/) converts the LaTeX to MathML, which browsers display natively.
9+
10+
<Callout type="info">
11+
This block is only available in React (`@blocknote/react`).
12+
</Callout>
13+
14+
```bash
15+
npm install @blocknote/math-block
16+
```
17+
18+
## Adding to your editor
19+
20+
The package exports `createReactMathBlockSpec` (the block) and `createReactInlineMathSpec` (the inline content). Add them to your schema's `blockSpecs` and `inlineContentSpecs` respectively:
21+
22+
```tsx
23+
import { BlockNoteSchema } from "@blocknote/core";
24+
import {
25+
createReactMathBlockSpec,
26+
createReactInlineMathSpec,
27+
} from "@blocknote/math-block";
28+
29+
const schema = BlockNoteSchema.create().extend({
30+
blockSpecs: {
31+
// Adds the Math block to the schema.
32+
mathBlock: createReactMathBlockSpec(),
33+
},
34+
inlineContentSpecs: {
35+
// Adds the inline Math content to the schema.
36+
math: createReactInlineMathSpec(),
37+
},
38+
});
39+
```
40+
41+
To highlight the LaTeX source in the popup, add the [syntax highlighting](/docs/features/blocks/code-blocks#syntax-highlighting) extension to your editor. The math block and inline math already declare their source language (`latex`), so no per-block configuration is needed:
42+
43+
```tsx
44+
import { syntaxHighlighter } from "@blocknote/code-block";
45+
46+
const editor = useCreateBlockNote({
47+
schema,
48+
extensions: [syntaxHighlighter],
49+
});
50+
```
51+
52+
## Menu items & localization
53+
54+
Because the math specs live in an optional package, their editor integrations are opt-in too — the package exports everything needed:
55+
56+
```tsx
57+
import {
58+
getMathSlashMenuItems, // Slash Menu items for inserting math
59+
getMathBlockTypeSelectItems, // Block Type Select item for the Formatting Toolbar
60+
locales as mathLocales, // dictionary strings, merged under the `math` key
61+
} from "@blocknote/math-block";
62+
```
63+
64+
- `getMathSlashMenuItems(editor)` returns [Slash Menu](/docs/react/components/suggestion-menus#slash-menu) items for inserting a math block or inline math — combine them with the default items via `combineByGroup`.
65+
- `getMathBlockTypeSelectItems(editor)` returns a [Block Type Select](/docs/react/components/formatting-toolbar) item for turning a block into a math block, to spread alongside the defaults.
66+
- `mathLocales` translates the math strings — merge a locale into the editor's `dictionary` under the `math` key (see [Localization](/docs/features/localization)); without one, the bundled English strings are used.
67+
68+
The example below wires them all up.
69+
70+
## Example
71+
72+
<Example name="custom-schema/math-block" />
73+
74+
## Exporting
75+
76+
Math exports to every format BlockNote supports:
77+
78+
- **HTML** works out of the box — the export produces a native MathML `<math>` element (with the LaTeX embedded for lossless round-trips), and pasting MathML back in converts to LaTeX.
79+
- **[Markdown](/docs/features/export/markdown)** also works out of the box — math blocks export as `$$` blocks and inline math as `$...$` spans, their common Markdown notations.
80+
- **PDF, DOCX, ODT, and email** use exporter mappings, which live as subpaths of this package — spread them into the default mappings of the exporter you use, as shown per format below. Invalid LaTeX renders an error placeholder identifying the offending source, mirroring the editor.
81+
82+
### DOCX
83+
84+
With the [DOCX exporter](/docs/features/export/docx), math exports as native (editable) Word equations. Works server-side out of the box — the LaTeX is converted to OMML without rendering:
85+
86+
```typescript
87+
import {
88+
DOCXExporter,
89+
docxDefaultSchemaMappings,
90+
} from "@blocknote/xl-docx-exporter";
91+
import {
92+
inlineMathMapping,
93+
mathBlockMapping,
94+
} from "@blocknote/math-block/docx-exporter";
95+
96+
const exporter = new DOCXExporter(editor.schema, {
97+
...docxDefaultSchemaMappings,
98+
blockMapping: {
99+
...docxDefaultSchemaMappings.blockMapping,
100+
mathBlock: mathBlockMapping,
101+
},
102+
inlineContentMapping: {
103+
...docxDefaultSchemaMappings.inlineContentMapping,
104+
math: inlineMathMapping,
105+
},
106+
});
107+
```
108+
109+
### ODT
110+
111+
With the [ODT exporter](/docs/features/export/odt), math exports as native (editable) formula objects. Also works server-side out of the box (LaTeX is converted to MathML without rendering):
112+
113+
```typescript
114+
import {
115+
inlineMathMapping,
116+
mathBlockMapping,
117+
} from "@blocknote/math-block/odt-exporter";
118+
119+
// Spread into the ODTExporter's mappings exactly as for DOCX above.
120+
```
121+
122+
### PDF
123+
124+
With the [PDF exporter](/docs/features/export/pdf), math blocks export as vector formulas — no rasterization, so they also work server-side out of the box. Inline math is rasterized to images that flow with the text:
125+
126+
```typescript
127+
import {
128+
createInlineMathMapping,
129+
mathBlockMapping,
130+
} from "@blocknote/math-block/pdf-exporter";
131+
132+
// Spread into the PDFExporter's mappings as for DOCX above - note that
133+
// inline math is a factory here: `math: createInlineMathMapping()`.
134+
```
135+
136+
The inline math factory takes one option:
137+
138+
```typescript
139+
createInlineMathMapping(options?: {
140+
/**
141+
* Rasterizes the formula SVG to an image. Defaults to the built-in
142+
* canvas rasterizer, which only works in the browser - when exporting
143+
* server-side, pass one backed by e.g. `@resvg/resvg-js` or `sharp`;
144+
* without it, a server-side export throws. The `RasterizeSVG` type is
145+
* exported from the same subpath.
146+
*/
147+
rasterize?: RasterizeSVG;
148+
});
149+
```
150+
151+
Math blocks require the `@react-pdf/math` package (a peer dependency of the PDF mapping).
152+
153+
### Email
154+
155+
With the [email exporter](/docs/features/export/email), math exports as images with the LaTeX source as the alt text: math blocks are rasterized to PNG in the browser (and embedded as SVG elsewhere), inline math is always embedded as SVG:
156+
157+
```typescript
158+
import {
159+
createInlineMathMapping,
160+
createMathBlockMapping,
161+
} from "@blocknote/math-block/email-exporter";
162+
163+
// Spread into the ReactEmailExporter's mappings as for DOCX above - both
164+
// are factories here: `mathBlock: createMathBlockMapping()` and
165+
// `math: createInlineMathMapping()`.
166+
```
167+
168+
Both factories take delivery-related options:
169+
170+
```typescript
171+
createMathBlockMapping(options?: {
172+
/**
173+
* Rasterizes the formula SVG to a raster image. Defaults to the built-in
174+
* canvas rasterizer in the browser; elsewhere (e.g. server-side email
175+
* rendering at send time), the formula is embedded as an SVG instead -
176+
* pass a rasterizer (e.g. backed by `@resvg/resvg-js` or `sharp`) to get
177+
* PNGs there, which more email clients display.
178+
*/
179+
rasterize?: RasterizeSVG;
180+
/**
181+
* How generated images get into the email: embedded as data URLs by
182+
* default, or as inline `cid:` attachments - see the email exporter's
183+
* image delivery docs.
184+
*/
185+
imageDelivery?: ReactEmailImageDelivery;
186+
});
187+
188+
// `createInlineMathMapping` takes the same `imageDelivery` option (inline
189+
// math is always SVG, so there's no `rasterize`).
190+
```
191+
192+
Some email clients don't display data URL images — see [image delivery](/docs/features/export/email#math--diagram-blocks) on the email page for delivering the generated images as inline `cid:` attachments instead.
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": "matthewlipski",
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/math-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+
# Math Block
2+
3+
In this example, we register the `@blocknote/math-block` block in a custom schema. The math block renders LaTeX as MathML (using Temml) for the browser to display natively, and reveals an editable LaTeX source popup when selected. Exporting to HTML produces a MathML `<math>` element, and pasting MathML back in is converted to LaTeX.
4+
5+
**Try it out:** Click a formula to edit its LaTeX!
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>Math 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-math-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/math-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: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import "@blocknote/core/fonts/inter.css";
2+
import { BlockNoteSchema, combineByGroup } from "@blocknote/core";
3+
import { filterSuggestionItems } from "@blocknote/core/extensions";
4+
import * as locales from "@blocknote/core/locales";
5+
import { syntaxHighlighter } from "@blocknote/code-block";
6+
import {
7+
createReactInlineMathSpec,
8+
createReactMathBlockSpec,
9+
getMathBlockTypeSelectItems,
10+
getMathSlashMenuItems,
11+
locales as mathLocales,
12+
} from "@blocknote/math-block";
13+
import { BlockNoteView } from "@blocknote/mantine";
14+
import "@blocknote/mantine/style.css";
15+
import {
16+
blockTypeSelectItems,
17+
FormattingToolbar,
18+
FormattingToolbarController,
19+
getDefaultReactSlashMenuItems,
20+
SuggestionMenuController,
21+
useCreateBlockNote,
22+
} from "@blocknote/react";
23+
24+
// Our schema with block specs, which contain the configs and implementations for blocks
25+
// that we want our editor to use.
26+
const schema = BlockNoteSchema.create().extend({
27+
blockSpecs: {
28+
// Creates an instance of the Math block and adds it to the schema.
29+
mathBlock: createReactMathBlockSpec(),
30+
},
31+
inlineContentSpecs: {
32+
// Creates an instance of the inline Math content and adds it to the schema.
33+
math: createReactInlineMathSpec(),
34+
},
35+
});
36+
37+
export default function App() {
38+
const editor = useCreateBlockNote({
39+
// The syntax highlighter extension highlights the LaTeX source of math
40+
// blocks (they declare `highlight: () => "latex"`). Without it, they render
41+
// as plain text.
42+
extensions: [syntaxHighlighter],
43+
schema,
44+
// Merges the default dictionary with the math dictionary, under the `math`
45+
// key the math block/inline content read their strings from.
46+
dictionary: {
47+
...locales.en,
48+
math: mathLocales.en,
49+
},
50+
initialContent: [
51+
{
52+
type: "paragraph",
53+
content: "Click a formula to edit its LaTeX source:",
54+
},
55+
{
56+
type: "mathBlock",
57+
content: "a^2 = \\sqrt{b^2 + c^2}",
58+
},
59+
{
60+
type: "mathBlock",
61+
content: "\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}",
62+
},
63+
{
64+
type: "paragraph",
65+
content: [
66+
"Equations can also be inline, like ",
67+
{ type: "math", content: "e^{i\\pi} + 1 = 0" },
68+
". Click one to edit its LaTeX source.",
69+
],
70+
},
71+
{
72+
type: "paragraph",
73+
content: "Press the '/' key to open the Slash Menu and add another",
74+
},
75+
],
76+
});
77+
78+
// Renders the editor instance using a React component.
79+
return (
80+
<BlockNoteView editor={editor} slashMenu={false} formattingToolbar={false}>
81+
{/* Replaces the default Formatting Toolbar, adding the Math block to the
82+
block type select so blocks can be converted to it. */}
83+
<FormattingToolbarController
84+
formattingToolbar={() => (
85+
<FormattingToolbar
86+
blockTypeSelectItems={[
87+
...blockTypeSelectItems(editor.dictionary),
88+
...getMathBlockTypeSelectItems(editor),
89+
]}
90+
/>
91+
)}
92+
/>
93+
{/* Replaces the default Slash Menu. */}
94+
<SuggestionMenuController
95+
triggerCharacter={"/"}
96+
getItems={async (query) => {
97+
// Gets the default slash menu items and adds the Math items at the
98+
// end of their group ("Advanced").
99+
const items = combineByGroup(
100+
getDefaultReactSlashMenuItems(editor),
101+
getMathSlashMenuItems(editor),
102+
);
103+
104+
// Returns filtered items based on the query.
105+
return filterSuggestionItems(items, query);
106+
}}
107+
/>
108+
</BlockNoteView>
109+
);
110+
}

0 commit comments

Comments
 (0)