Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
358d755
Rework math & diagram exporters: block-package mappings, typed errors…
YousefED Aug 11, 2026
4db800e
Single full-resolution export screenshots; drop native canvas from lo…
YousefED Aug 11, 2026
07ba680
Extract screenshotFull util with upstream references; guard test
YousefED Aug 11, 2026
8461feb
Merge remote-tracking branch 'origin/code-block-previews' into math-d…
YousefED Aug 11, 2026
5d2f68e
upgrade shiki
YousefED Aug 11, 2026
6023ddf
Exporters never hardcode language strings: dictionary-backed exporter…
YousefED Aug 11, 2026
4019784
Fix TexError interop crash on invalid formulas; make pnpm build self-…
YousefED Aug 11, 2026
8fcf5fd
Tighten the KaTeX error boundary to ParseError
YousefED Aug 11, 2026
732d6e6
Suggestion-gallery example uses the repo-wide @shared alias
YousefED Aug 11, 2026
9ba02c9
Fix example type-checking: ODT mapping variance, @shared paths
YousefED Aug 11, 2026
4935914
merge and fix build
YousefED Aug 11, 2026
112f1ee
Refactor image source extraction in email export test to ensure prope…
YousefED Aug 11, 2026
4a2c068
Merge remote-tracking branch 'origin/code-block-previews' into math-d…
YousefED Aug 11, 2026
d7eb205
Unify shiki on v4 across the workspace
YousefED Aug 11, 2026
0f0e7a9
Update diagram export snapshot to the merged empty-state markup
YousefED Aug 11, 2026
0757fd7
Stretch Vercel build retry backoff to 15s/60s/180s
YousefED Aug 11, 2026
e4bb0e9
Destroy mounted editors in AttributionExtension tests
YousefED Aug 11, 2026
0a23145
Merge remote-tracking branch 'origin/code-block-previews' into math-d…
YousefED Aug 11, 2026
a3b7f96
Address CodeRabbit review
YousefED Aug 12, 2026
37a9be7
Docs: make the math/diagram mapping snippets self-contained
YousefED Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions .claude/skills/testing-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ In most cases, once a feature, bug fix, or other modification has been written,

`/packages/xl-*`: Contain tests for functionality included in a given `xl-*` package.

### Colocated Browser Unit Tests

`packages/*/src/**/*.browser.test.{ts,tsx}`: Unit tests for browser-only implementations (e.g. canvas or DOM-dependent code) live next to the code they test, with a `.browser.test` suffix. They run as part of the browser suite in Docker (the `tests` package's browser config includes them); the packages' own node-mode vitest configs exclude them. Use this when the unit under test genuinely needs a real browser — everything else should be a plain node unit test.

### End-to-End Tests

`tests/src/end-to-end`: Any test which interacts with the editor UI or simulates user interaction goes here. New subdirectories can be added if the functionality being tested is not covered by any of the existing ones. Important note about existing E2E tests - many are written poorly and should only loosely be used as reference. We want to avoid abstraction layers and `waitForTimeout` as much as possible.
`tests/src/end-to-end`: Tests that need a real browser and span multiple packages go here — chiefly tests which interact with the editor UI or simulate user interaction, but also browser integration tests that exercise complete flows without interaction (e.g. exporting a full document, static rendering). New subdirectories can be added if the functionality being tested is not covered by any of the existing ones. Important note about existing E2E tests - many are written poorly and should only loosely be used as reference. We want to avoid abstraction layers and `waitForTimeout` as much as possible.

## When & How to Add Tests

Expand All @@ -29,6 +33,8 @@ However, this may not be true when adding edge case handling or a new feature, w

We want to avoid adding end-to-end tests where it's possible to use unit tests instead.

**Don't use jsdom** (`@vitest-environment jsdom`) in new tests. It's a murky middle ground — `document` exists but rendering doesn't — which makes browser-capability checks pass while the capability itself is broken. Use the default node environment with pluggable seams for logic, and the browser suite (`tests/src/end-to-end`, vitest browser mode in Docker) for anything that needs real rendering.

## Running & Updating Tests

### Unit Tests
Expand All @@ -39,24 +45,18 @@ Updating tests can be done by adding the `-u` argument, i.e. `vp run test -u`. A

### End-to-End Tests

End-to-end tests run inside a docker container. While its possible to run them outside of it, we do not have existing snapshots to compare results with, and the results sometimes differ to when they're run within Docker, so it's not worth doing.

To run end-to-end tests, you must first build the project and run the preview. You can do this by running `vp start` from the root directory.
End-to-end tests run in vitest browser mode (chromium, firefox and webkit) inside a Docker container, so screenshot baselines are identical locally and on CI. Run them from the repository root:

You can then run the tests from the `/tests` directory using the following command:

```
docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.51.1-noble npx playwright test
```bash
bash tests/docker-run.sh -e CI=1 -- --run [filters]
```

A specific test file may be targeted by appending its name, i.e. `... npx playwright test fileName`. Individual tests in a file may be disabled using `skip`, i.e. `test.skip("Test name", ...)` (remember to revert this once all tests pass).
A specific test file may be targeted by appending (part of) its name as a filter. A single browser may be targeted with `--project "e2e (chromium)"`. Individual tests in a file may be disabled using `skip`, i.e. `test.skip("Test name", ...)` (remember to revert this once all tests pass).

Updating tests can be done by adding the `-u` argument, i.e. `... npx playwright test -u`. All of the other things you can do to scope which tests to target still apply.
Screenshot baselines can be regenerated with the `-u` argument, which must come **after** the filters (`--run <filters> -u`): written as `--run -u <filter>`, the filter is parsed as the flag's value and the **whole** suite runs in update mode, silently rewriting unrelated baselines. Note that `-u` only rewrites baselines whose comparison **fails** — a small intended change (e.g. a short text edit) that fits inside the suite's 2% pixel tolerance leaves the baseline stale while the test passes. To force a fresh capture, delete the baseline file first. Baselines are per-browser (`<name>-<browser>-linux.png`); after regenerating, always inspect the images before committing them.

Note that running this command may result in errors or other issues, listed below along with what to do when encountered:

- **Tests failing to navigating to preview**: project should be built and the preview started, after which the command should be run again.
- **Docker not running**: the user should be notified to launch Docker.
- **Incorrect Playwright image version**: update Playwright images and re-run the command.
If Docker isn't running, notify the user to launch it.

When testing a visual change, prefer writing screenshots to verify that the change is working as expected.

**Screenshots of tall content**: browser-suite tests run inside a tester iframe sized to the browser window (1280x720), and element screenshots only contain what the iframe actually paints — anything below its fold captures as blank white, silently. Growing the iframe with `page.viewport()` alone doesn't fix this at full resolution: the harness scales the iframe down to fit the window, shrinking the resulting baseline (`static.test.tsx` accepts that trade-off). For full-resolution captures use `screenshotFull` (`tests/src/utils/screenshotFull.ts`), which grows the iframe past the content and neutralizes the harness's scale transform during the capture — the same mechanism upstream Vitest adopted in vitest-dev/vitest#9745 (milestone 5.0.0; the util can be deleted once vite-plus ships it). Always eyeball newly generated baselines for truncation.
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

BlockNote is a block-based rich text editor for the web. It's designed as a batteries-included product that offers a solid user experience with minimal setup. However, it also offers extensibility via plugins and custom block types.

# Code Conventions

- **Leverage the type system so mistakes surface at compile time, not runtime.** Model states and outcomes explicitly: discriminated unions over boolean flags with optional fields, no `any` or casts that hide a case a caller should handle, exhaustive `switch`es over union members. If the compiler can enforce a contract, prefer that over documentation or runtime checks.
- **Expected failures are values, not exceptions.** When an operation can fail as part of normal use (canonical example: parsing user input, like LaTeX or Mermaid source), that failure is part of the function's contract — so it belongs in the return type. Catch it at the lowest level (the small adapter around the throwing third-party call) and convert it into a Result-style discriminated union (e.g. `{ error?: undefined; ...data } | { error: string }`). The failure then propagates through the type system, and the compiler forces every caller to decide how to handle it. Exceptions don't appear in TypeScript signatures, so a thrown expected error is invisible to callers — and a `try/catch` around a whole pipeline conflates expected failures with genuine bugs.
- **Exceptions are only for unexpected failures** — broken invariants, environment or infrastructure problems, programmer errors. Let them propagate and fail loudly; don't catch-and-continue. Corollary: never render a caught exception's message into user-facing output (documents, UI) — a catch-all can capture anything, and arbitrary messages can leak internals. Only messages carried by typed expected-error results are known-safe to display.
- **Prefer `function name() {}` declarations over `const name = () => {}`** for named functions (anonymous callbacks and returned closures can stay arrows).

# Common Commands

All commands below are listed under `package.json` in the project root. See `vite.config.ts` for relevant configuration settings.
Expand Down
4 changes: 3 additions & 1 deletion docs/content/docs/features/blocks/code-blocks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ const editor = useCreateBlockNote({
});
```

The math block renders LaTeX as MathML (via [Temml](https://temml.org/)) for the browser to display natively. Exporting to HTML produces a MathML `<math>` element, and pasting MathML back in is converted to LaTeX. Additionally, the source code is rendered to an `annotation` element in the HTML export for lossless round-trip conversion.
The math block renders LaTeX as MathML (via [KaTeX](https://katex.org/)) for the browser to display natively. Exporting to HTML produces a MathML `<math>` element, and pasting MathML back in is converted to LaTeX. Additionally, the source code is rendered to an `annotation` element in the HTML export for lossless round-trip conversion.

Math and diagram blocks can also be exported to [Markdown](/docs/features/export/markdown), [PDF](/docs/features/export/pdf), [DOCX](/docs/features/export/docx), [ODT](/docs/features/export/odt), and [email](/docs/features/export/email) — see each format's docs for the mappings to wire up.

<Example name="custom-schema/math-block" />

Expand Down
51 changes: 51 additions & 0 deletions docs/content/docs/features/export/docx.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,53 @@ new DOCXExporter(schema, {
});
```

### Math & diagram blocks

The [math and diagram blocks](/docs/features/blocks/code-blocks#code-blocks-with-previews) live in separate packages, and so do their DOCX mappings. Spread them into the default mappings to export math as native (editable) Word equations and diagrams as embedded images:

```typescript
import {
DOCXExporter,
docxDefaultSchemaMappings,
} from "@blocknote/xl-docx-exporter";
import {
inlineMathMapping,
mathBlockMapping,
} from "@blocknote/math-block/docx-exporter";
import { createDiagramBlockMapping } from "@blocknote/diagram-block/docx-exporter";

const exporter = new DOCXExporter(editor.schema, {
...docxDefaultSchemaMappings,
Comment thread
YousefED marked this conversation as resolved.
blockMapping: {
...docxDefaultSchemaMappings.blockMapping,
math: mathBlockMapping,
diagram: createDiagramBlockMapping(),
},
inlineContentMapping: {
...docxDefaultSchemaMappings.inlineContentMapping,
inlineMath: inlineMathMapping,
},
});
```

Invalid LaTeX or Mermaid sources render an error placeholder identifying the offending source, mirroring the editor.

Math exports fine server-side (the LaTeX is converted to OMML without rendering). Rendering diagrams to images, however, requires a browser — when exporting server-side, pass a `renderDiagram` function to `createDiagramBlockMapping` (e.g. backed by [`@mermaid-js/mermaid-cli`](https://github.com/mermaid-js/mermaid-cli) or a [Kroki](https://kroki.io) server); without one, the export throws:

```typescript
import { createDiagramBlockMapping } from "@blocknote/diagram-block/docx-exporter";
import type { RenderDiagram } from "@blocknote/diagram-block/docx-exporter";

const renderDiagram: RenderDiagram = async (source) => {
// Render the Mermaid source to an image with your renderer of choice.
return {
image: { data: pngBytes, mimeType: "image/png", width, height },
};
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

createDiagramBlockMapping({ renderDiagram });
```

### Exporter options

The `DOCXExporter` constructor takes an optional `options` parameter.
Expand All @@ -120,6 +167,10 @@ const defaultOptions = {
// a function to resolve external resources in order to avoid CORS issues
// by default, this calls a BlockNote hosted server-side proxy to resolve files
resolveFileUrl: corsProxyResolveFileUrl,
// the strings rendered into the exported document (file link texts, error
// placeholders); pass a locale from @blocknote/core/locales (or your
// editor's dictionary) to export in another language
dictionary: locales.en,
// the colors to use in the Docx for things like highlighting, background colors and font colors.
colors: COLORS_DEFAULT, // defaults from @blocknote/core
};
Expand Down
62 changes: 62 additions & 0 deletions docs/content/docs/features/export/email.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,64 @@ new ReactEmailExporter(schema, {
});
```

### Math & diagram blocks

The [math and diagram blocks](/docs/features/blocks/code-blocks#code-blocks-with-previews) live in separate packages, and so do their email mappings. Spread them into the default mappings to export math and diagrams as images, with the LaTeX/Mermaid source as the alt text:

```typescript
import {
ReactEmailExporter,
reactEmailDefaultSchemaMappings,
} from "@blocknote/xl-email-exporter";
import {
createInlineMathMapping,
createMathBlockMapping,
} from "@blocknote/math-block/email-exporter";
import { createDiagramBlockMapping } from "@blocknote/diagram-block/email-exporter";

const exporter = new ReactEmailExporter(editor.schema, {
...reactEmailDefaultSchemaMappings,
Comment thread
YousefED marked this conversation as resolved.
blockMapping: {
...reactEmailDefaultSchemaMappings.blockMapping,
math: createMathBlockMapping(),
diagram: createDiagramBlockMapping(),
},
inlineContentMapping: {
...reactEmailDefaultSchemaMappings.inlineContentMapping,
inlineMath: createInlineMathMapping(),
},
});
```

Invalid LaTeX or Mermaid sources render an error placeholder identifying the offending source, mirroring the editor.

**Image delivery.** By default the images are embedded as data URLs — self-contained, but some email clients (notably Gmail and Outlook for Windows) don't display data URL images and show the alt text instead. For those, deliver the images as inline `cid:` attachments (the most widely supported way to embed generated images) and pass the collected attachments to your mailer at send time:

```typescript
import { createCIDImageDelivery } from "@blocknote/xl-email-exporter";

const imageDelivery = createCIDImageDelivery();
const exporter = new ReactEmailExporter(editor.schema, {
...reactEmailDefaultSchemaMappings,
blockMapping: {
...reactEmailDefaultSchemaMappings.blockMapping,
math: createMathBlockMapping({ imageDelivery }),
diagram: createDiagramBlockMapping({ imageDelivery }),
},
inlineContentMapping: {
...reactEmailDefaultSchemaMappings.inlineContentMapping,
inlineMath: createInlineMathMapping({ imageDelivery }),
},
});

const html = await exporter.toReactEmailDocument(editor.document);

// e.g. with nodemailer (works the same with other mailers):
await transporter.sendMail({ html, attachments: imageDelivery.attachments });
```

**Server-side rendering.** Emails are usually rendered server-side at send time. Math handles this out of the box: block math is rasterized to PNG in the browser and embedded as SVG elsewhere (pass `rasterize` to `createMathBlockMapping`, e.g. backed by [`@resvg/resvg-js`](https://github.com/nlopes/resvg-js), to get PNGs server-side too — more email clients display them); inline math is always embedded as SVG. Rendering diagrams, however, requires a browser — when exporting server-side, pass a `renderDiagram` function to `createDiagramBlockMapping` (e.g. backed by [`@mermaid-js/mermaid-cli`](https://github.com/mermaid-js/mermaid-cli) or a [Kroki](https://kroki.io) server); without one, the export throws.

### Exporter options

The `ReactEmailExporter` constructor takes an optional `options` parameter.
Expand All @@ -126,6 +184,10 @@ const defaultOptions = {
// a function to resolve external resources in order to avoid CORS issues
// by default, this calls a BlockNote hosted server-side proxy to resolve files
resolveFileUrl: corsProxyResolveFileUrl,
// the strings rendered into the exported document (file link texts, error
// placeholders); pass a locale from @blocknote/core/locales (or your
// editor's dictionary) to export in another language
dictionary: locales.en,
// the colors to use in the email for things like highlighting, background colors and font colors.
colors: COLORS_DEFAULT, // defaults from @blocknote/core
};
Expand Down
2 changes: 2 additions & 0 deletions docs/content/docs/features/export/markdown.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const markdownFromBlocks = editor.blocksToMarkdownLossy(blocks);

The output is simplified as Markdown does not support all features of BlockNote (e.g.: children of blocks which aren't list items are un-nested and certain styles are removed).

[Math and diagram blocks](/docs/features/blocks/code-blocks#code-blocks-with-previews) export to their common Markdown notations: math blocks as `$$` blocks, inline math as `$...$` spans, and diagrams as ` ```mermaid ` fenced code blocks.

**Demo**

<Example name="interoperability/converting-blocks-to-md" />
37 changes: 37 additions & 0 deletions docs/content/docs/features/export/odt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,39 @@ new ODTExporter(schema, {
});
```

### Math & diagram blocks

The [math and diagram blocks](/docs/features/blocks/code-blocks#code-blocks-with-previews) live in separate packages, and so do their ODT mappings. Spread them into the default mappings to export math as native (editable) formula objects and diagrams as embedded images:

```typescript
import {
ODTExporter,
odtDefaultSchemaMappings,
} from "@blocknote/xl-odt-exporter";
import {
inlineMathMapping,
mathBlockMapping,
} from "@blocknote/math-block/odt-exporter";
import { createDiagramBlockMapping } from "@blocknote/diagram-block/odt-exporter";

const exporter = new ODTExporter(editor.schema, {
...odtDefaultSchemaMappings,
blockMapping: {
...odtDefaultSchemaMappings.blockMapping,
math: mathBlockMapping,
diagram: createDiagramBlockMapping(),
},
inlineContentMapping: {
...odtDefaultSchemaMappings.inlineContentMapping,
inlineMath: inlineMathMapping,
},
});
```

Invalid LaTeX or Mermaid sources render an error placeholder identifying the offending source, mirroring the editor.

Math exports fine server-side (the LaTeX is converted to MathML without rendering). Rendering diagrams to images, however, requires a browser — when exporting server-side, pass a `renderDiagram` function to `createDiagramBlockMapping` (e.g. backed by [`@mermaid-js/mermaid-cli`](https://github.com/mermaid-js/mermaid-cli) or a [Kroki](https://kroki.io) server); without one, the export throws.

### Exporter options

The `ODTExporter` constructor takes an optional `options` parameter.
Expand All @@ -95,6 +128,10 @@ const defaultOptions = {
// a function to resolve external resources in order to avoid CORS issues
// by default, this calls a BlockNote hosted server-side proxy to resolve files
resolveFileUrl: corsProxyResolveFileUrl,
// the strings rendered into the exported document (file link texts, error
// placeholders); pass a locale from @blocknote/core/locales (or your
// editor's dictionary) to export in another language
dictionary: locales.en,
// the colors to use in the ODT for things like highlighting, background colors and font colors.
colors: COLORS_DEFAULT, // defaults from @blocknote/core
};
Expand Down
Loading
Loading