Math & diagram exporters: block-package mappings, typed errors, ExportImage contract, email export - #2961
Conversation
…, ExportImage contract, email export
- Move the math/diagram exporter mappings out of the (GPL) xl-* exporter
packages into the (MPL) block packages as subpath exports:
@blocknote/{math,diagram}-block/{docx,odt,pdf,email}-exporter. The xl
exporters no longer ship math/diagram defaults.
- Expected failures are typed results: invalid LaTeX/Mermaid is caught at
the lowest adapter around the throwing library and returned as
{ error: string }, propagating through the type system. Placeholders
render the source's first line plus the typed message - never a caught
exception's message. Environment problems still throw, naming the
option to pass (renderDiagram / rasterize).
- ExportImage (bytes + mime + display dimensions) is the image contract
between renderers, rasterizers, deliveries and mappings; rasterization
scale is owned by the rasterizer implementation.
- Email exporter support for math & diagrams: data-URL images by
default, createCIDImageDelivery() for nodemailer-style inline
attachments. PDF inline math rasterizes at asset resolution. Markdown
exports math as $...$ / $$...$$.
- Fix WebKit rasterizing Mermaid SVGs letterboxed at half size (its
intrinsic sizing honors Mermaid's inline max-width style over the
explicit width/height attributes - strip the style).
- Tests: per-module matrices (valid/invalid/capability/empty/CID) in
node with stubs; colocated .browser.test files for the browser-only
implementations; a browser e2e exporting the complete shared test
document through the real exporters with visual baselines (email
captured in window-sized pages, each PDF page rendered via pdf.js -
element screenshots blank out below the tester iframe's fold, so tall
captures must be paged).
- Docs for the export formats and interoperability examples updated;
xl-pdf-exporter tests run without jsdom; stale pdf-image snapshot
experiments and their dependencies removed.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis change adds package-specific math and diagram exporters for DOCX, ODT, PDF, and email output. It adds exporter localization, image contracts, Markdown math serialization, browser rendering tests, screenshot utilities, updated examples, and revised exporter package boundaries. ChangesExporter foundations
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ckfile - Capture the email export as one full-resolution baseline instead of page slices, and PDF pages at their natural size: grow the tester iframe past the content and neutralize the harness's fit-to-window scale transform during the capture (screenshotFull). A harness DOM change fails the baseline dimension check loudly. - Ignore pdfjs-dist's optional @napi-rs/canvas dependency workspace-wide: it exists for Node-side rendering, which we never do - pdf.js only runs in the browser suite, where the browser is the canvas. - Testing skill: document the -u-after-filters requirement (before the filters it swallows them and the whole suite runs in update mode), the full-resolution capture pattern, and that end-to-end/ hosts browser integration tests beyond UI-interaction e2e.
- screenshotFull moves to tests/src/utils: grows the tester iframe past the content and neutralizes the harness's fit-to-window scale transform during the capture - the same mechanism upstream Vitest adopted in vitest-dev/vitest#9745 (milestone 5.0.0) to fix #9124 and #9363, so the util can be deleted once vite-plus ships it. - New screenshotFull.test.tsx guards the harness-internals dependency on synthetic striped content: a plain capture blanks below the ~712px iframe fold, a viewport-only capture downscales to ~0.14x - the 600x2000 baselines prove the full-resolution render. - static.test.tsx: document that scale: "css" is load-bearing - dropping it pushes the chromium equality diff past the pixel budget under the iframe's scale transform.
… i18n - ExporterOptions gains `dictionary` (a core locale or an editor dictionary); Exporter exposes the `exporter` string section with English defaults. Mappings already receive the exporter, so every render site reads from it. - Core Dictionary gains an `exporter` section (open_file, open_video_file, open_audio_file) - translated in all 24 locales, and unified on "Open video"/"Open audio" wording across exporters (docx/ odt snapshots regenerated). - math-block/diagram-block own their exporter strings: their locales gain an `exporter` sub-section (invalid_formula/invalid_diagram templates, function-valued like the core dictionary) with getMathExporterDictionary/getDiagramExporterDictionary reading them from the exporter's dictionary - the same shape hosts merge into editor dictionaries, bundled-English fallback. - All hardcoded literals replaced (10 sites across the four exporters, 8 math/diagram modules); dictionary tests prove the seams (German file links, custom diagram placeholder); export docs document the option.
…sufficient
- mathjax-full ships CommonJS and vite's interop can resolve the
default TexError import as a { default: class } namespace, making the
instanceof boundary throw on the first invalid formula. isTexError
resolves the constructor defensively; the e2e document now includes an
invalid formula (a structural error - MathJax's noundefined package
renders unknown commands as text, not errors), which exercises the
real error class through vite's bundling and would have caught this.
- shared's build task now declares its dist output - without it the
cache replayed nothing on hits, leaving consumers to type-check
against missing or stale declarations (the 'pnpm build' failure).
- The playground's build-mode diagram-block alias points at src/: the
prefix replace bypasses the exports map, so the package root broke
every subpath import in production builds.
- Testing skill: -u only rewrites baselines whose comparison fails;
changes within the 2% tolerance leave baselines silently stale -
delete the file to force a fresh capture.
Only ParseError messages (invalid user LaTeX) become typed errors safe to render to readers; any other throw is a bug and propagates. ParseError is read off the same katex object whose renderToString just ran, so unlike a separately imported class it can't diverge under bundler interop.
Declaring @blocknote/shared in an example's .bnexample.json dependencies now emits the @shared vite alias and tsconfig paths into its generated configs (the package is private, so it only resolves inside the monorepo - which is also why the previous package-name import never worked standalone). The tests browser config is down to the single @shared alias all consumers use. Also syncs the custom-code-block example's .bnexample.json with its hand-edited shiki version pins, so gen stops reverting them.
- The ODT math/diagram mappings typed their exporter parameter as ODTExporter, but mapping signatures are contravariant in it - a function requiring the subclass isn't assignable to the mapping type, which surfaced once the interoperability example spread these mappings in. They now take the base Exporter and cast internally (only the ODTExporter ever invokes them). - The playground type-checks ../examples but had no paths mapping for the @shared alias the suggestion-gallery example uses.
The merged SourceWithPreview UI renders the empty diagram preview with a CSS-driven data-placeholder instead of the old static placeholder DOM; the snapshot predated it (it fails identically on the parent branch).
An undestroyed EditorView leaves ProseMirror DOMObserver debounce timers behind; on slow CI they fire after the jsdom environment is torn down and fail the run with an unhandled "document is not defined". The same mount-without-destroy pattern exists in other jsdom test files, but only this one mutates the editor in its last test right before teardown - which is the window the flake needs.
@blocknote/ariakit
@blocknote/code-block
@blocknote/core
@blocknote/diagram-block
@blocknote/mantine
@blocknote/math-block
@blocknote/react
@blocknote/server-util
@blocknote/shadcn
@blocknote/xl-ai
@blocknote/xl-docx-exporter
@blocknote/xl-email-exporter
@blocknote/xl-multi-column
@blocknote/xl-odt-exporter
@blocknote/xl-pdf-exporter
commit: |
- Docs: the renderDiagram examples return the ExportImage shape (they showed the pre-refactor dataURL shape), and the placeholder wording matches the source-identifying placeholders. - Markdown: nested multi-line blocks (math and code fences) indent every line including the closing delimiter - an unindented line would end the parent list item. Covered by nested toggle-item snapshot tests (external HTML flattens other list items' non-list children, so toggles are where nesting actually reaches the serializer). - DOCX diagrams clamp their display width to the page body (aspect preserved), like the email mapping; with a scaled-extents test. - ODT: ODTExporter deduplicates identical automatic styles at the source (both registerStyle and styled-text BN_T styles) - documents with many alike blocks or styled runs no longer accumulate duplicate styles. Unit-tested; snapshots shrink accordingly. - RTL locales (ar/fa/he) wrap the interpolated LTR source in bidi isolates in both block packages. - Playground build-mode alias map gains @shared; canvas 2D context gets an explicit guard in the image test util; testing-skill fence gets a language.
The snippets used DOCXExporter & co. without showing their imports (review feedback).
| composite: true, | ||
| // The repo-wide alias for the shared test-utils package, for examples | ||
| // that depend on it (private, so it only resolves inside the monorepo). | ||
| ...(project.config.dependencies?.["@blocknote/shared"] |
There was a problem hiding this comment.
Why not just include this in the package.json template by default?
| // metadata, so stub images must be actual PNGs. | ||
| export const pngBytes = Uint8Array.from( | ||
| atob( | ||
| "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", |
There was a problem hiding this comment.
This seems a bit esoteric
| } from "@blocknote/core"; | ||
| import { plainContentToString } from "@blocknote/core"; | ||
| import { AlignmentType, ImportedXmlComponent, Paragraph, TextRun } from "docx"; | ||
| import { mml2omml } from "mathml2omml"; |
There was a problem hiding this comment.
Is this dep always installed when the math-block package is installed? Probably not a big deal but just checking since I expect not everyone who uses the math block will need docx export.
There was a problem hiding this comment.
sub-export so should be fine
| import type { ExportImage } from "@blocknote/core"; | ||
| import { liteAdaptor } from "mathjax-full/js/adaptors/liteAdaptor.js"; | ||
| import { RegisterHTMLHandler } from "mathjax-full/js/handlers/html.js"; | ||
| import { TeX } from "mathjax-full/js/input/tex.js"; |
There was a problem hiding this comment.
If we have to use mathjax for this, maybe makes sense to remove the katex dep and use mathjax for HTML too? Both packages do sort of the same thing so it feels like we should just stick to one
There was a problem hiding this comment.
Looked into it, it is worth only doing katex on the live editor, and mathjax is only ever used for the export path (if pulled in since it is a sub-export). This tries to pull only the minimal possible. But the import/export that katex does in the browser would be pretty heavy, so I'll stick with this
…iagram-exporters # Conflicts: # packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts # packages/xl-docx-exporter/src/docx/defaultSchema/inlinecontent.ts # packages/xl-docx-exporter/src/docx/docxExporter.test.ts # packages/xl-docx-exporter/src/math-block/index.ts # packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx # packages/xl-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx # packages/xl-odt-exporter/src/math-block/index.tsx # packages/xl-odt-exporter/src/odt/defaultSchema/blocks.tsx # packages/xl-odt-exporter/src/odt/defaultSchema/inlineContent.tsx # packages/xl-odt-exporter/src/odt/odtExporter.test.ts # packages/xl-pdf-exporter/src/math-block/index.tsx # packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx # packages/xl-pdf-exporter/src/pdf/defaultSchema/inlinecontent.tsx # packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx
Stacked on #2857 (
code-block-previews). Rounds off the math & diagram exporter work: architecture, error handling, email support, and browser-verified visual coverage.What changed
Mappings live in the block packages. The math/diagram exporter mappings moved out of the (GPL)
xl-*exporter packages into the (MPL) block packages as subpath exports —@blocknote/math-block/{docx,odt,pdf,email}-exporterand the same for@blocknote/diagram-block. Keeps MPL block code out of the GPL exporters and puts each mapping next to the block it maps. Thexl-*exporters no longer ship math/diagram defaults; consumers spread the mappings in (see the updated interoperability examples/docs).ExportImageis the image contract. Renderers, rasterizers, deliveries and mappings exchange{ data: Uint8Array, mimeType, width, height }(display dimensions) instead of SVG strings / data URLs.exportImageToDataURLlives in core; rasterization scale is owned by the rasterizer implementation, not sprinkled through call sites.Pluggable seams with browser defaults.
rasterize: RasterizeSVG(math),renderDiagram: RenderDiagram(Mermaid), andimageDelivery: ReactEmailImageDelivery(email) plug in via thecreate*Mappingfactories. In the browser the defaults just work; headless exports throw a capability error that names the exact option to pass (e.g. mermaid-cli / Kroki for diagrams).Exporters never hardcode language strings.
ExporterOptionsgainsdictionary(pass a core locale or your editor's dictionary); core'sDictionarygains anexportersection translated in all 24 locales, and math/diagram own their exporter strings in their own locales (invalid_formula/invalid_diagramtemplates), read viagetMathExporterDictionary(exporter)— the same merge-a-section shape as their editor dictionaries, with bundled-English fallback. Also fixes aTexErrorCJS/ESM interop crash on invalid formulas under vite bundling, now covered by an invalid formula in the browser e2e document.Email export embeds math & diagrams as data-URL images by default, or as
cid:inline attachments viacreateCIDImageDelivery()(nodemailer-shaped) for clients that don't render data URLs. PDF inline math works now (rasterized during react-pdf asset resolution). Markdown exports math as$…$/$$…$$.Takeaways for the team (now in AGENTS.md / the testing skill)
{ error: string } | { …data }). The compiler then forces every caller to handle it. Corollary: never render a caught exception's message into a document — a catch-all can capture anything and leak internals. Only messages carried by typed results are known-safe to show; placeholders render the source's first line plus that typed message. Environment problems (no browser, nothing plugged in) still throw loudly.any/casts hiding cases, exhaustive switches. When an image format reaches DOCX embedding that the renderer contract doesn't allow, we throw instead of silently mislabeling bytes.documentexists but rendering doesn't — so browser-capability checks pass while the capability is broken. Node with pluggable stubs for logic; the Docker browser suite for real rendering. Browser-only implementations get colocatedpackages/*/src/**/*.browser.test.tsfiles, which run in the browser suite.tests/src/end-to-end/exporters/exporterImages.test.tsx) exports the full shared test document through the real exporters and screenshots the results: the email as one full-resolution capture, and each page of an actually-produced PDF rendered with pdf.js (a real browser needs no native canvas — which is what blocked the old Node attempt; pdf.js itself is a single pure-JS devDep, and its optional@napi-rs/canvasis excluded workspace-wide).page.viewport()alone makes the harness downscale the iframe to fit the window. This is known and fixed upstream (page.screenshot is extremely low resolution with a large viewport vitest-dev/vitest#9124, #9363, fixed by fix(browser)!: iframe scale vitest-dev/vitest#9745 in the Vitest 5.0.0 milestone); until vite-plus ships that, thescreenshotFullutil (tests/src/utils) backports the same mechanism — grow the iframe, neutralize its scale transform during the capture — andscreenshotFull.test.tsxguards it on synthetic striped content. Always eyeball regenerated baselines.max-widthstyle overrode our explicit width/height and letterboxed diagrams at half size. The renderer now strips the style, and a browser test asserts ink coverage so this class of bug can't pass as "non-blank image".Test status
code-block-previewsand are untouched by this branch —dragdrop"Formatting toolbar should not appear when dragging image block" (chromium+webkit) andkeyboardhandlers"Delete before shallower block" snapshot (chromium+webkit). Worth a look on the parent PR.Follow-ups
createReactMathInlineContentSpec.test.tsx,createReactDiagramBlockSpec.test.tsx) to the browser suite.deliveris sync because react-email rendering is sync).latexToSvgexport to@react-pdf/renderer's math support so PDF inline math needs less glue.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes