diff --git a/docs/package.json b/docs/package.json index ff2e637b2e..a162ca6171 100644 --- a/docs/package.json +++ b/docs/package.json @@ -97,7 +97,10 @@ "y-partykit": "^0.0.25", "yjs": "^13.6.27", "zod": "^4.3.5", - "@blocknote/math-block": "workspace:*" + "@blocknote/math-block": "workspace:*", + "mermaid": "^11.0.0", + "@blocknote/diagram-block": "workspace:*", + "docx": "^9.6.1" }, "devDependencies": { "@blocknote/code-block": "workspace:*", diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/.bnexample.json b/examples/05-interoperability/05-converting-blocks-to-pdf/.bnexample.json index 6115e659a5..bc12cb5950 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/.bnexample.json +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/.bnexample.json @@ -2,10 +2,14 @@ "playground": true, "docs": true, "author": "yousefed", - "tags": ["Interoperability"], + "tags": [ + "Interoperability" + ], "dependencies": { - "@blocknote/xl-pdf-exporter": "latest", + "@blocknote/diagram-block": "latest", + "@blocknote/math-block": "latest", "@blocknote/xl-multi-column": "latest", + "@blocknote/xl-pdf-exporter": "latest", "@react-pdf/renderer": "^4.3.0" }, "pro": true diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/package.json b/examples/05-interoperability/05-converting-blocks-to-pdf/package.json index 73bb3589ab..563ee49477 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/package.json +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/package.json @@ -20,8 +20,10 @@ "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", - "@blocknote/xl-pdf-exporter": "latest", + "@blocknote/diagram-block": "latest", + "@blocknote/math-block": "latest", "@blocknote/xl-multi-column": "latest", + "@blocknote/xl-pdf-exporter": "latest", "@react-pdf/renderer": "^4.3.0" }, "devDependencies": { diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx b/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx index 4aaad9c805..c2ded234c7 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/src/App.tsx @@ -8,6 +8,11 @@ import "@blocknote/core/fonts/inter.css"; import * as locales from "@blocknote/core/locales"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; +import { + createReactInlineMathSpec, + createReactMathBlockSpec, +} from "@blocknote/math-block"; +import { createReactDiagramBlockSpec } from "@blocknote/diagram-block"; import { SuggestionMenuController, getDefaultReactSlashMenuItems, @@ -38,7 +43,16 @@ export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ // Adds support for page breaks & multi-column blocks. - schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())), + // Adds support for math & diagram blocks. + schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())).extend({ + blockSpecs: { + math: createReactMathBlockSpec(), + diagram: createReactDiagramBlockSpec(), + }, + inlineContentSpecs: { + inlineMath: createReactInlineMathSpec(), + }, + }), dropCursor: multiColumnDropCursor, dictionary: { ...locales.en, @@ -330,6 +344,31 @@ export default function App() { console.log("Hello World", message); };`, }, + { + type: "math", + content: "a^2 = \\sqrt{b^2 + c^2}", + }, + { + type: "diagram", + content: `graph TD + A[Start] --> B{Works?} + B -->|Yes| C[Ship it] + B -->|No| A`, + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Inline math: ", + styles: {}, + }, + { + type: "inlineMath", + content: "e^{i\\pi} + 1 = 0", + }, + ], + }, { type: "columnList", children: [ diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/.bnexample.json b/examples/05-interoperability/06-converting-blocks-to-docx/.bnexample.json index 17ec620bc9..fedca4fad3 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/.bnexample.json +++ b/examples/05-interoperability/06-converting-blocks-to-docx/.bnexample.json @@ -2,8 +2,12 @@ "playground": true, "docs": true, "author": "yousefed", - "tags": [""], + "tags": [ + "" + ], "dependencies": { + "@blocknote/diagram-block": "latest", + "@blocknote/math-block": "latest", "@blocknote/xl-docx-exporter": "latest", "@blocknote/xl-multi-column": "latest" }, diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/package.json b/examples/05-interoperability/06-converting-blocks-to-docx/package.json index 4d8be18e97..158887af62 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/package.json +++ b/examples/05-interoperability/06-converting-blocks-to-docx/package.json @@ -20,6 +20,8 @@ "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", + "@blocknote/diagram-block": "latest", + "@blocknote/math-block": "latest", "@blocknote/xl-docx-exporter": "latest", "@blocknote/xl-multi-column": "latest" }, diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx b/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx index 4b873d9a40..1558bd6eb8 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx +++ b/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx @@ -8,6 +8,11 @@ import "@blocknote/core/fonts/inter.css"; import * as locales from "@blocknote/core/locales"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; +import { + createReactInlineMathSpec, + createReactMathBlockSpec, +} from "@blocknote/math-block"; +import { createReactDiagramBlockSpec } from "@blocknote/diagram-block"; import { SuggestionMenuController, getDefaultReactSlashMenuItems, @@ -32,7 +37,16 @@ export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ // Adds support for page breaks & multi-column blocks. - schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())), + // Adds support for math & diagram blocks. + schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())).extend({ + blockSpecs: { + math: createReactMathBlockSpec(), + diagram: createReactDiagramBlockSpec(), + }, + inlineContentSpecs: { + inlineMath: createReactInlineMathSpec(), + }, + }), dropCursor: multiColumnDropCursor, dictionary: { ...locales.en, @@ -324,6 +338,31 @@ export default function App() { console.log("Hello World", message); };`, }, + { + type: "math", + content: "a^2 = \\sqrt{b^2 + c^2}", + }, + { + type: "diagram", + content: `graph TD + A[Start] --> B{Works?} + B -->|Yes| C[Ship it] + B -->|No| A`, + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Inline math: ", + styles: {}, + }, + { + type: "inlineMath", + content: "e^{i\\pi} + 1 = 0", + }, + ], + }, { type: "columnList", diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/.bnexample.json b/examples/05-interoperability/07-converting-blocks-to-odt/.bnexample.json index 7e3174aeea..a65c6d6993 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/.bnexample.json +++ b/examples/05-interoperability/07-converting-blocks-to-odt/.bnexample.json @@ -2,10 +2,14 @@ "playground": true, "docs": true, "author": "areknawo", - "tags": [""], + "tags": [ + "" + ], "dependencies": { - "@blocknote/xl-odt-exporter": "latest", - "@blocknote/xl-multi-column": "latest" + "@blocknote/diagram-block": "latest", + "@blocknote/math-block": "latest", + "@blocknote/xl-multi-column": "latest", + "@blocknote/xl-odt-exporter": "latest" }, "pro": true } diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/package.json b/examples/05-interoperability/07-converting-blocks-to-odt/package.json index 3877e20d75..e7ac42e55c 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/package.json +++ b/examples/05-interoperability/07-converting-blocks-to-odt/package.json @@ -20,8 +20,10 @@ "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", - "@blocknote/xl-odt-exporter": "latest", - "@blocknote/xl-multi-column": "latest" + "@blocknote/diagram-block": "latest", + "@blocknote/math-block": "latest", + "@blocknote/xl-multi-column": "latest", + "@blocknote/xl-odt-exporter": "latest" }, "devDependencies": { "@types/react": "^19.2.3", diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx b/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx index 7b884ac658..77dce213ee 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx +++ b/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx @@ -8,6 +8,11 @@ import * as locales from "@blocknote/core/locales"; import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; +import { + createReactInlineMathSpec, + createReactMathBlockSpec, +} from "@blocknote/math-block"; +import { createReactDiagramBlockSpec } from "@blocknote/diagram-block"; import { SuggestionMenuController, getDefaultReactSlashMenuItems, @@ -32,7 +37,16 @@ export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ // Adds support for page breaks & multi-column blocks. - schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())), + // Adds support for math & diagram blocks. + schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())).extend({ + blockSpecs: { + math: createReactMathBlockSpec(), + diagram: createReactDiagramBlockSpec(), + }, + inlineContentSpecs: { + inlineMath: createReactInlineMathSpec(), + }, + }), dropCursor: multiColumnDropCursor, dictionary: { ...locales.en, @@ -324,6 +338,31 @@ export default function App() { console.log("Hello World", message); };`, }, + { + type: "math", + content: "a^2 = \\sqrt{b^2 + c^2}", + }, + { + type: "diagram", + content: `graph TD + A[Start] --> B{Works?} + B -->|Yes| C[Ship it] + B -->|No| A`, + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Inline math: ", + styles: {}, + }, + { + type: "inlineMath", + content: "e^{i\\pi} + 1 = 0", + }, + ], + }, { type: "columnList", children: [ diff --git a/examples/06-custom-schema/10-diagram-block/.bnexample.json b/examples/06-custom-schema/10-diagram-block/.bnexample.json new file mode 100644 index 0000000000..915c0c3db7 --- /dev/null +++ b/examples/06-custom-schema/10-diagram-block/.bnexample.json @@ -0,0 +1,17 @@ +{ + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Intermediate", + "Blocks", + "Custom Schemas", + "Suggestion Menus", + "Slash Menu" + ], + "dependencies": { + "@blocknote/code-block": "latest", + "@blocknote/diagram-block": "latest", + "react-icons": "^5.5.0" + } +} diff --git a/examples/06-custom-schema/10-diagram-block/README.md b/examples/06-custom-schema/10-diagram-block/README.md new file mode 100644 index 0000000000..5e8ad1523b --- /dev/null +++ b/examples/06-custom-schema/10-diagram-block/README.md @@ -0,0 +1,10 @@ +# Diagram Block + +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. + +**Try it out:** Click a diagram to edit its Mermaid source! + +**Relevant Docs:** + +- [Custom Blocks](/docs/features/custom-schemas/custom-blocks) +- [Editor Setup](/docs/getting-started/editor-setup) diff --git a/examples/06-custom-schema/10-diagram-block/index.html b/examples/06-custom-schema/10-diagram-block/index.html new file mode 100644 index 0000000000..2a2ca2d29b --- /dev/null +++ b/examples/06-custom-schema/10-diagram-block/index.html @@ -0,0 +1,14 @@ + +
+ + +` elements, so the diagram's
+ // rule must be tried first to claim the `language-mermaid` ones.
+ runsBefore: ["codeBlock"],
+ render: DiagramBlockPreviewWithPopup,
+ toExternalHTML: (props) =>
,
+ },
+ [
+ // Diagram blocks always render a preview. Diagram sources span multiple
+ // lines, so Enter inserts a line break instead of closing the popup.
+ SourceBlockWithPreviewExtension({
+ key: "diagram-block-preview",
+ blockType: "diagram",
+ hasPreview: () => true,
+ enterBehaviour: "newline",
+ }),
+ ],
+);
diff --git a/packages/diagram-block/src/block/shared/parse/parseDiagramCodeElement.ts b/packages/diagram-block/src/block/shared/parse/parseDiagramCodeElement.ts
new file mode 100644
index 0000000000..ae30b454bd
--- /dev/null
+++ b/packages/diagram-block/src/block/shared/parse/parseDiagramCodeElement.ts
@@ -0,0 +1,30 @@
+import { parsePreCodeContent } from "@blocknote/core";
+
+// Parses `` elements - the fenced code
+// block representation used by Markdown & other editors - into diagram
+// blocks.
+export const parseDiagramCodeElement = (el: HTMLElement) => {
+ if (el.tagName !== "PRE") {
+ return undefined;
+ }
+
+ const code = el.firstElementChild;
+ if (el.childElementCount !== 1 || code?.tagName !== "CODE") {
+ return undefined;
+ }
+
+ const language =
+ code.getAttribute("data-language") ||
+ code.className
+ .split(" ")
+ .find((name) => name.startsWith("language-"))
+ ?.replace("language-", "");
+
+ return language === "mermaid" ? {} : undefined;
+};
+
+// Parses the code element's text as the diagram block's source, preserving
+// line breaks.
+export const parseDiagramCodeContent = (
+ options: Parameters[0],
+) => parsePreCodeContent(options, "diagram");
diff --git a/packages/diagram-block/src/index.ts b/packages/diagram-block/src/index.ts
new file mode 100644
index 0000000000..78fecb007e
--- /dev/null
+++ b/packages/diagram-block/src/index.ts
@@ -0,0 +1,7 @@
+export * from "./block/createDiagramBlockConfig.js";
+export * from "./block/react/createReactDiagramBlockSpec.js";
+export * from "./block/react/DiagramBlockPreviewWithPopup.js";
+export * from "./block/shared/parse/parseDiagramCodeElement.js";
+export * from "./shared/getDiagramPlainTextContent.js";
+export * from "./shared/renderDiagramToImage.js";
+
diff --git a/packages/diagram-block/src/shared/getDiagramPlainTextContent.ts b/packages/diagram-block/src/shared/getDiagramPlainTextContent.ts
new file mode 100644
index 0000000000..f450824add
--- /dev/null
+++ b/packages/diagram-block/src/shared/getDiagramPlainTextContent.ts
@@ -0,0 +1,14 @@
+// Converts rich text content in diagram blocks to the plain text Mermaid
+// source. Should be removed once we add plain text support for blocks.
+export const getDiagramPlainTextContent = (content: unknown): string => {
+ if (!Array.isArray(content)) {
+ return "";
+ }
+
+ return content
+ .map((node) =>
+ node && typeof node === "object" && "text" in node ? node.text : "",
+ )
+ .join("");
+};
+// TODO: remove
\ No newline at end of file
diff --git a/packages/diagram-block/src/shared/initializeMermaid.ts b/packages/diagram-block/src/shared/initializeMermaid.ts
new file mode 100644
index 0000000000..11537104f7
--- /dev/null
+++ b/packages/diagram-block/src/shared/initializeMermaid.ts
@@ -0,0 +1,18 @@
+import mermaid from "mermaid";
+
+// The diagrams are rendered manually whenever a block's source changes.
+let initialized = false;
+
+export const initializeMermaid = () => {
+ if (!initialized) {
+ initialized = true;
+ mermaid.initialize({
+ startOnLoad: false,
+ // On render errors, makes Mermaid throw right away - instead of
+ // rendering its own error graphic AND leaving its temporary render
+ // element behind in the document (it only cleans the element up with
+ // this option set). The block renders its own error UI.
+ suppressErrorRendering: true,
+ });
+ }
+};
diff --git a/packages/diagram-block/src/shared/renderDiagramToImage.ts b/packages/diagram-block/src/shared/renderDiagramToImage.ts
new file mode 100644
index 0000000000..55715fcce5
--- /dev/null
+++ b/packages/diagram-block/src/shared/renderDiagramToImage.ts
@@ -0,0 +1,62 @@
+import mermaid from "mermaid";
+
+import { initializeMermaid } from "./initializeMermaid.js";
+
+// Each render call needs its own element ID (Mermaid removes any existing
+// document element with the given ID when rendering).
+let exportElementId = 0;
+
+/**
+ * Renders the Mermaid source to a PNG image (as a data URL, with the
+ * diagram's natural dimensions in pixels), e.g. to embed diagrams as images
+ * when exporting documents to PDF/DOCX/ODT. Throws when the source is
+ * invalid. Browser-only - Mermaid can't render outside of it.
+ */
+export const renderDiagramToImage = async (
+ source: string,
+): Promise<{ dataURL: string; width: number; height: number }> => {
+ initializeMermaid();
+
+ await mermaid.parse(source);
+ const { svg } = await mermaid.render(
+ `diagram-export-${exportElementId++}`,
+ source,
+ );
+
+ // Mermaid sizes the SVG relatively (`width: 100%`), so it needs explicit
+ // pixel dimensions before rasterizing. These must be set on the SVG itself,
+ // not the `Image` element: the element's width/height only affect layout,
+ // while canvas rasterization uses the SVG's intrinsic size - without one,
+ // Safari falls back to a default size (and older Firefox refuses to draw
+ // the image to a canvas at all). The view box is also the only reliable
+ // source for the diagram's pixel dimensions, which the exporters need for
+ // layout.
+ const svgElement = new DOMParser().parseFromString(
+ svg,
+ "image/svg+xml",
+ ).documentElement;
+ const viewBox = svgElement
+ .getAttribute("viewBox")
+ ?.split(/\s+/)
+ .map(Number);
+ const width = Math.ceil(viewBox?.[2] || 800);
+ const height = Math.ceil(viewBox?.[3] || 600);
+ // Sized at 2x so the diagram stays sharp in the exported document. The 2x
+ // goes into the SVG's dimensions (rather than only the canvas), so browsers
+ // rasterize the vector at the full canvas resolution instead of upscaling a
+ // 1x raster.
+ svgElement.setAttribute("width", String(width * 2));
+ svgElement.setAttribute("height", String(height * 2));
+ const sizedSVG = new XMLSerializer().serializeToString(svgElement);
+
+ const image = new Image();
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(sizedSVG)}`;
+ await image.decode();
+
+ const canvas = document.createElement("canvas");
+ canvas.width = width * 2;
+ canvas.height = height * 2;
+ canvas.getContext("2d")!.drawImage(image, 0, 0, canvas.width, canvas.height);
+
+ return { dataURL: canvas.toDataURL("image/png"), width, height };
+};
diff --git a/packages/diagram-block/src/vite-env.d.ts b/packages/diagram-block/src/vite-env.d.ts
new file mode 100644
index 0000000000..bc2d8a36f3
--- /dev/null
+++ b/packages/diagram-block/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/packages/diagram-block/tsconfig.json b/packages/diagram-block/tsconfig.json
new file mode 100644
index 0000000000..c74ac34642
--- /dev/null
+++ b/packages/diagram-block/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "noEmit": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "outDir": "dist",
+ "declaration": true,
+ "declarationDir": "types",
+ "composite": true,
+ "skipLibCheck": true,
+ "emitDeclarationOnly": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/diagram-block/vite.config.ts b/packages/diagram-block/vite.config.ts
new file mode 100644
index 0000000000..a73943757f
--- /dev/null
+++ b/packages/diagram-block/vite.config.ts
@@ -0,0 +1,87 @@
+import * as path from "path";
+import { webpackStats } from "rollup-plugin-webpack-stats";
+import { defineConfig, type UserConfig } from "vite-plus";
+import pkg from "./package.json";
+
+// https://vitejs.dev/config/
+export default defineConfig(
+ (conf) =>
+ ({
+ run: {
+ tasks: {
+ build: {
+ command: "tsc && vp build",
+ input: [
+ { auto: true },
+ { pattern: "!**/*.tsbuildinfo", base: "workspace" },
+ ],
+ },
+ },
+ },
+ test: {
+ setupFiles: ["./vitestSetup.ts"],
+ },
+ plugins: [webpackStats() as any],
+ // used so that vitest resolves the core package from the sources instead of the built version
+ resolve: {
+ alias:
+ conf.command === "build"
+ ? ({} as Record)
+ : ({
+ // load live from sources with live reload working
+ "@blocknote/core": path.resolve(__dirname, "../core/src/"),
+ "@blocknote/react": path.resolve(__dirname, "../react/src/"),
+ } as Record),
+ },
+ build: {
+ sourcemap: true,
+ lib: {
+ entry: {
+ "blocknote-diagram-block": path.resolve(__dirname, "src/index.ts"),
+ },
+ name: "blocknote-diagram-block",
+ formats: ["es", "cjs"],
+ fileName: (format, entryName) =>
+ format === "es" ? `${entryName}.js` : `${entryName}.cjs`,
+ },
+ rollupOptions: {
+ // make sure to externalize deps that shouldn't be bundled
+ // into your library
+ external: (source) => {
+ // Bundle react-icons into the output (tree-shaken) so consumers
+ // don't need to install it as a peer/runtime dependency.
+ const bundledDeps = ["react-icons"];
+ if (
+ bundledDeps.some(
+ (dep) => source === dep || source.startsWith(dep + "/"),
+ )
+ ) {
+ return false;
+ }
+ if (
+ Object.keys({
+ ...pkg.dependencies,
+ ...((pkg as any).peerDependencies || {}),
+ ...pkg.devDependencies,
+ }).some((dep) => source === dep || source.startsWith(dep + "/"))
+ ) {
+ return true;
+ }
+ return (
+ source.startsWith("react/") ||
+ source.startsWith("react-dom/") ||
+ source.startsWith("prosemirror-") ||
+ source.startsWith("@tiptap/") ||
+ source.startsWith("@blocknote/") ||
+ source.startsWith("node:")
+ );
+ },
+ output: {
+ // Provide global variables to use in the UMD build
+ // for externalized deps
+ globals: {},
+ },
+ },
+ },
+ }) as UserConfig,
+);
diff --git a/packages/diagram-block/vitestSetup.ts b/packages/diagram-block/vitestSetup.ts
new file mode 100644
index 0000000000..dbcf3eb39c
--- /dev/null
+++ b/packages/diagram-block/vitestSetup.ts
@@ -0,0 +1,10 @@
+import { afterEach, beforeEach } from "vite-plus/test";
+
+beforeEach(() => {
+ globalThis.window = globalThis.window || ({} as any);
+ (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS = {};
+});
+
+afterEach(() => {
+ delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS;
+});
diff --git a/packages/math-block/package.json b/packages/math-block/package.json
index ee8f87a7d2..c1802fcc1d 100644
--- a/packages/math-block/package.json
+++ b/packages/math-block/package.json
@@ -63,6 +63,7 @@
},
"devDependencies": {
"@blocknote/react": "workspace:^",
+ "react-icons": "^5.5.0",
"@blocknote/xl-multi-column": "workspace:^",
"@types/katex": "^0.16.7",
"@types/react": "^19.2.3",
diff --git a/packages/math-block/src/block/react/render/MathBlockPreviewWithPopup.tsx b/packages/math-block/src/block/react/render/MathBlockPreviewWithPopup.tsx
index d593898e1d..da02031103 100644
--- a/packages/math-block/src/block/react/render/MathBlockPreviewWithPopup.tsx
+++ b/packages/math-block/src/block/react/render/MathBlockPreviewWithPopup.tsx
@@ -1,16 +1,18 @@
import {
+ PreviewPlaceholder,
ReactCustomBlockRenderProps,
SourceBlockWithPreview,
} from "@blocknote/react";
+import { TbMathFunction } from "react-icons/tb";
-import { MathBlockConfig } from "../../createMathBlockConfig.js";
import { getMathPlainTextContent } from "../../../shared/getMathPlainTextContent.js";
import { useLatexToMathMLString } from "../../../shared/react/render/useLatexToMathML.js";
+import { MathBlockConfig } from "../../createMathBlockConfig.js";
export const MathBlockPreviewWithPopup = (
props: ReactCustomBlockRenderProps,
) => {
- const source = getMathPlainTextContent(props.block.content);
+ const source = getMathPlainTextContent(props.block.content).trim();
const { mathMLString, error } = useLatexToMathMLString(source);
return (
@@ -19,8 +21,22 @@ export const MathBlockPreviewWithPopup = (
editor={props.editor}
contentRef={props.contentRef}
source={source}
- preview={}
+ // `undefined` while nothing has rendered successfully, so an error
+ // shows the error state instead of an empty preview.
+ preview={
+ mathMLString ? (
+
+ ) : undefined
+ }
error={error}
+ emptySourcePlaceholder={
+ }
+ text={
+ props.editor.dictionary.code_block.add_source_button_text
+ }
+ />
+ }
/>
);
};
diff --git a/packages/math-block/src/inlineContent/react/render/MathInlinePreviewWithPopup.tsx b/packages/math-block/src/inlineContent/react/render/MathInlinePreviewWithPopup.tsx
index 91a06507bd..50b779542d 100644
--- a/packages/math-block/src/inlineContent/react/render/MathInlinePreviewWithPopup.tsx
+++ b/packages/math-block/src/inlineContent/react/render/MathInlinePreviewWithPopup.tsx
@@ -1,12 +1,14 @@
import { StyleSchema } from "@blocknote/core";
import {
+ PreviewPlaceholder,
ReactCustomInlineContentRenderProps,
SourceInlineContentWithPreview,
} from "@blocknote/react";
+import { TbMathFunction } from "react-icons/tb";
-import { MathInlineContentConfig } from "../../mathInlineContentConfig.js";
import { getMathPlainTextContent } from "../../../shared/getMathPlainTextContent.js";
import { useLatexToMathMLString } from "../../../shared/react/render/useLatexToMathML.js";
+import { MathInlineContentConfig } from "../../mathInlineContentConfig.js";
export const MathInlinePreviewWithPopup = (
props: ReactCustomInlineContentRenderProps<
@@ -14,7 +16,7 @@ export const MathInlinePreviewWithPopup = (
StyleSchema
>,
) => {
- const source = getMathPlainTextContent(props.inlineContent.content);
+ const source = getMathPlainTextContent(props.inlineContent.content).trim();
const { mathMLString, error } = useLatexToMathMLString(source, true);
return (
@@ -24,8 +26,22 @@ export const MathInlinePreviewWithPopup = (
node={props.node}
getPos={props.getPos}
source={source}
- preview={}
+ // `undefined` while nothing has rendered successfully, so an error
+ // shows the error state instead of an empty preview.
+ preview={
+ mathMLString ? (
+
+ ) : undefined
+ }
error={error}
+ emptySourcePlaceholder={
+ }
+ text={
+ props.editor.dictionary.code_block.add_source_button_text
+ }
+ />
+ }
/>
);
};
diff --git a/packages/math-block/vite.config.ts b/packages/math-block/vite.config.ts
index 99bdfd9d91..833d404b19 100644
--- a/packages/math-block/vite.config.ts
+++ b/packages/math-block/vite.config.ts
@@ -48,6 +48,16 @@ export default defineConfig(
// make sure to externalize deps that shouldn't be bundled
// into your library
external: (source) => {
+ // Bundle react-icons into the output (tree-shaken) so consumers
+ // don't need to install it as a peer/runtime dependency.
+ const bundledDeps = ["react-icons"];
+ if (
+ bundledDeps.some(
+ (dep) => source === dep || source.startsWith(dep + "/"),
+ )
+ ) {
+ return false;
+ }
if (
Object.keys({
...pkg.dependencies,
diff --git a/packages/react/src/blocks/SourceWithPreview/AddSourceButton.tsx b/packages/react/src/blocks/SourceWithPreview/AddSourceButton.tsx
deleted file mode 100644
index 8a128657bc..0000000000
--- a/packages/react/src/blocks/SourceWithPreview/AddSourceButton.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-// Shown in place of the preview when the source content is empty.
-export const AddSourceButton = (props: { text: string }) => (
-
-
-
-
- {props.text}
-
-);
diff --git a/packages/react/src/blocks/SourceWithPreview/PreviewPlaceholder.tsx b/packages/react/src/blocks/SourceWithPreview/PreviewPlaceholder.tsx
new file mode 100644
index 0000000000..7a2c8b1625
--- /dev/null
+++ b/packages/react/src/blocks/SourceWithPreview/PreviewPlaceholder.tsx
@@ -0,0 +1,31 @@
+import { ReactNode } from "react";
+import { FaCode } from "react-icons/fa";
+import { MdErrorOutline } from "react-icons/md";
+
+/**
+ * Shown in place of the preview when there's nothing to render: the "add
+ * source" state when the source is empty, or - with `error` - the compact
+ * error state when the source failed to render (the full error message is
+ * shown in the source popup while editing).
+ */
+export const PreviewPlaceholder = (props: {
+ text: string;
+ icon?: ReactNode;
+ error?: boolean;
+}) => (
+
+ {/* The icon is decorative next to the text, so hide it from screen
+ readers - react-icons don't set this themselves (some even carry
+ `role="img"`, announcing a nameless image). */}
+
+ {props.text}
+
+);
diff --git a/packages/react/src/blocks/SourceWithPreview/PreviewWithSourcePopup.tsx b/packages/react/src/blocks/SourceWithPreview/PreviewWithSourcePopup.tsx
deleted file mode 100644
index 6bb9a94c08..0000000000
--- a/packages/react/src/blocks/SourceWithPreview/PreviewWithSourcePopup.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
-
-export type PreviewWithSourcePopupProps = {
- /**
- * Renders with `span` wrappers so the shell can be used for inline content.
- * Defaults to `false`, which renders with `div` wrappers for blocks.
- */
- inline?: boolean;
- /**
- * Whether the source popup is shown.
- */
- isOpen: boolean;
- /**
- * Whether the preview is highlighted as selected.
- */
- isSelected: boolean;
- /**
- * The rendered preview (e.g. a formula or diagram). Rendered inside a
- * non-editable container that opens the source popup on click.
- */
- preview: ReactNode;
- /**
- * Error from rendering the preview, shown below the source in the popup.
- * Accepts arbitrary elements, so actions (e.g. a button that fixes the
- * source) can be rendered alongside the error message.
- */
- error?: ReactNode;
- /**
- * Ref for the element holding the editable source content.
- */
- contentRef: (node: HTMLElement | null) => void;
- /**
- * Additional props for the preview container, e.g. a mouse handler that
- * opens the popup. `className` is merged with the container's own, and
- * `contentEditable` can't be overridden.
- */
- previewContainerProps?: HTMLAttributes;
- /**
- * Additional props for the "OK" button, e.g. a mouse handler that closes
- * the popup. `className` is merged with the button's own.
- */
- okButtonProps?: ButtonHTMLAttributes;
-};
-
-/**
- * Presentational shell for blocks & inline content that render a preview of
- * their source content (e.g. math or diagrams): shows the preview, with the
- * editable source in a popup. Purely visual - opening/closing the popup and
- * keyboard handling are driven by the caller, typically via
- * `useSourceBlockPreviewPopup`/`useSourceInlineContentPreviewPopup` (see
- * `SourceBlockWithPreview` and `SourceInlineContentWithPreview`).
- */
-export const PreviewWithSourcePopup = (props: PreviewWithSourcePopupProps) => {
- const Wrapper = props.inline ? "span" : "div";
- const PreviewContainer = props.inline ? "span" : "div";
-
- return (
-
-
- {props.preview}
-
-
-
-
-
-
-
-
-
-
-
- {props.error}
-
-
-
- );
-};
diff --git a/packages/react/src/blocks/SourceWithPreview/SourceWithPreview.tsx b/packages/react/src/blocks/SourceWithPreview/SourceWithPreview.tsx
new file mode 100644
index 0000000000..0c63d02ae3
--- /dev/null
+++ b/packages/react/src/blocks/SourceWithPreview/SourceWithPreview.tsx
@@ -0,0 +1,201 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import { MouseEvent, ReactNode, useRef } from "react";
+
+import { PreviewPlaceholder } from "./PreviewPlaceholder.js";
+import { SourcePreviewPopup } from "./SourcePreviewPopup.js";
+
+/**
+ * Props shared by {@link SourceBlockWithPreview} and
+ * {@link SourceInlineContentWithPreview}.
+ */
+export type SourceWithPreviewProps = {
+ /**
+ * Ref for the element holding the editable source content.
+ */
+ contentRef: (node: HTMLElement | null) => void;
+ /**
+ * The source as plain text. When empty, an "add source" button is shown in
+ * place of the preview.
+ */
+ source: string;
+ /**
+ * The rendered preview (e.g. a formula or diagram). When the current source
+ * has an error, pass the last successfully rendered preview so it stays up
+ * while the user edits, or `undefined` when there is none - the error state
+ * is shown in its place. The last-good preview only stays up within the
+ * editing session that introduced the error: once the popup closes on an
+ * erroneous source, the error state shows until the source renders
+ * successfully again.
+ */
+ preview?: ReactNode;
+ /**
+ * Error from rendering the preview, shown below the source in the popup.
+ * Accepts arbitrary elements, so actions (e.g. a button that fixes the
+ * source) can be rendered alongside the error message.
+ */
+ error?: ReactNode;
+ /**
+ * Shown in place of the preview when the source is empty. A string sets
+ * the text of the default "add source" button, while an element replaces
+ * the button entirely. Defaults to the editor dictionary's
+ * `code_block.add_source_button_text`.
+ */
+ emptySourcePlaceholder?: ReactNode;
+ /**
+ * Shown in place of the preview when the source has an error, unless
+ * there's a last-good `preview` and the error hasn't been committed yet
+ * (the popup hasn't closed since it appeared). A string sets the text of
+ * the default compact error state, while an element replaces it entirely.
+ * The full `error` is only shown in the popup while editing.
+ */
+ errorPreview?: ReactNode;
+};
+
+/**
+ * Renders a preview of source content (e.g. math or diagrams), with the
+ * editable source in a popup driven by the given popup controller: decides
+ * what shows in place of the preview (empty state, last-good preview, or
+ * error state) and opens/closes the popup on clicks.
+ * `SourceBlockWithPreview` and `SourceInlineContentWithPreview` are thin
+ * per-kind wrappers around this, obtaining the popup controller from their
+ * respective hooks.
+ */
+export const SourceWithPreview = (
+ props: SourceWithPreviewProps & {
+ editor: BlockNoteEditor;
+ popup: SourcePreviewPopup;
+ /**
+ * Renders with `span` wrappers for inline content.
+ */
+ inline?: boolean;
+ },
+) => {
+ const {
+ editor,
+ popup,
+ inline,
+ contentRef,
+ source,
+ preview,
+ error,
+ emptySourcePlaceholder,
+ errorPreview,
+ } = props;
+
+ // Whether the error has been "committed": shown while the popup was
+ // closed. The last-good preview only stays up within the editing session
+ // that introduced the error - once committed, the error state also shows
+ // when the popup reopens, until the source renders successfully again.
+ // Updated during render (rather than in an effect) so reopening doesn't
+ // flash the stale preview for a frame first.
+ const errorCommittedRef = useRef(false);
+ if (error == null) {
+ errorCommittedRef.current = false;
+ } else if (!popup.isOpen) {
+ errorCommittedRef.current = true;
+ }
+
+ // For both placeholder props, a string customizes the default element's
+ // text while any other element replaces it entirely.
+ const emptyState =
+ emptySourcePlaceholder == null ||
+ typeof emptySourcePlaceholder === "string" ? (
+
+ ) : (
+ emptySourcePlaceholder
+ );
+ const errorState =
+ errorPreview == null || typeof errorPreview === "string" ? (
+
+ ) : (
+ errorPreview
+ );
+
+ // What to show in place of the source: the empty state, the preview, or -
+ // when the source has an error that's committed (or no last-good preview
+ // to keep showing) - the error state.
+ const previewContent =
+ source.length === 0
+ ? emptyState
+ : error != null && (errorCommittedRef.current || preview == null)
+ ? errorState
+ : preview;
+
+ // The actions run on click (rather than mousedown), so keyboard &
+ // assistive technology activation also works. No mousedown focus guards:
+ // React's synthetic `onMouseDown` doesn't fire on node view elements, and
+ // testing with real input showed they aren't needed - `open`/`close`
+ // restore the editor's focus & selection themselves.
+
+ // Opens the popup when clicking the preview.
+ const handlePreviewClick = (event: MouseEvent) => {
+ if (!editor.isEditable) {
+ return;
+ }
+
+ event.stopPropagation();
+
+ popup.open();
+ };
+
+ // Closes the popup when clicking the "OK" button.
+ const handleOkButtonClick = (event: MouseEvent) => {
+ event.stopPropagation();
+
+ popup.close();
+ };
+
+ // `span` wrappers so inline content stays valid inside a paragraph.
+ const Wrapper = inline ? "span" : "div";
+ const PreviewContainer = inline ? "span" : "div";
+
+ return (
+
+
+ {previewContent}
+
+
+
+
+
+
+
+
+
+
+
+ {error}
+
+
+
+ );
+};
diff --git a/packages/react/src/blocks/SourceWithPreview/block/SourceBlockWithPreview.tsx b/packages/react/src/blocks/SourceWithPreview/block/SourceBlockWithPreview.tsx
index b7e9e02727..9956141d70 100644
--- a/packages/react/src/blocks/SourceWithPreview/block/SourceBlockWithPreview.tsx
+++ b/packages/react/src/blocks/SourceWithPreview/block/SourceBlockWithPreview.tsx
@@ -1,31 +1,18 @@
-import { MouseEvent, ReactNode } from "react";
-
import { ReactCustomBlockRenderProps } from "../../../schema/ReactBlockSpec.js";
-import { AddSourceButton } from "../AddSourceButton.js";
-import { PreviewWithSourcePopup } from "../PreviewWithSourcePopup.js";
+import {
+ SourceWithPreview,
+ SourceWithPreviewProps,
+} from "../SourceWithPreview.js";
import { useSourceBlockPreviewPopup } from "./useSourceBlockPreviewPopup.js";
-export type SourceBlockWithPreviewProps = Pick, "block" | "editor"> & {
- // Not picked from `ReactCustomBlockRenderProps` like the props above, as
- // it's conditional on the block's content type there, which TypeScript
- // can't resolve for a generic block config.
- contentRef: (node: HTMLElement | null) => void;
- /**
- * The block's source content as plain text. When empty, an "add source"
- * button is shown in place of the preview.
- */
- source: string;
- /**
- * The rendered preview (e.g. a formula or diagram).
- */
- preview: ReactNode;
- /**
- * Error from rendering the preview, shown below the source in the popup.
- * Accepts arbitrary elements, so actions (e.g. a button that fixes the
- * source) can be rendered alongside the error message.
- */
- error?: ReactNode;
-};
+export type SourceBlockWithPreviewProps = Pick<
+ ReactCustomBlockRenderProps,
+ "block" | "editor"
+> &
+ // `contentRef` comes from the shared props rather than being picked from
+ // `ReactCustomBlockRenderProps`, as it's conditional on the block's content
+ // type there, which TypeScript can't resolve for a generic block config.
+ SourceWithPreviewProps;
/**
* Renders a block as a preview of its source content, with the editable
@@ -36,54 +23,10 @@ export type SourceBlockWithPreviewProps = Pick,
* The caller only provides the preview itself, making this the base for
* custom blocks rendered from source code (math, diagrams, etc).
*/
-export const SourceBlockWithPreview = (
- props: SourceBlockWithPreviewProps,
-) => {
- const { block, editor, contentRef, source, preview, error } = props;
+export const SourceBlockWithPreview = (props: SourceBlockWithPreviewProps) => {
+ const { block, editor, ...shared } = props;
const popup = useSourceBlockPreviewPopup({ editor, block });
- // The actions run on click (rather than mousedown), so keyboard &
- // assistive technology activation also works. No mousedown focus guards:
- // React's synthetic `onMouseDown` doesn't fire on node view elements, and
- // testing with real input showed they aren't needed - `open`/`close`
- // restore the editor's focus & selection themselves.
-
- // Opens the popup when clicking the preview.
- const handlePreviewClick = (event: MouseEvent) => {
- if (!editor.isEditable) {
- return;
- }
-
- event.stopPropagation();
-
- popup.open();
- };
-
- // Closes the popup when clicking the "OK" button.
- const handleOkButtonClick = (event: MouseEvent) => {
- event.stopPropagation();
-
- popup.close();
- };
-
- return (
- 0 ? (
- preview
- ) : (
-
- )
- }
- error={error}
- contentRef={contentRef}
- previewContainerProps={{ onClick: handlePreviewClick }}
- okButtonProps={{ onClick: handleOkButtonClick }}
- />
- );
+ return ;
};
diff --git a/packages/react/src/blocks/SourceWithPreview/inlineContent/SourceInlineContentWithPreview.tsx b/packages/react/src/blocks/SourceWithPreview/inlineContent/SourceInlineContentWithPreview.tsx
index be723dd3e8..5b1eceaafe 100644
--- a/packages/react/src/blocks/SourceWithPreview/inlineContent/SourceInlineContentWithPreview.tsx
+++ b/packages/react/src/blocks/SourceWithPreview/inlineContent/SourceInlineContentWithPreview.tsx
@@ -1,30 +1,15 @@
-import { MouseEvent, ReactNode } from "react";
-
import { ReactCustomInlineContentRenderProps } from "../../../schema/ReactInlineContentSpec.js";
-import { AddSourceButton } from "../AddSourceButton.js";
-import { PreviewWithSourcePopup } from "../PreviewWithSourcePopup.js";
+import {
+ SourceWithPreview,
+ SourceWithPreviewProps,
+} from "../SourceWithPreview.js";
import { useSourceInlineContentPreviewPopup } from "./useSourceInlineContentPreviewPopup.js";
export type SourceInlineContentWithPreviewProps = Pick<
ReactCustomInlineContentRenderProps,
- "editor" | "contentRef" | "node" | "getPos"
-> & {
- /**
- * The inline content's source as plain text. When empty, an "add source"
- * button is shown in place of the preview.
- */
- source: string;
- /**
- * The rendered preview (e.g. a formula or diagram).
- */
- preview: ReactNode;
- /**
- * Error from rendering the preview, shown below the source in the popup.
- * Accepts arbitrary elements, so actions (e.g. a button that fixes the
- * source) can be rendered alongside the error message.
- */
- error?: ReactNode;
-};
+ "editor" | "node" | "getPos"
+> &
+ SourceWithPreviewProps;
/**
* Renders inline content as a preview of its source, with the editable source
@@ -38,56 +23,9 @@ export type SourceInlineContentWithPreviewProps = Pick<
export const SourceInlineContentWithPreview = (
props: SourceInlineContentWithPreviewProps,
) => {
- const { editor, contentRef, node, getPos, source, preview, error } = props;
+ const { editor, node, getPos, ...shared } = props;
const popup = useSourceInlineContentPreviewPopup({ editor, node, getPos });
- // The actions run on click (rather than mousedown), so keyboard &
- // assistive technology activation also works. No mousedown focus guards:
- // React's synthetic `onMouseDown` doesn't fire on node view elements, and
- // testing with real input showed they aren't needed - `open`/`close`
- // restore the editor's focus & selection themselves.
-
- // Opens the popup when clicking the preview.
- const handlePreviewClick = (event: MouseEvent) => {
- if (!editor.isEditable) {
- return;
- }
-
- event.stopPropagation();
-
- popup.open();
- };
-
- // Closes the popup when clicking the "OK" button.
- const handleOkButtonClick = (event: MouseEvent) => {
- if (!editor.isEditable) {
- return;
- }
-
- event.stopPropagation();
-
- popup.close();
- };
-
- return (
- 0 ? (
- preview
- ) : (
-
- )
- }
- error={error}
- contentRef={contentRef}
- previewContainerProps={{ onClick: handlePreviewClick }}
- okButtonProps={{ onClick: handleOkButtonClick }}
- />
- );
+ return ;
};
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index bb25cf3260..cd7137e7a7 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -16,8 +16,8 @@ export * from "./blocks/File/helpers/toExternalHTML/LinkWithCaption.js";
export * from "./blocks/File/useResolveUrl.js";
export * from "./blocks/Image/block.js";
export * from "./blocks/PageBreak/getPageBreakReactSlashMenuItems.js";
-export * from "./blocks/SourceWithPreview/AddSourceButton.js";
-export * from "./blocks/SourceWithPreview/PreviewWithSourcePopup.js";
+export * from "./blocks/SourceWithPreview/PreviewPlaceholder.js";
+export * from "./blocks/SourceWithPreview/SourceWithPreview.js";
export * from "./blocks/SourceWithPreview/SourcePreviewPopup.js";
export * from "./blocks/SourceWithPreview/block/SourceBlockWithPreview.js";
export * from "./blocks/SourceWithPreview/block/useSourceBlockPreviewPopup.js";
diff --git a/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts b/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts
index f926915448..4564d74493 100644
--- a/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts
+++ b/packages/xl-docx-exporter/src/docx/defaultSchema/blocks.ts
@@ -30,6 +30,7 @@ import { multiColumnSchema } from "@blocknote/xl-multi-column";
type BSchema = DefaultBlockSchema & {
pageBreak: ReturnType;
math: BlockConfig<"math", {}, "inline">;
+ diagram: BlockConfig<"diagram", {}, "inline">;
} & typeof multiColumnSchema.blockSchema;
function blockPropsToStyles(
@@ -188,6 +189,7 @@ export const docxBlockMappingForDefaultSchema: BlockMapping<
},
codeBlock: codeMapping,
math: codeMapping,
+ diagram: codeMapping,
pageBreak: () => {
return new Paragraph({
children: [new PageBreak()],
diff --git a/packages/xl-docx-exporter/src/docx/defaultSchema/inlinecontent.ts b/packages/xl-docx-exporter/src/docx/defaultSchema/inlinecontent.ts
index 5d8b0f442d..9ec65395b2 100644
--- a/packages/xl-docx-exporter/src/docx/defaultSchema/inlinecontent.ts
+++ b/packages/xl-docx-exporter/src/docx/defaultSchema/inlinecontent.ts
@@ -6,8 +6,16 @@ import {
import { ExternalHyperlink, ParagraphChild, TextRun } from "docx";
import type { DOCXExporter } from "../docxExporter.js";
+type ICSchema = DefaultInlineContentSchema & {
+ inlineMath: {
+ type: "inlineMath";
+ propSchema: Record;
+ content: "styled";
+ };
+};
+
export const docxInlineContentMappingForDefaultSchema: InlineContentMapping<
- DefaultInlineContentSchema,
+ ICSchema,
DefaultStyleSchema,
ParagraphChild,
TextRun
@@ -26,4 +34,12 @@ export const docxInlineContentMappingForDefaultSchema: InlineContentMapping<
text: (ic, t) => {
return t.transformStyledText(ic);
},
+ // Renders inline math as its monospaced LaTeX source.
+ // TODO
+ inlineMath: (ic) => {
+ return new TextRun({
+ text: ic.content.map((content) => content.text).join(""),
+ style: "VerbatimChar",
+ });
+ },
};
diff --git a/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx
index 21afc61c1b..b2828e7a83 100644
--- a/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx
+++ b/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx
@@ -120,6 +120,7 @@ export const defaultReactEmailTextStyles = {
type BSchema = DefaultBlockSchema & {
pageBreak: ReturnType;
math: BlockConfig<"math", {}, "inline">;
+ diagram: BlockConfig<"diagram", {}, "inline">;
};
// Renders a block's inline content as a code block. Used for both code blocks
@@ -292,6 +293,8 @@ export const createReactEmailBlockMappingForDefaultSchema = (
codeBlock: (block) =>
codeMapping(block, block.props.language as PrismLanguage, textStyles),
math: (block) => codeMapping(block, "latex" as PrismLanguage, textStyles),
+ diagram: (block) =>
+ codeMapping(block, "mermaid" as PrismLanguage, textStyles),
audio: (block) => {
// Audio icon SVG
const icon = (
diff --git a/packages/xl-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx b/packages/xl-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx
index 96adc8dfba..0fa893c812 100644
--- a/packages/xl-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx
+++ b/packages/xl-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx
@@ -15,10 +15,18 @@ export const defaultReactEmailLinkStyles: ReactEmailLinkStyles = {
link: {},
};
+type ICSchema = DefaultInlineContentSchema & {
+ inlineMath: {
+ type: "inlineMath";
+ propSchema: Record;
+ content: "styled";
+ };
+};
+
export const createReactEmailInlineContentMappingForDefaultSchema = (
linkStyles: ReactEmailLinkStyles = defaultReactEmailLinkStyles,
): InlineContentMapping<
- DefaultInlineContentSchema,
+ ICSchema,
DefaultStyleSchema,
React.ReactElement | React.ReactElement,
React.ReactElement
@@ -35,6 +43,14 @@ export const createReactEmailInlineContentMappingForDefaultSchema = (
text: (ic, t) => {
return t.transformStyledText(ic);
},
+ // Renders inline math as its monospaced LaTeX source.
+ inlineMath: (ic, t) => {
+ return (
+
+ {...ic.content.map((content) => t.transformStyledText(content))}
+
+ );
+ },
});
// Export the original mapping for backward compatibility
diff --git a/packages/xl-odt-exporter/src/odt/defaultSchema/blocks.tsx b/packages/xl-odt-exporter/src/odt/defaultSchema/blocks.tsx
index 08f9bb325b..90a1b2ca36 100644
--- a/packages/xl-odt-exporter/src/odt/defaultSchema/blocks.tsx
+++ b/packages/xl-odt-exporter/src/odt/defaultSchema/blocks.tsx
@@ -10,12 +10,13 @@ import {
StyledText,
TableCell,
} from "@blocknote/core";
-import { ODTExporter } from "../odtExporter.js";
import { multiColumnSchema } from "@blocknote/xl-multi-column";
+import { ODTExporter } from "../odtExporter.js";
type BSchema = DefaultBlockSchema & {
pageBreak: ReturnType;
math: BlockConfig<"math", {}, "inline">;
+ diagram: BlockConfig<"diagram", {}, "inline">;
} & typeof multiColumnSchema.blockSchema;
// Renders a block's inline content as a code paragraph. Used for both code
@@ -525,9 +526,10 @@ export const odtBlockMappingForDefaultSchema: BlockMapping<
);
},
-
+// TODO
codeBlock: codeMapping,
math: codeMapping,
+ diagram: codeMapping,
file: async (block) => {
return (
diff --git a/packages/xl-odt-exporter/src/odt/defaultSchema/inlineContent.tsx b/packages/xl-odt-exporter/src/odt/defaultSchema/inlineContent.tsx
index 94f0fcaae4..f007a82c3f 100644
--- a/packages/xl-odt-exporter/src/odt/defaultSchema/inlineContent.tsx
+++ b/packages/xl-odt-exporter/src/odt/defaultSchema/inlineContent.tsx
@@ -3,11 +3,21 @@ import {
InlineContentMapping,
} from "@blocknote/core";
+type ICSchema = DefaultInlineContentSchema & {
+ inlineMath: {
+ type: "inlineMath";
+ propSchema: Record;
+ content: "styled";
+ };
+};
+
+// `React.ReactNode` result types, matching `ODTExporter`'s `Exporter`
+// generics - mismatched result types make the mappings unassignable.
export const odtInlineContentMappingForDefaultSchema: InlineContentMapping<
- DefaultInlineContentSchema,
+ ICSchema,
any,
- React.JSX.Element,
- React.JSX.Element
+ React.ReactNode,
+ React.ReactNode
> = {
link: (ic, exporter) => {
const content = ic.content.map((c) => exporter.transformStyledText(c));
@@ -28,4 +38,13 @@ export const odtInlineContentMappingForDefaultSchema: InlineContentMapping<
text: (ic, exporter) => {
return exporter.transformStyledText(ic);
},
+// TODO
+ // Renders inline math as its LaTeX source.
+ inlineMath: (ic, exporter) => {
+ return (
+
+ {ic.content.map((content) => exporter.transformStyledText(content))}
+
+ );
+ },
};
diff --git a/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx b/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx
index 5a8ca88124..ad3881671c 100644
--- a/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx
+++ b/packages/xl-pdf-exporter/src/pdf/defaultSchema/blocks.tsx
@@ -2,9 +2,9 @@ import {
BlockConfig,
BlockFromConfigNoChildren,
BlockMapping,
+ createPageBreakBlockConfig,
DefaultBlockSchema,
DefaultProps,
- createPageBreakBlockConfig,
StyledText,
} from "@blocknote/core";
import { multiColumnSchema } from "@blocknote/xl-multi-column";
@@ -24,6 +24,7 @@ const FONT_SIZE = 16;
type BSchema = DefaultBlockSchema & {
pageBreak: ReturnType;
math: BlockConfig<"math", {}, "inline">;
+ diagram: BlockConfig<"diagram", {}, "inline">;
} & typeof multiColumnSchema.blockSchema;
// Renders a block's inline content as monospaced source code. Used for both
@@ -161,8 +162,10 @@ export const pdfBlockMappingForDefaultSchema: BlockMapping<
);
},
+ // TODO
codeBlock: codeMapping,
math: codeMapping,
+ diagram: codeMapping,
pageBreak: () => {
return ;
},
diff --git a/packages/xl-pdf-exporter/src/pdf/defaultSchema/inlinecontent.tsx b/packages/xl-pdf-exporter/src/pdf/defaultSchema/inlinecontent.tsx
index a1ea3cc7e2..5398c77d5a 100644
--- a/packages/xl-pdf-exporter/src/pdf/defaultSchema/inlinecontent.tsx
+++ b/packages/xl-pdf-exporter/src/pdf/defaultSchema/inlinecontent.tsx
@@ -4,8 +4,16 @@ import {
} from "@blocknote/core";
import { Link, Text } from "@react-pdf/renderer";
+type ICSchema = DefaultInlineContentSchema & {
+ inlineMath: {
+ type: "inlineMath";
+ propSchema: Record;
+ content: "styled";
+ };
+};
+
export const pdfInlineContentMappingForDefaultSchema: InlineContentMapping<
- DefaultInlineContentSchema,
+ ICSchema,
any,
React.ReactElement | React.ReactElement,
React.ReactElement
@@ -20,4 +28,13 @@ export const pdfInlineContentMappingForDefaultSchema: InlineContentMapping<
text: (ic, exporter) => {
return exporter.transformStyledText(ic);
},
+ // TODO
+ // Renders inline math as its monospaced LaTeX source.
+ inlineMath: (ic, exporter) => {
+ return (
+
+ {ic.content.map((content) => exporter.transformStyledText(content))}
+
+ );
+ },
};
diff --git a/playground/package.json b/playground/package.json
index 246bdf865d..40857c2fbd 100644
--- a/playground/package.json
+++ b/playground/package.json
@@ -18,6 +18,7 @@
"@blocknote/core": "workspace:^",
"@blocknote/mantine": "workspace:^",
"@blocknote/math-block": "workspace:^",
+ "@blocknote/diagram-block": "workspace:^",
"@blocknote/react": "workspace:^",
"@blocknote/server-util": "workspace:^",
"@blocknote/shadcn": "workspace:^",
diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx
index 3e69de7ac3..ba214b0b1c 100644
--- a/playground/src/examples.gen.tsx
+++ b/playground/src/examples.gen.tsx
@@ -1127,8 +1127,10 @@ export const examples = {
author: "yousefed",
tags: ["Interoperability"],
dependencies: {
- "@blocknote/xl-pdf-exporter": "latest",
+ "@blocknote/diagram-block": "latest",
+ "@blocknote/math-block": "latest",
"@blocknote/xl-multi-column": "latest",
+ "@blocknote/xl-pdf-exporter": "latest",
"@react-pdf/renderer": "^4.3.0",
} as any,
pro: true,
@@ -1152,6 +1154,8 @@ export const examples = {
author: "yousefed",
tags: [""],
dependencies: {
+ "@blocknote/diagram-block": "latest",
+ "@blocknote/math-block": "latest",
"@blocknote/xl-docx-exporter": "latest",
"@blocknote/xl-multi-column": "latest",
} as any,
@@ -1176,8 +1180,10 @@ export const examples = {
author: "areknawo",
tags: [""],
dependencies: {
- "@blocknote/xl-odt-exporter": "latest",
+ "@blocknote/diagram-block": "latest",
+ "@blocknote/math-block": "latest",
"@blocknote/xl-multi-column": "latest",
+ "@blocknote/xl-odt-exporter": "latest",
} as any,
pro: true,
},
@@ -1474,6 +1480,35 @@ export const examples = {
readme:
"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 `