Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7937669
poc
YousefED Jun 22, 2026
56a7916
improve typst exporter
YousefED Jun 30, 2026
776075d
update tests
YousefED Jun 30, 2026
f6dc68a
Merge origin/main into playground/typst-pdf-poc
YousefED Aug 20, 2026
cfb9b6b
feat(typst): math & diagram mappings, SVG diagrams, @cantoo/pdf-lib
YousefED Aug 20, 2026
12b63f4
fix(typst): review-driven hardening of the typst/pdf-ua pipeline
YousefED Aug 20, 2026
87e9e74
fix(typst): review pass-3 fixes, fully offline fonts, pinned poppler …
YousefED Aug 20, 2026
2299aba
refactor(typst): split into xl-typst-exporter + Typst-based PDFExport…
YousefED Aug 20, 2026
90e1426
docs(typst): export docs + review-driven packaging and API hardening
YousefED Aug 20, 2026
6262696
fix(docs): demo page still imported the react-pdf exporter from the p…
YousefED Aug 20, 2026
7cb50ae
fix(ci): e2e failure artifacts were never uploaded
YousefED Aug 20, 2026
d23319f
fix: address CodeRabbit review findings
YousefED Aug 20, 2026
4d69138
ci: add update-e2e-screenshots dispatch workflow
YousefED Aug 20, 2026
6b2cd52
ci: allow triggering the screenshot workflow from the feature branch
YousefED Aug 20, 2026
a0c356b
test: regenerate Linux e2e screenshot baselines
YousefED Aug 20, 2026
4697df2
ci: drop the temporary branch trigger from the screenshot workflow
YousefED Aug 20, 2026
21db062
ci: pass the screenshot filter via env, not template expansion
YousefED Aug 20, 2026
b3dcb74
feat(pdf): bundled default fonts, CDN-free by default; ODT cell merge…
YousefED Aug 20, 2026
f27153c
chore(e2e): docker-only policy for the browser suite
YousefED Aug 20, 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
4 changes: 2 additions & 2 deletions .claude/skills/testing-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ End-to-end tests run in vitest browser mode (chromium, firefox and webkit) insid
bash tests/docker-run.sh -e CI=1 -- --run [filters]
```

**Never run the browser suite natively** (`vp test -c vite.config.browser.ts` outside the container) — not even "just one file". Two failure modes, both silent: the screenshot matcher _seeds_ a new `-darwin`/`-win32` baseline for every screenshot test that has none (passing without comparing anything, and littering the tree with hundreds of unvetted PNGs — these are gitignored as a backstop, and only `-linux` baselines are tracked), and several suites genuinely behave differently outside Linux (e.g. caret placement is font-metric-dependent, so platform-shared JSON snapshots mismatch). If Docker isn't running, notify the user to launch it instead of falling back to a native run.

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).

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.

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.
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
- name: Lint packages
run: vp lint

- name: Install PDF tooling (veraPDF conformance gate + poppler visual snapshots)
run: bash tests/scripts/install-pdf-tooling.sh

- name: Run unit tests
run: vp run test

Expand Down Expand Up @@ -110,6 +113,9 @@ jobs:
with:
name: e2e-attachments-${{ matrix.browser }}-${{ matrix.shardIndex }}
path: tests/.vitest-attachments/
# The attachments dir is a dot-directory; without this the action
# silently uploads nothing ("No files were found").
include-hidden-files: true
retention-days: 7

merge-reports:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/fresh-install-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ jobs:
name: Build packages
run: vp run -r build

- name: Install PDF tooling (veraPDF conformance gate + poppler visual snapshots)
run: bash tests/scripts/install-pdf-tooling.sh

- id: run_unit_tests
name: Run unit tests
run: vp run test
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/update-e2e-screenshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Regenerates the Linux screenshot baselines for the browser (e2e) suite in
# the same Playwright container the e2e jobs use, and uploads them as an
# artifact to download and commit. Linux baselines can otherwise only be
# produced locally through docker; this is the push-button alternative.
name: Update e2e screenshots
Comment on lines +1 to +5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get why we'd need this, you can re-gen with docker, why is that different?


on:
workflow_dispatch:
inputs:
filter:
description: "Test file filter (empty = all browser tests)"
required: false
default: "exporterImages"

permissions:
contents: read

jobs:
update-screenshots:
name: "Update screenshots"
runs-on: ubuntu-latest
timeout-minutes: 30
container:
image: mcr.microsoft.com/playwright:v1.60.0-noble
env:
# Routed through env so the input never expands into the shell script
# itself (template-level expansion is a code-injection vector - zizmor
# template-injection). Deliberately unquoted at use sites: an empty
# filter must expand to no argument at all.
FILTER: ${{ inputs.filter }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 100
persist-credentials: false

- uses: voidzero-dev/setup-vp@313600b80b104eadebb9111787d37a2e83e014ca # v1.17.0
with:
node-version-file: ".node-version"
cache: true

- name: Install dependencies
run: vp install

# `-u` must come after the filter, and each browser runs separately so a
# failure in one doesn't stop the others' baselines from updating.
- name: Update screenshots (chromium)
run: HOME=/root vp test -c vite.config.browser.ts --run --browser chromium $FILTER -u
working-directory: tests
continue-on-error: true
- name: Update screenshots (firefox)
run: HOME=/root vp test -c vite.config.browser.ts --run --browser firefox $FILTER -u
working-directory: tests
continue-on-error: true
- name: Update screenshots (webkit)
run: HOME=/root vp test -c vite.config.browser.ts --run --browser webkit $FILTER -u
working-directory: tests
continue-on-error: true

- name: Show changed baselines
run: git config --global --add safe.directory "$GITHUB_WORKSPACE" && git status --porcelain tests/src

- name: Upload updated screenshots
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: updated-screenshots
path: tests/src/**/__screenshots__/
retention-days: 7
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ All commands below are listed under `package.json` in the project root. See `vit
- `vp run preview`: Previews the build on port 3000.
- `vp run test`: Runs unit tests. Append with `-u` to update snapshots. Append with a file name to target only that file.
- To run individual unit tests, use `vp run test <file>`. For example, `vp run test packages/core/src/extensions/Versioning/inMemoryVersioning.test.ts`.
- `vp run e2e`: Runs end-to-end tests. Append with a file name to target only that file.
- `vp run e2e`: Runs end-to-end tests (always in Docker - NEVER run the browser suite natively; it seeds bogus per-platform snapshots). Append with a file name to target only that file.
- `vp run e2e:updateSnaps`: Runs end-to-end tests & updates snapshots. Append with a file name to target only that file.
- `vp help`: Prints a list of all available commands.

Expand Down
9 changes: 9 additions & 0 deletions docs/AGENTS.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not useful

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
1 change: 1 addition & 0 deletions docs/CLAUDE.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not useful

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
20 changes: 15 additions & 5 deletions docs/app/demo/_components/DemoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ import {
} from "@blocknote/xl-odt-exporter";
import {
PDFExporter,
pdfDefaultSchemaMappings,
typstDefaultSchemaMappings,
} from "@blocknote/xl-pdf-exporter";
import { pdf } from "@react-pdf/renderer";
// Bundle the Typst compiler wasm (resolved to a local asset in
// next.config.ts) instead of loading it from a CDN.
import compilerWasmUrl from "@myriaddreamin/typst-ts-web-compiler/wasm?url";
import { DefaultChatTransport } from "ai";
import { useTheme } from "next-themes";
import { useEffect, useMemo, useState } from "react";
Expand Down Expand Up @@ -336,9 +338,17 @@ function DemoEditorInner({
let filename = `blocknote-export.${format}`;

if (format === "pdf") {
const exporter = new PDFExporter(editor.schema, pdfDefaultSchemaMappings);
const pdfDocs = await exporter.toReactPDFDocument(editor.document);
blob = await pdf(pdfDocs).toBlob();
// Tagged PDF/UA-1 via the Typst-based exporter; the default fonts
// (matching the editor) load lazily from the package.
const exporter = new PDFExporter(
editor.schema,
typstDefaultSchemaMappings,
);
blob = await exporter.toBlob(
editor.document,
{ getModule: () => compilerWasmUrl },
{ title: "BlockNote demo document" },
);
} else if (format === "docx") {
const exporter = new DOCXExporter(
editor.schema,
Expand Down
11 changes: 11 additions & 0 deletions docs/components/typstCompilerWasmUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// The Typst compiler wasm for the docs build (see next.config.ts): the
// pdf-ua example bundles it via Vite's `?url`; under the docs site the
// import is aliased here. `new URL(..., import.meta.url)` is Turbopack's
// static-asset reference: the ~29MB wasm is emitted as a hashed static
// file and served by the site itself - no CDN involved, and the version
// can't skew from the installed package because it IS the installed file.
const url = new URL(
"../node_modules/@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm",
import.meta.url,
).href;
export default url;
4 changes: 4 additions & 0 deletions docs/components/typstRendererStub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Stub for @myriaddreamin/typst-ts-renderer (see next.config.ts): the
// pdf-ua demo only compiles Typst, never canvas-renders, so the optional
// renderer peer is not installed.
export {};
5 changes: 3 additions & 2 deletions docs/content/docs/features/blocks/diagrams.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,16 @@ The example below wires them all up.

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.

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:
The [PDF](/docs/features/export/pdf) (and [Typst](/docs/features/export/typst)), [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 others work the same way with their respective subpaths:

```typescript
import {
DOCXExporter,
docxDefaultSchemaMappings,
} from "@blocknote/xl-docx-exporter";
import { createDiagramBlockMapping } from "@blocknote/diagram-block/docx-exporter";
// ...or "@blocknote/diagram-block/pdf-exporter",
// ...or "@blocknote/diagram-block/typst-exporter" (PDF + Typst - embeds
// vector SVG with labels as real text),
// "@blocknote/diagram-block/odt-exporter",
// "@blocknote/diagram-block/email-exporter"

Expand Down
35 changes: 15 additions & 20 deletions docs/content/docs/features/blocks/math.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,34 +121,29 @@ import {

### PDF

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:
With the [PDF exporter](/docs/features/export/pdf) (or the standalone
[Typst exporter](/docs/features/export/typst) — they share mappings), math
blocks and inline math export as **native Typst equations**: real, selectable
text, no rasterization, so they also work server-side out of the box:

```typescript
import {
createInlineMathMapping,
inlineMathMapping,
mathBlockMapping,
} from "@blocknote/math-block/pdf-exporter";
} from "@blocknote/math-block/typst-exporter";

// Spread into the PDFExporter's mappings as for DOCX above - note that
// inline math is a factory here: `math: createInlineMathMapping()`.
// Spread into the PDFExporter's mappings as for DOCX above:
// `mathBlock: mathBlockMapping` and `math: inlineMathMapping`.
```

The inline math factory takes one option:

```typescript
createInlineMathMapping(options?: {
/**
* Rasterizes the formula SVG to an image. Defaults to the built-in
* canvas rasterizer, which only works in the browser - when exporting
* server-side, pass one backed by e.g. `@resvg/resvg-js` or `sharp`;
* without it, a server-side export throws. The `RasterizeSVG` type is
* exported from the same subpath.
*/
rasterize?: RasterizeSVG;
});
```
The LaTeX is converted to Typst's math notation. Most KaTeX commands are
covered; a valid formula using an uncovered command fails the export loudly
(rather than silently exporting something else), while invalid LaTeX renders
the error placeholder as in other formats.

Math blocks require the `@react-pdf/math` package (a peer dependency of the PDF mapping).
The deprecated [react-pdf exporter](/docs/features/export/pdf#deprecated-the-react-pdf-exporter)'s
mappings remain available from `@blocknote/math-block/pdf-exporter` during its
deprecation window.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see reason in preserving this for math which is new


### Email

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/features/export/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Export",
"pages": ["markdown", "html", "pdf", "docx", "email", "odt", "..."]
"pages": ["markdown", "html", "pdf", "docx", "email", "odt", "typst", "..."]
}
Loading
Loading