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 @@ + + + + + Diagram Block + + + +
+ + + diff --git a/examples/06-custom-schema/10-diagram-block/main.tsx b/examples/06-custom-schema/10-diagram-block/main.tsx new file mode 100644 index 0000000000..1260513388 --- /dev/null +++ b/examples/06-custom-schema/10-diagram-block/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./src/App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + , +); diff --git a/examples/06-custom-schema/10-diagram-block/package.json b/examples/06-custom-schema/10-diagram-block/package.json new file mode 100644 index 0000000000..6d39ea4b51 --- /dev/null +++ b/examples/06-custom-schema/10-diagram-block/package.json @@ -0,0 +1,33 @@ +{ + "name": "@blocknote/example-custom-schema-diagram-block", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "type": "module", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vp dev", + "dev": "vp dev", + "build:prod": "tsc && vp build", + "preview": "vp preview" + }, + "dependencies": { + "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", + "@blocknote/mantine": "latest", + "@blocknote/react": "latest", + "@blocknote/shadcn": "latest", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "@blocknote/code-block": "latest", + "@blocknote/diagram-block": "latest", + "react-icons": "^5.5.0" + }, + "devDependencies": { + "@types/react": "^19.2.3", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "vite-plus": "catalog:" + } +} diff --git a/examples/06-custom-schema/10-diagram-block/src/App.tsx b/examples/06-custom-schema/10-diagram-block/src/App.tsx new file mode 100644 index 0000000000..4c74ece702 --- /dev/null +++ b/examples/06-custom-schema/10-diagram-block/src/App.tsx @@ -0,0 +1,95 @@ +import { createHighlighter } from "@blocknote/code-block"; +import { BlockNoteSchema } from "@blocknote/core"; +import { + filterSuggestionItems, + insertOrUpdateBlockForSlashMenu, +} from "@blocknote/core/extensions"; +import "@blocknote/core/fonts/inter.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { createReactDiagramBlockSpec } from "@blocknote/diagram-block"; +import { + getDefaultReactSlashMenuItems, + SuggestionMenuController, + useCreateBlockNote, +} from "@blocknote/react"; +import { TbSitemap } from "react-icons/tb"; + +// Our schema with block specs, which contain the configs and implementations +// for blocks that we want our editor to use. +const schema = BlockNoteSchema.create().extend({ + blockSpecs: { + // Creates an instance of the Diagram block and adds it to the schema. + // TODO: naing + diagram: createReactDiagramBlockSpec(), + }, +}); + +// Slash menu item to insert a Diagram block. +// TODO: extract? +const insertDiagram = (editor: typeof schema.BlockNoteEditor) => ({ + title: "Diagram", + subtext: "Insert a diagram rendered from Mermaid source", + onItemClick: () => + insertOrUpdateBlockForSlashMenu(editor, { + type: "diagram", + content: "graph TD\n A[Start] --> B[Stop]", + }), + aliases: ["mermaid", "diagram", "flowchart", "chart", "graph"], + group: "Advanced", + icon: , +}); + +export default function App() { + const editor = useCreateBlockNote({ + // Configures the syntax highlighting extension to use Mermaid syntax + // highlighting in the Diagram block's source popup. + syntaxHighlighting: { + createHighlighter, + highlightBlock: (block) => + block.type === "diagram" ? "mermaid" : block.props.language, + }, + schema, + initialContent: [ + { + type: "paragraph", + content: "Click a diagram to edit its Mermaid source:", + }, + { + type: "diagram", + content: `graph TD + A[Write docs] --> B{Diagram needed?} + B -->|Yes| C[Type /diagram] + B -->|No| D[Keep writing] + C --> D`, + }, + { + type: "paragraph", + content: "Press the '/' key to open the Slash Menu and add another", + }, + ], + }); + + // Renders the editor instance using a React component. + return ( + + {/* Replaces the default Slash Menu. */} + { + // Gets all default slash menu items. + const defaultItems = getDefaultReactSlashMenuItems(editor); + // Finds index of last item in "Advanced" group. + const lastAdvancedIndex = defaultItems.findLastIndex( + (item) => item.group === "Advanced", + ); + // Inserts the Diagram item at the end of the "Advanced" group. + defaultItems.splice(lastAdvancedIndex + 1, 0, insertDiagram(editor)); + + // Returns filtered items based on the query. + return filterSuggestionItems(defaultItems, query); + }} + /> + + ); +} diff --git a/examples/06-custom-schema/10-diagram-block/tsconfig.json b/examples/06-custom-schema/10-diagram-block/tsconfig.json new file mode 100644 index 0000000000..93fa81bee8 --- /dev/null +++ b/examples/06-custom-schema/10-diagram-block/tsconfig.json @@ -0,0 +1,29 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": ["."], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} diff --git a/examples/06-custom-schema/10-diagram-block/vite-env.d.ts b/examples/06-custom-schema/10-diagram-block/vite-env.d.ts new file mode 100644 index 0000000000..bc2d8a36f3 --- /dev/null +++ b/examples/06-custom-schema/10-diagram-block/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/06-custom-schema/10-diagram-block/vite.config.ts b/examples/06-custom-schema/10-diagram-block/vite.config.ts new file mode 100644 index 0000000000..0133a6da9e --- /dev/null +++ b/examples/06-custom-schema/10-diagram-block/vite.config.ts @@ -0,0 +1,31 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite-plus"; +// https://vitejs.dev/config/ +export default defineConfig(((conf: { command: string }) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/", + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/", + ), + } as any), + }, +})) as Parameters[0]); diff --git a/packages/core/src/blocks/Code/helpers/extensions/SourceBlockWithPreviewExtension.ts b/packages/core/src/blocks/Code/helpers/extensions/SourceBlockWithPreviewExtension.ts index 5832f05038..e3ca5a9697 100644 --- a/packages/core/src/blocks/Code/helpers/extensions/SourceBlockWithPreviewExtension.ts +++ b/packages/core/src/blocks/Code/helpers/extensions/SourceBlockWithPreviewExtension.ts @@ -1,3 +1,5 @@ +import { TextSelection } from "prosemirror-state"; + import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; import { createExtension, @@ -8,13 +10,30 @@ import { Block } from "../../../index.js"; export const SourceBlockWithPreviewExtension = createExtension( ({ editor, - options: { key, blockType, hasPreview, runsBefore = [] }, + options: { + key, + blockType, + hasPreview, + enterBehaviour = "close", + runsBefore = [], + }, }: { editor: BlockNoteEditor; options: { key: string; blockType: string; hasPreview: (block: Block) => boolean; + /** + * What pressing Enter does while the popup is open: + * - `"close"`: commits the source and closes the popup - for + * single-line sources (e.g. math). + * - `"newline"`: inserts a line break into the source - for multiline + * sources (e.g. diagrams); the popup is then closed with Escape, the + * "OK" button, or by moving the selection out. + * + * @default "close" + */ + enterBehaviour?: "close" | "newline"; runsBefore?: readonly string[]; }; }) => { @@ -55,13 +74,26 @@ export const SourceBlockWithPreviewExtension = createExtension( store, runsBefore, keyboardShortcuts: { - // Toggles the popup. + // Toggles the popup. With the "newline" Enter behaviour, Enter + // inserts a line break while the popup is open instead of closing it. Enter: ({ editor }) => { const { block } = editor.getTextCursorPosition(); if (!blockHasPreview(block)) { return false; } + if ( + enterBehaviour === "newline" && + store.state.popupOpen === block.id + ) { + const view = editor.prosemirrorView!; + view.dispatch( + view.state.tr.insertText("\n") + ); + + return true; + } + editor.setTextCursorPosition(block.id, "end"); store.setState((state) => ({ ...state, @@ -84,6 +116,28 @@ export const SourceBlockWithPreviewExtension = createExtension( return true; }, + // While the popup is open, selects the whole source instead of the + // whole document. + "Mod-a": ({ editor }) => { + const { block } = editor.getTextCursorPosition(); + if (!blockHasPreview(block) || store.state.popupOpen !== block.id) { + return false; + } + + const view = editor.prosemirrorView!; + const { $from } = view.state.selection; + if ($from.parent.type.name !== blockType) { + return false; + } + + view.dispatch( + view.state.tr.setSelection( + TextSelection.create(view.state.doc, $from.start(), $from.end()), + ), + ); + + return true; + }, // While the popup is closed, moves the selection straight to the previous/next block // instead of into the (hidden) source. ArrowUp: handleArrow("prev"), diff --git a/packages/core/src/blocks/Code/helpers/extensions/SourceInlineContentWithPreviewExtension.ts b/packages/core/src/blocks/Code/helpers/extensions/SourceInlineContentWithPreviewExtension.ts index 04951e9cd7..79c2d640b8 100644 --- a/packages/core/src/blocks/Code/helpers/extensions/SourceInlineContentWithPreviewExtension.ts +++ b/packages/core/src/blocks/Code/helpers/extensions/SourceInlineContentWithPreviewExtension.ts @@ -1,4 +1,4 @@ -import { Selection } from "prosemirror-state"; +import { Selection, TextSelection } from "prosemirror-state"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; import { @@ -71,6 +71,23 @@ export const SourceInlineContentWithPreviewExtension = createExtension( Escape: moveSelectionOut("after"), ArrowUp: moveSelectionOut("before"), ArrowDown: moveSelectionOut("after"), + // While editing the source, selects the whole source instead of the + // whole document. + "Mod-a": ({ editor }) => { + const { $from } = editor.prosemirrorState.selection; + if ($from.node().type.name !== inlineContentType) { + return false; + } + + const view = editor.prosemirrorView!; + view.dispatch( + view.state.tr.setSelection( + TextSelection.create(view.state.doc, $from.start(), $from.end()), + ), + ); + + return true; + }, }, mount: ({ dom, signal }) => { // The popup is open exactly when the selection is inside the inline diff --git a/packages/core/src/blocks/Code/helpers/render/createSourceBlockWithPreview.ts b/packages/core/src/blocks/Code/helpers/render/createSourceBlockWithPreview.ts index c8aa7ce95f..34e4f164e8 100644 --- a/packages/core/src/blocks/Code/helpers/render/createSourceBlockWithPreview.ts +++ b/packages/core/src/blocks/Code/helpers/render/createSourceBlockWithPreview.ts @@ -9,17 +9,19 @@ import { SourceBlockWithPreviewExtension } from "../extensions/SourceBlockWithPr // Element shown instead of the preview when block has no content. const createAddSourceButton = (editor: BlockNoteEditor) => { const addSourceButton = document.createElement("div"); - addSourceButton.className = "bn-add-source-code-button"; + addSourceButton.className = "bn-preview-placeholder"; addSourceButton.contentEditable = "false"; const addSourceButtonIcon = document.createElement("div"); - addSourceButtonIcon.className = "bn-add-source-code-button-icon"; + addSourceButtonIcon.className = "bn-preview-placeholder-icon"; + // Decorative next to the text, so hidden from screen readers. + addSourceButtonIcon.setAttribute("aria-hidden", "true"); addSourceButtonIcon.innerHTML = ''; addSourceButton.appendChild(addSourceButtonIcon); const addSourceButtonText = document.createElement("p"); - addSourceButtonText.className = "bn-add-source-code-button-text"; + addSourceButtonText.className = "bn-preview-placeholder-text"; addSourceButtonText.textContent = editor.dictionary.code_block.add_source_button_text; addSourceButton.appendChild(addSourceButtonText); diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index 7b22546a31..7f0f60310b 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -501,7 +501,12 @@ NESTED BLOCKS box-shadow: var(--bn-shadow-medium); max-height: 200px; opacity: 0; - overflow: auto; + /* Only the source itself scrolls (see the popup body's `pre`), so the + language select, "OK" button, and error message stay visible for large + multiline sources. */ + display: flex; + flex-direction: column; + overflow: hidden; pointer-events: none; } @@ -538,11 +543,14 @@ NESTED BLOCKS .bn-code-block-source-popup-body { display: flex; align-items: flex-end; + flex: 1; + min-height: 0; } .bn-code-block-source-popup-body > pre { white-space: pre; - overflow-x: auto; + overflow: auto; + align-self: stretch; margin: 0; flex: 1; min-width: 0; @@ -581,7 +589,10 @@ NESTED BLOCKS background-color: rgb(29, 78, 216); } -.bn-add-source-code-button { +/* Shown in place of the preview when there's nothing to render (see + `PreviewPlaceholder`): the "add source" state for an empty source, or - + with the error modifier - the compact error state. */ +.bn-preview-placeholder { align-items: center; background-color: rgb(242, 241, 238); border-radius: 4px; @@ -591,27 +602,45 @@ NESTED BLOCKS padding: 12px; } -.bn-add-source-code-button:where(.dark, .dark *) { +.bn-preview-placeholder:where(.dark, .dark *) { background-color: rgb(70, 70, 70); color: rgb(190, 190, 190); } -.bn-editor[contenteditable="true"] .bn-add-source-code-button:hover { +.bn-editor[contenteditable="true"] + .bn-preview-placeholder:not(.bn-preview-placeholder-error):hover { background-color: rgb(225, 225, 225); } .bn-editor[contenteditable="true"] - .bn-add-source-code-button:hover:where(.dark, .dark *) { + .bn-preview-placeholder:not(.bn-preview-placeholder-error):hover:where( + .dark, + .dark * + ) { background-color: rgb(90, 90, 90); } -.bn-add-source-code-button-icon { +/* The error state only differs in color. The highlight variables already + flip with the theme, so this also overrides the dark rule above. */ +.bn-preview-placeholder-error { + background-color: var(--bn-colors-highlights-red-background); + color: var(--bn-colors-highlights-red-text); +} + +.bn-preview-placeholder-icon { display: flex; width: 24px; height: 24px; } -.bn-add-source-code-button-text { +/* Icons render at 1em by default (e.g. react-icons), so stretch them to the + wrapper's size instead. */ +.bn-preview-placeholder-icon > svg { + width: 100%; + height: 100%; +} + +.bn-preview-placeholder-text { font-size: 0.9rem; margin: 0; } @@ -639,9 +668,9 @@ NESTED BLOCKS box-sizing: content-box; } -/* The "add source" button sits inline among text, so it needs to be compact - rather than the full-size block-preview button. */ -.bn-inline-content-section .bn-add-source-code-button { +/* The placeholder sits inline among text, so it needs to be compact rather + than the full-size block-preview version. */ +.bn-inline-content-section .bn-preview-placeholder { display: inline-flex; gap: 4px; height: 19px; @@ -649,12 +678,12 @@ NESTED BLOCKS padding: 0 6px; } -.bn-inline-content-section .bn-add-source-code-button-icon { +.bn-inline-content-section .bn-preview-placeholder-icon { width: 16px; height: 16px; } -.bn-inline-content-section .bn-add-source-code-button-text { +.bn-inline-content-section .bn-preview-placeholder-text { font-size: 0.85em; } diff --git a/packages/core/src/exporter/mapping.ts b/packages/core/src/exporter/mapping.ts index 0dca63ebc3..fbe88d693e 100644 --- a/packages/core/src/exporter/mapping.ts +++ b/packages/core/src/exporter/mapping.ts @@ -42,7 +42,10 @@ export type InlineContentMapping< > = { [K in keyof I]: ( inlineContent: InlineContentFromConfig, - exporter: Exporter, + // Deliberately loose on the schema generics, like `BlockMapping` above - + // otherwise a mapping declared for one schema can't be reused (e.g. + // spread) in a mapping for a schema with different types. + exporter: Exporter, ) => RI; }; diff --git a/packages/diagram-block/.gitignore b/packages/diagram-block/.gitignore new file mode 100644 index 0000000000..58f115c8dc --- /dev/null +++ b/packages/diagram-block/.gitignore @@ -0,0 +1,23 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/diagram-block/LICENSE b/packages/diagram-block/LICENSE new file mode 100644 index 0000000000..fa0086a952 --- /dev/null +++ b/packages/diagram-block/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/packages/diagram-block/package.json b/packages/diagram-block/package.json new file mode 100644 index 0000000000..ff777e784c --- /dev/null +++ b/packages/diagram-block/package.json @@ -0,0 +1,77 @@ +{ + "name": "@blocknote/diagram-block", + "homepage": "https://github.com/TypeCellOS/BlockNote", + "private": false, + "sideEffects": [ + "*.css" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/TypeCellOS/BlockNote.git", + "directory": "packages/diagram-block" + }, + "license": "MPL-2.0", + "version": "0.51.4", + "files": [ + "dist", + "types", + "src" + ], + "keywords": [ + "react", + "javascript", + "editor", + "typescript", + "prosemirror", + "wysiwyg", + "rich-text-editor", + "notion", + "yjs", + "block-based", + "tiptap", + "mermaid", + "diagram", + "flowchart" + ], + "description": "A \"Notion-style\" block-based extensible text editor built on top of Prosemirror and Tiptap.", + "type": "module", + "source": "src/index.ts", + "types": "./types/src/index.d.ts", + "main": "./dist/blocknote-diagram-block.cjs", + "module": "./dist/blocknote-diagram-block.js", + "exports": { + ".": { + "types": "./types/src/index.d.ts", + "import": "./dist/blocknote-diagram-block.js", + "require": "./dist/blocknote-diagram-block.cjs" + } + }, + "scripts": { + "dev": "vp dev", + "lint": "vp lint src", + "test": "vp test --run", + "test-watch": "vp test watch", + "clean": "rimraf dist && rimraf types" + }, + "dependencies": { + "mermaid": "^11.0.0" + }, + "devDependencies": { + "@blocknote/react": "workspace:^", + "react-icons": "^5.5.0", + "@types/react": "^19.2.3", + "@types/react-dom": "^19.2.3", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "rimraf": "^5.0.10", + "rollup-plugin-webpack-stats": "^0.2.6", + "typescript": "^5.9.3", + "vite-plus": "catalog:" + }, + "peerDependencies": { + "@blocknote/core": "workspace:^", + "@blocknote/react": "workspace:^", + "react": "^18.0 || ^19.0 || >= 19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" + } +} diff --git a/packages/diagram-block/src/block/createDiagramBlockConfig.ts b/packages/diagram-block/src/block/createDiagramBlockConfig.ts new file mode 100644 index 0000000000..824e9961f4 --- /dev/null +++ b/packages/diagram-block/src/block/createDiagramBlockConfig.ts @@ -0,0 +1,15 @@ +import { createBlockConfig } from "@blocknote/core"; + +export const createDiagramBlockConfig = createBlockConfig( + () => + ({ + type: "diagram" as const, + // The block is semantically a diagram; Mermaid is its (only, for now) + // rendering engine. An `engine` prop with a "mermaid" default can be + // added later without breaking stored documents. + propSchema: {}, + content: "inline" as const, + }) as const, +); + +export type DiagramBlockConfig = ReturnType; diff --git a/packages/diagram-block/src/block/react/DiagramBlockPreviewWithPopup.tsx b/packages/diagram-block/src/block/react/DiagramBlockPreviewWithPopup.tsx new file mode 100644 index 0000000000..ac1139f34e --- /dev/null +++ b/packages/diagram-block/src/block/react/DiagramBlockPreviewWithPopup.tsx @@ -0,0 +1,96 @@ +import { + PreviewPlaceholder, + ReactCustomBlockRenderProps, + SourceBlockWithPreview, +} from "@blocknote/react"; +import mermaid from "mermaid"; +import { useEffect, useState } from "react"; +import { SiMermaid } from "react-icons/si"; + +import { getDiagramPlainTextContent } from "../../shared/getDiagramPlainTextContent.js"; +import { initializeMermaid } from "../../shared/initializeMermaid.js"; +import { DiagramBlockConfig } from "../createDiagramBlockConfig.js"; + +// Each render call needs its own element ID. +let mermaidElementId = 0; + +/** + * Renders the Mermaid source to an SVG string. The current diagram (or the + * last valid one, when the source has an error) stays up until the new one + * has fully rendered, and swapping inline SVG commits in a single frame - so + * the preview never flashes. + */ +export const useMermaidSVG = (source: string) => { + const [svg, setSVG] = useState(""); + const [error, setError] = useState(undefined); + + useEffect(() => { + if (!source.trim()) { + setSVG(""); + setError(undefined); + + return; + } + + initializeMermaid(); + + // Rendering is asynchronous, so bail out if the source changed (or the + // block was removed) before it finished. + let stale = false; + void (async () => { + // The rendered SVG carries the given ID into the document, and Mermaid + // removes any existing element with that ID when rendering. So each + // render gets a fresh ID - reusing one makes Mermaid yank the displayed + // diagram out of the page mid-render. + const id = `mermaid-preview-${mermaidElementId++}`; + try { + await mermaid.parse(source); + const { svg } = await mermaid.render(id, source); + if (!stale) { + setSVG(svg); + setError(undefined); + } + } catch (err) { + if (!stale) { + setError(err instanceof Error ? err.message : String(err)); + } + } + })(); + + return () => { + stale = true; + }; + }, [source]); + + return { svg, error }; +}; +export const DiagramBlockPreviewWithPopup = ( + props: ReactCustomBlockRenderProps, +) => { + const source = getDiagramPlainTextContent(props.block.content).trim(); + const { svg, error } = useMermaidSVG(source); + + return ( + + ) : undefined + } + error={error} + emptySourcePlaceholder={ + } text="Add a Mermaid diagram" /> + } + /> + ); +}; diff --git a/packages/diagram-block/src/block/react/createReactDiagramBlockSpec.test.tsx b/packages/diagram-block/src/block/react/createReactDiagramBlockSpec.test.tsx new file mode 100644 index 0000000000..54cc1d0ef5 --- /dev/null +++ b/packages/diagram-block/src/block/react/createReactDiagramBlockSpec.test.tsx @@ -0,0 +1,272 @@ +import { BlockNoteEditor, BlockNoteSchema } from "@blocknote/core"; +import { BlockNoteViewRaw } from "@blocknote/react"; +import { flushSync } from "react-dom"; +import { createRoot, Root } from "react-dom/client"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vite-plus/test"; +import { createReactDiagramBlockSpec } from "./createReactDiagramBlockSpec.js"; + +// TODO: deprecate jsdom, use vitest browser test? +/** + * @vitest-environment jsdom + */ + +// Mermaid needs a real browser to render, so the tests mock it - the specs +// under test cover the block's editing behavior, not Mermaid itself. Sources +// containing "INVALID" fail to parse, for testing the error states. +vi.mock("mermaid", () => ({ + default: { + initialize: vi.fn(), + parse: vi.fn(async (source: string) => { + if (source.includes("INVALID")) { + throw new Error("mock parse error"); + } + return true; + }), + render: vi.fn(async () => ({ svg: '' })), + }, +})); + +// The diagram block isn't a default block, so register it in a custom schema. +const schema = BlockNoteSchema.create().extend({ + blockSpecs: { diagram: createReactDiagramBlockSpec() }, +}); + +describe("Diagram block source popup", () => { + let editor: BlockNoteEditor; + let div: HTMLDivElement; + let root: Root; + + beforeEach(async () => { + // Mounted in the document tree so capture-phase handlers on ancestors see + // dispatched events. + div = document.createElement("div"); + document.body.appendChild(div); + + editor = BlockNoteEditor.create({ + schema, + trailingBlock: false, + initialContent: [ + { id: "para", type: "paragraph", content: "before" }, + { id: "diagram", type: "diagram", content: "graph TD\n A --> B" }, + ], + }); + + root = createRoot(div); + flushSync(() => { + root.render(); + }); + // Let the React node view render (and the mocked Mermaid "render") before + // assertions read its DOM. + await flush(); + + editor.setTextCursorPosition("diagram", "start"); + }); + + afterEach(() => { + root.unmount(); + editor._tiptapEditor.destroy(); + editor = undefined as any; + div.remove(); + }); + + /** Yields to the event loop so async renders & store updates can flush. */ + function flush() { + return new Promise((resolve) => setTimeout(resolve, 0)); + } + + /** The preview-with-source-popup root, which holds `data-open`. */ + function previewRoot(): HTMLElement { + return div.querySelector(".bn-preview-with-source-popup") as HTMLElement; + } + + /** Whether the source popup is open (the source is being edited). */ + function isPopupOpen(): boolean { + return previewRoot()?.getAttribute("data-open") === "true"; + } + + /** The diagram block's source as plain text. */ + function source(): string { + const block = editor.getBlock("diagram")!; + return (block.content as { text: string }[]) + .map((node) => node.text ?? "") + .join(""); + } + + /** Dispatches a keydown on the editor DOM, running the ProseMirror keymap. + * Returns whether the default was prevented. */ + function pressKey(key: string, init: KeyboardEventInit = {}): boolean { + const event = new KeyboardEvent("keydown", { + key, + bubbles: true, + cancelable: true, + ...init, + }); + editor.prosemirrorView!.dom.dispatchEvent(event); + return event.defaultPrevented; + } + + it("renders the (mocked) diagram preview", () => { + expect( + previewRoot().querySelector( + '.bn-preview-container [data-mermaid-mock="true"]', + ), + ).not.toBeNull(); + }); + + it("Enter opens the source popup", async () => { + expect(isPopupOpen()).toBe(false); + + expect(pressKey("Enter")).toBe(true); + await flush(); + + expect(isPopupOpen()).toBe(true); + }); + + it("Enter inserts a line break while the popup is open (newline enter behaviour)", async () => { + pressKey("Enter"); + await flush(); + expect(isPopupOpen()).toBe(true); + const sourceBefore = source(); + + // Unlike the (single-line) math block, Enter extends the source instead + // of closing the popup. + expect(pressKey("Enter")).toBe(true); + await flush(); + + expect(isPopupOpen()).toBe(true); + expect(source()).toBe(sourceBefore + "\n"); + }); + + it("Mod+A selects only the source while the popup is open", async () => { + pressKey("Enter"); + await flush(); + expect(isPopupOpen()).toBe(true); + + // jsdom isn't detected as macOS, so "Mod" resolves to Ctrl. + expect(pressKey("a", { ctrlKey: true })).toBe(true); + await flush(); + + // The selection spans exactly the block's source, not the document. + const selection = editor.prosemirrorState.selection; + expect( + editor.prosemirrorState.doc.textBetween(selection.from, selection.to), + ).toBe(source()); + expect(isPopupOpen()).toBe(true); + }); + + it("Escape closes the source popup", async () => { + pressKey("Enter"); + await flush(); + expect(isPopupOpen()).toBe(true); + + expect(pressKey("Escape")).toBe(true); + await flush(); + + expect(isPopupOpen()).toBe(false); + }); + + it("blocks character input while the popup is closed", () => { + expect(isPopupOpen()).toBe(false); + + // The source is hidden, so the keystroke is swallowed rather than + // silently editing the source the user can't see. + expect(pressKey("a")).toBe(true); + }); + + /** The mocked rendered diagram, if currently shown as the preview. */ + function renderedDiagram(): Element | null { + return previewRoot().querySelector( + '.bn-preview-container [data-mermaid-mock="true"]', + ); + } + + /** The compact error state, if currently shown as the preview. */ + function errorState(): Element | null { + return previewRoot().querySelector( + ".bn-preview-container .bn-preview-placeholder-error", + ); + } + + it("keeps the last preview while editing an erroneous source", async () => { + pressKey("Enter"); + await flush(); + expect(isPopupOpen()).toBe(true); + + editor.updateBlock("diagram", { content: "graph INVALID" }); + await flush(); + + // The last valid diagram stays up instead of the error state. + expect(renderedDiagram()).not.toBeNull(); + expect(errorState()).toBeNull(); + }); + + it("shows the error state once an erroneous source is committed", async () => { + pressKey("Enter"); + await flush(); + editor.updateBlock("diagram", { content: "graph INVALID" }); + await flush(); + + pressKey("Escape"); + await flush(); + expect(isPopupOpen()).toBe(false); + + // Committed with an error, so the (stale) preview is replaced by the + // error state. + expect(renderedDiagram()).toBeNull(); + expect(errorState()).not.toBeNull(); + }); + + it("keeps showing the error state when reopening a committed error", async () => { + pressKey("Enter"); + await flush(); + editor.updateBlock("diagram", { content: "graph INVALID" }); + await flush(); + pressKey("Escape"); + await flush(); + expect(errorState()).not.toBeNull(); + + pressKey("Enter"); + await flush(); + expect(isPopupOpen()).toBe(true); + + // The error was committed, so reopening doesn't bring the stale preview + // back - the error state stays until the source renders successfully. + expect(renderedDiagram()).toBeNull(); + expect(errorState()).not.toBeNull(); + }); + + it("shows the error state while editing when there's no last preview", async () => { + // A fresh block whose source never rendered successfully. + editor.removeBlocks(["diagram"]); + editor.insertBlocks( + [{ id: "broken", type: "diagram", content: "graph INVALID" }], + "para", + "after", + ); + await flush(); + + expect(renderedDiagram()).toBeNull(); + expect(errorState()).not.toBeNull(); + }); + + it("shows the placeholder for an empty source", async () => { + editor.removeBlocks(["diagram"]); + editor.insertBlocks( + [{ id: "empty", type: "diagram", content: "" }], + "para", + "after", + ); + await flush(); + + expect( + previewRoot().querySelector(".bn-preview-placeholder-text")?.textContent, + ).toBe("Add a Mermaid diagram"); + }); +}); diff --git a/packages/diagram-block/src/block/react/createReactDiagramBlockSpec.tsx b/packages/diagram-block/src/block/react/createReactDiagramBlockSpec.tsx new file mode 100644 index 0000000000..84374564f2 --- /dev/null +++ b/packages/diagram-block/src/block/react/createReactDiagramBlockSpec.tsx @@ -0,0 +1,37 @@ +import { SourceBlockWithPreviewExtension } from "@blocknote/core"; +import { createReactBlockSpec } from "@blocknote/react"; + +import { createDiagramBlockConfig } from "../createDiagramBlockConfig.js"; +import { + parseDiagramCodeContent, + parseDiagramCodeElement, +} from "../shared/parse/parseDiagramCodeElement.js"; +import { DiagramBlockPreviewWithPopup } from "./DiagramBlockPreviewWithPopup.js"; + +export const createReactDiagramBlockSpec = createReactBlockSpec( + createDiagramBlockConfig, + { + meta: { + code: true, + defining: true, + isolating: false, + }, + parse: parseDiagramCodeElement, + parseContent: parseDiagramCodeContent, + // The code block also parses `
` 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 `` element, and pasting MathML back in is converted to LaTeX.\n\n**Try it out:** Click a formula to edit its LaTeX!\n\n**Relevant Docs:**\n\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)", }, + { + projectSlug: "diagram-block", + fullSlug: "custom-schema/diagram-block", + pathFromRoot: "examples/06-custom-schema/10-diagram-block", + config: { + 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", + } as any, + }, + title: "Diagram Block", + group: { + pathFromRoot: "examples/06-custom-schema", + slug: "custom-schema", + }, + readme: + "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.\n\n**Try it out:** Click a diagram to edit its Mermaid source!\n\n**Relevant Docs:**\n\n- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)\n- [Editor Setup](/docs/getting-started/editor-setup)", + }, { projectSlug: "draggable-inline-content", fullSlug: "custom-schema/draggable-inline-content", diff --git a/playground/vite.config.ts b/playground/vite.config.ts index 1a8e8590ca..2762b8f046 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -36,6 +36,10 @@ const devAliases: Record = { "../packages/xl-email-exporter/src", ), "@blocknote/math-block": resolve(__dirname, "../packages/math-block/src"), + "@blocknote/diagram-block": resolve( + __dirname, + "../packages/diagram-block/src", + ), // "@liveblocks/react-blocknote": resolve( // __dirname, // "../../liveblocks/packages/liveblocks-react-blocknote/src/", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba54b2fbd4..b12aea197d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ importers: '@blocknote/core': specifier: workspace:* version: link:../packages/core + '@blocknote/diagram-block': + specifier: workspace:* + version: link:../packages/diagram-block '@blocknote/mantine': specifier: workspace:* version: link:../packages/mantine @@ -226,6 +229,9 @@ importers: class-variance-authority: specifier: ^0.7.1 version: 0.7.1 + docx: + specifier: ^9.6.1 + version: 9.6.1 framer-motion: specifier: ^12.26.2 version: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -247,6 +253,9 @@ importers: lucide-react: specifier: ^0.562.0 version: 0.562.0(react@19.2.5) + mermaid: + specifier: ^11.0.0 + version: 11.16.0 motion: specifier: ^12.28.1 version: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -2728,9 +2737,15 @@ importers: '@blocknote/core': specifier: latest version: link:../../../packages/core + '@blocknote/diagram-block': + specifier: latest + version: link:../../../packages/diagram-block '@blocknote/mantine': specifier: latest version: link:../../../packages/mantine + '@blocknote/math-block': + specifier: latest + version: link:../../../packages/math-block '@blocknote/react': specifier: latest version: link:../../../packages/react @@ -2780,9 +2795,15 @@ importers: '@blocknote/core': specifier: latest version: link:../../../packages/core + '@blocknote/diagram-block': + specifier: latest + version: link:../../../packages/diagram-block '@blocknote/mantine': specifier: latest version: link:../../../packages/mantine + '@blocknote/math-block': + specifier: latest + version: link:../../../packages/math-block '@blocknote/react': specifier: latest version: link:../../../packages/react @@ -2829,9 +2850,15 @@ importers: '@blocknote/core': specifier: latest version: link:../../../packages/core + '@blocknote/diagram-block': + specifier: latest + version: link:../../../packages/diagram-block '@blocknote/mantine': specifier: latest version: link:../../../packages/mantine + '@blocknote/math-block': + specifier: latest + version: link:../../../packages/math-block '@blocknote/react': specifier: latest version: link:../../../packages/react @@ -3413,6 +3440,58 @@ importers: specifier: 'catalog:' version: 0.1.24(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(terser@5.46.2)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)) + examples/06-custom-schema/10-diagram-block: + dependencies: + '@blocknote/ariakit': + specifier: latest + version: link:../../../packages/ariakit + '@blocknote/code-block': + specifier: latest + version: link:../../../packages/code-block + '@blocknote/core': + specifier: latest + version: link:../../../packages/core + '@blocknote/diagram-block': + specifier: latest + version: link:../../../packages/diagram-block + '@blocknote/mantine': + specifier: latest + version: link:../../../packages/mantine + '@blocknote/react': + specifier: latest + version: link:../../../packages/react + '@blocknote/shadcn': + specifier: latest + version: link:../../../packages/shadcn + '@mantine/core': + specifier: ^9.0.2 + version: 9.1.1(@mantine/hooks@9.1.1(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@mantine/hooks': + specifier: ^9.0.2 + version: 9.1.1(react@19.2.5) + react: + specifier: ^19.2.3 + version: 19.2.5 + react-dom: + specifier: ^19.2.3 + version: 19.2.5(react@19.2.5) + react-icons: + specifier: ^5.5.0 + version: 5.6.0(react@19.2.5) + devDependencies: + '@types/react': + specifier: ^19.2.3 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)) + vite-plus: + specifier: 'catalog:' + version: 0.1.24(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(terser@5.46.2)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)) + examples/06-custom-schema/draggable-inline-content: dependencies: '@blocknote/ariakit': @@ -4764,6 +4843,46 @@ importers: specifier: 'catalog:' version: 0.1.24(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(terser@5.46.2)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)) + packages/diagram-block: + dependencies: + '@blocknote/core': + specifier: workspace:^ + version: link:../core + mermaid: + specifier: ^11.0.0 + version: 11.16.0 + devDependencies: + '@blocknote/react': + specifier: workspace:^ + version: link:../react + '@types/react': + specifier: ^19.2.3 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + react: + specifier: ^19.2.5 + version: 19.2.5 + react-dom: + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) + react-icons: + specifier: ^5.5.0 + version: 5.6.0(react@19.2.5) + rimraf: + specifier: ^5.0.10 + version: 5.0.10 + rollup-plugin-webpack-stats: + specifier: ^0.2.6 + version: 0.2.6(rollup@4.60.1) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite-plus: + specifier: 'catalog:' + version: 0.1.24(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(terser@5.46.2)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)) + packages/mantine: dependencies: '@blocknote/core': @@ -4858,6 +4977,9 @@ importers: react-dom: specifier: ^19.2.5 version: 19.2.5(react@19.2.5) + react-icons: + specifier: ^5.5.0 + version: 5.6.0(react@19.2.5) rimraf: specifier: ^5.0.10 version: 5.0.10 @@ -5303,6 +5425,9 @@ importers: specifier: ^0.2.2 version: 0.2.2 devDependencies: + '@blocknote/diagram-block': + specifier: workspace:^ + version: link:../diagram-block '@blocknote/shared': specifier: workspace:^ version: link:../../shared @@ -5456,6 +5581,9 @@ importers: specifier: ^0.2.2 version: 0.2.2 devDependencies: + '@blocknote/diagram-block': + specifier: workspace:^ + version: link:../diagram-block '@blocknote/shared': specifier: workspace:^ version: link:../../shared @@ -5502,6 +5630,9 @@ importers: specifier: ^4.3.0 version: 4.3.2(react@19.2.5) devDependencies: + '@blocknote/diagram-block': + specifier: workspace:^ + version: link:../diagram-block '@blocknote/shared': specifier: workspace:^ version: link:../../shared @@ -5568,6 +5699,9 @@ importers: '@blocknote/core': specifier: workspace:^ version: link:../packages/core + '@blocknote/diagram-block': + specifier: workspace:^ + version: link:../packages/diagram-block '@blocknote/mantine': specifier: workspace:^ version: link:../packages/mantine @@ -5760,6 +5894,9 @@ importers: '@blocknote/core': specifier: workspace:^ version: link:../packages/core + '@blocknote/diagram-block': + specifier: workspace:^ + version: link:../packages/diagram-block '@blocknote/mantine': specifier: workspace:^ version: link:../packages/mantine @@ -5879,6 +6016,9 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@ariakit/core@0.4.18': resolution: {integrity: sha512-9urEa+GbZTSyredq3B/3thQjTcSZSUC68XctwCkJNH/xNfKN5O+VThiem2rcJxpsGw8sRUQenhagZi0yB4foyg==} @@ -6207,10 +6347,16 @@ packages: '@better-fetch/fetch@1.1.21': resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + '@bramus/specificity@2.4.2': resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -6836,6 +6982,12 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.3': + resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -7241,6 +7393,9 @@ packages: '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + '@mermaid-js/parser@1.2.0': + resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==} + '@mswjs/interceptors@0.37.6': resolution: {integrity: sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==} engines: {node: '>=18'} @@ -9925,30 +10080,96 @@ packages: '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + '@types/d3-ease@3.0.2': resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + '@types/d3-interpolate@3.0.4': resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} '@types/d3-path@3.1.1': resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + '@types/d3-scale@4.0.9': resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + '@types/d3-shape@3.1.8': resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + '@types/d3-time@3.0.4': resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -9970,6 +10191,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -10097,6 +10321,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -10270,6 +10497,9 @@ packages: peerDependencies: '@uppy/core': ^3.13.0 + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + '@vercel/analytics@1.6.1': resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==} peerDependencies: @@ -11125,6 +11355,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + commander@8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} @@ -11190,6 +11424,12 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} @@ -11223,34 +11463,129 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.34.0: + resolution: {integrity: sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + d3-color@3.1.0: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + d3-ease@3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + d3-format@3.1.2: resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} engines: {node: '>=12'} + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + d3-interpolate@3.0.1: resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} engines: {node: '>=12'} + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + d3-path@3.1.0: resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} engines: {node: '>=12'} + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + d3-scale@4.0.2: resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} engines: {node: '>=12'} + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + d3-shape@3.2.0: resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} engines: {node: '>=12'} @@ -11267,6 +11602,23 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -11293,6 +11645,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + debounce-fn@6.0.0: resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} engines: {node: '>=18'} @@ -11377,6 +11732,9 @@ packages: defu@6.1.6: resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -11436,6 +11794,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -12108,6 +12469,9 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -12251,6 +12615,9 @@ packages: resolution: {integrity: sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg==} engines: {node: '>=18'} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -12278,6 +12645,9 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} @@ -12631,6 +13001,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -12643,6 +13016,12 @@ packages: resolution: {integrity: sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA==} engines: {node: '>=20.0.0'} + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -12746,6 +13125,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -12836,6 +13218,11 @@ packages: engines: {node: '>= 18'} hasBin: true + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -12903,6 +13290,9 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + mermaid@11.16.0: + resolution: {integrity: sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -13430,6 +13820,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.7.0: + resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==} + pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} @@ -13462,6 +13855,9 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -13605,6 +14001,12 @@ packages: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -14106,6 +14508,9 @@ packages: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rolldown@1.0.0-rc.15: resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -14128,6 +14533,9 @@ packages: rou3@0.7.12: resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -14138,6 +14546,9 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -14490,6 +14901,9 @@ packages: stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -14672,6 +15086,10 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -14862,6 +15280,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -15343,6 +15765,11 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.7.0 + tinyexec: 1.0.4 + '@ariakit/core@0.4.18': {} '@ariakit/react-core@0.4.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': @@ -16016,10 +16443,14 @@ snapshots: '@better-fetch/fetch@1.1.21': {} + '@braintree/sanitize-url@7.1.2': {} + '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 + '@chevrotain/types@11.1.2': {} + '@csstools/color-helpers@5.1.0': {} '@csstools/color-helpers@6.0.2': {} @@ -16502,6 +16933,14 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.3': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + '@img/colour@1.1.0': optional: true @@ -17030,6 +17469,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@mermaid-js/parser@1.2.0': + dependencies: + '@chevrotain/types': 11.1.2 + '@mswjs/interceptors@0.37.6': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -19769,7 +20212,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@types/cors@2.8.19': dependencies: @@ -19777,28 +20220,121 @@ snapshots: '@types/d3-array@3.2.2': {} + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + '@types/d3-color@3.1.3': {} + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + '@types/d3-ease@3.0.2': {} + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + '@types/d3-interpolate@3.0.4': dependencies: '@types/d3-color': 3.1.3 '@types/d3-path@3.1.1': {} + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + '@types/d3-scale@4.0.9': dependencies: '@types/d3-time': 3.0.4 + '@types/d3-selection@3.0.11': {} + '@types/d3-shape@3.1.8': dependencies: '@types/d3-path': 3.1.1 + '@types/d3-time-format@4.0.3': {} + '@types/d3-time@3.0.4': {} '@types/d3-timer@3.0.2': {} + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -19823,6 +20359,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/geojson@7946.0.16': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -19890,7 +20428,7 @@ snapshots: '@types/mysql@2.15.27': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@types/node@20.19.37': dependencies: @@ -19928,7 +20466,7 @@ snapshots: '@types/pg@8.15.6': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 pg-protocol: 1.13.0 pg-types: 2.2.0 @@ -19964,10 +20502,13 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -20154,6 +20695,11 @@ snapshots: '@uppy/core': 3.13.1 '@uppy/utils': 5.9.0 + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + '@vercel/analytics@1.6.1(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)': optionalDependencies: next: 16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -21146,6 +21692,8 @@ snapshots: commander@4.1.1: {} + commander@7.2.0: {} + commander@8.3.0: {} commondir@1.0.1: {} @@ -21216,6 +21764,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 @@ -21250,22 +21806,107 @@ snapshots: csstype@3.2.3: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.0): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.34.0 + + cytoscape-fcose@2.2.0(cytoscape@3.34.0): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.34.0 + + cytoscape@3.34.0: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + d3-array@3.2.4: dependencies: internmap: 2.0.3 + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + d3-color@3.1.0: {} + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + d3-ease@3.0.1: {} + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + d3-format@3.1.2: {} + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + d3-interpolate@3.0.1: dependencies: d3-color: 3.1.0 + d3-path@1.0.9: {} + d3-path@3.1.0: {} + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + d3-scale@4.0.2: dependencies: d3-array: 3.2.4 @@ -21274,6 +21915,12 @@ snapshots: d3-time: 3.1.0 d3-time-format: 4.1.0 + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + d3-shape@3.2.0: dependencies: d3-path: 3.1.0 @@ -21288,6 +21935,61 @@ snapshots: d3-timer@3.0.1: {} + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -21322,6 +22024,8 @@ snapshots: date-fns@4.1.0: {} + dayjs@1.11.21: {} + debounce-fn@6.0.0: dependencies: mimic-function: 5.0.1 @@ -21386,6 +22090,10 @@ snapshots: defu@6.1.6: {} + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + delayed-stream@1.0.0: {} delegates@1.0.0: @@ -21448,6 +22156,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.11: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -22254,6 +22966,8 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 + hachure-fill@0.5.2: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -22452,6 +23166,8 @@ snapshots: cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 + import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} inflight@1.0.6: @@ -22477,6 +23193,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@1.0.1: {} + internmap@2.0.3: {} is-alphabetical@2.0.1: {} @@ -22855,12 +23573,18 @@ snapshots: dependencies: json-buffer: 3.0.1 + khroma@2.1.0: {} + kind-of@6.0.3: {} kleur@3.0.3: {} kysely@0.28.15: {} + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + leac@0.6.0: {} levn@0.4.1: @@ -22938,6 +23662,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.18.1: {} + lodash.debounce@4.0.8: {} lodash.isequal@4.5.0: {} @@ -23007,6 +23733,8 @@ snapshots: marked@15.0.12: {} + marked@16.4.2: {} + math-intrinsics@1.1.0: {} mathml-to-latex@1.8.0: @@ -23184,6 +23912,30 @@ snapshots: merge-stream@2.0.0: {} + mermaid@11.16.0: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.3 + '@mermaid-js/parser': 1.2.0 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.34.0 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.0) + cytoscape-fcose: 2.2.0(cytoscape@3.34.0) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.21 + dompurify: 3.4.11 + es-toolkit: 1.45.1 + katex: 0.16.47 + khroma: 2.1.0 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.3.0 + uuid: 14.0.1 + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -24152,6 +24904,8 @@ snapshots: package-json-from-dist@1.0.1: {} + package-manager-detector@1.7.0: {} + pako@0.2.9: {} pako@1.0.11: {} @@ -24194,6 +24948,8 @@ snapshots: path-browserify@1.0.1: {} + path-data-parser@0.1.0: {} + path-exists@4.0.0: {} path-expression-matcher@1.2.0: {} @@ -24319,6 +25075,13 @@ snapshots: pngjs@7.0.0: {} + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + possible-typed-array-names@1.1.0: {} postcss-selector-parser@7.1.1: @@ -24955,6 +25718,8 @@ snapshots: dependencies: glob: 10.5.0 + robust-predicates@3.0.3: {} + rolldown@1.0.0-rc.15: dependencies: '@oxc-project/types': 0.124.0 @@ -25015,12 +25780,21 @@ snapshots: rou3@0.7.12: {} + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + rrweb-cssom@0.7.1: {} rrweb-cssom@0.8.0: {} run-applescript@7.1.0: {} + rw@1.3.3: {} + rxjs@7.8.2: dependencies: tslib: 2.8.1 @@ -25471,6 +26245,8 @@ snapshots: stylis@4.2.0: {} + stylis@4.4.0: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -25636,6 +26412,8 @@ snapshots: trough@2.2.0: {} + ts-dedent@2.3.0: {} + ts-interface-checker@0.1.13: {} ts-morph@27.0.2: @@ -25852,6 +26630,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@14.0.1: {} + uuid@9.0.1: {} valibot@1.3.1(typescript@5.9.3): diff --git a/tests/package.json b/tests/package.json index de40568ca3..8271b28a92 100644 --- a/tests/package.json +++ b/tests/package.json @@ -12,6 +12,7 @@ "@blocknote/ariakit": "workspace:^", "@blocknote/core": "workspace:^", "@blocknote/mantine": "workspace:^", + "@blocknote/diagram-block": "workspace:^", "@blocknote/math-block": "workspace:^", "@blocknote/react": "workspace:^", "@blocknote/shadcn": "workspace:^", diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/diagram/basic.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/diagram/basic.html new file mode 100644 index 0000000000..608a912f53 --- /dev/null +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/diagram/basic.html @@ -0,0 +1,55 @@ +
+
+
+
+
+
+
+ +

Add a Mermaid diagram

+
+
+
+
+
+                graph TD
+  A[Start] --> B[End]
+              
+
+ +
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/html/diagram/basic.html b/tests/src/unit/react/formatConversion/export/__snapshots__/html/diagram/basic.html new file mode 100644 index 0000000000..eb3b214fd7 --- /dev/null +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/html/diagram/basic.html @@ -0,0 +1,4 @@ +
+  graph TD
+  A[Start] --> B[End]
+
\ No newline at end of file diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/markdown/diagram/basic.md b/tests/src/unit/react/formatConversion/export/__snapshots__/markdown/diagram/basic.md new file mode 100644 index 0000000000..0c7bf63597 --- /dev/null +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/markdown/diagram/basic.md @@ -0,0 +1,4 @@ +```mermaid +graph TD + A[Start] --> B[End] +``` diff --git a/tests/src/unit/react/formatConversion/export/exportTestInstances.ts b/tests/src/unit/react/formatConversion/export/exportTestInstances.ts index bfd16d765e..30b339b8e3 100644 --- a/tests/src/unit/react/formatConversion/export/exportTestInstances.ts +++ b/tests/src/unit/react/formatConversion/export/exportTestInstances.ts @@ -2,6 +2,7 @@ import { ExportTestCase } from "../../../shared/formatConversion/export/exportTe import { testExportBlockNoteHTML, testExportHTML, + testExportMarkdown, } from "../../../shared/formatConversion/export/exportTestExecutors.js"; import { TestInstance } from "../../../types.js"; import { @@ -501,6 +502,18 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, executeTest: testExportBlockNoteHTML, }, + { + testCase: { + name: "diagram/basic", + content: [ + { + type: "diagram", + content: "graph TD\n A[Start] --> B[End]", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, ]; export const exportTestInstancesHTML: TestInstance< @@ -512,3 +525,25 @@ export const exportTestInstancesHTML: TestInstance< testCase, executeTest: testExportHTML, })); + +// Markdown export runs the external HTML through remark, so the diagram's +// fenced-code representation should come out as a ```mermaid fence. +export const exportTestInstancesMarkdown: TestInstance< + ExportTestCase, + TestBlockSchema, + TestInlineContentSchema, + TestStyleSchema +>[] = [ + { + testCase: { + name: "diagram/basic", + content: [ + { + type: "diagram", + content: "graph TD\n A[Start] --> B[End]", + }, + ], + }, + executeTest: testExportMarkdown, + }, +]; diff --git a/tests/src/unit/react/formatConversion/export/runTests.test.ts b/tests/src/unit/react/formatConversion/export/runTests.test.ts index 514171d525..4a63a80858 100644 --- a/tests/src/unit/react/formatConversion/export/runTests.test.ts +++ b/tests/src/unit/react/formatConversion/export/runTests.test.ts @@ -5,6 +5,7 @@ import { testSchema } from "../../testSchema.js"; import { exportTestInstancesBlockNoteHTML, exportTestInstancesHTML, + exportTestInstancesMarkdown, } from "./exportTestInstances.js"; describe("React export tests (BlockNote HTML)", () => { @@ -26,3 +27,13 @@ describe("React export tests (HTML)", () => { }); } }); + +describe("React export tests (Markdown)", () => { + const getEditor = setupTestEditor(testSchema); + + for (const { testCase, executeTest } of exportTestInstancesMarkdown) { + it(`${testCase.name}`, async () => { + await executeTest(getEditor(), testCase); + }); + } +}); diff --git a/tests/src/unit/react/formatConversion/parse/__snapshots__/html/diagramBlock.json b/tests/src/unit/react/formatConversion/parse/__snapshots__/html/diagramBlock.json new file mode 100644 index 0000000000..4c8f430455 --- /dev/null +++ b/tests/src/unit/react/formatConversion/parse/__snapshots__/html/diagramBlock.json @@ -0,0 +1,16 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "graph TD + A[Start] --> B[End]", + "type": "text", + }, + ], + "id": "1", + "props": {}, + "type": "diagram", + }, +] \ No newline at end of file diff --git a/tests/src/unit/react/formatConversion/parse/__snapshots__/markdown/codeBlockNotDiagram.json b/tests/src/unit/react/formatConversion/parse/__snapshots__/markdown/codeBlockNotDiagram.json new file mode 100644 index 0000000000..d48d7ebb30 --- /dev/null +++ b/tests/src/unit/react/formatConversion/parse/__snapshots__/markdown/codeBlockNotDiagram.json @@ -0,0 +1,17 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "console.log('hi');", + "type": "text", + }, + ], + "id": "1", + "props": { + "language": "javascript", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/react/formatConversion/parse/__snapshots__/markdown/diagramBlock.json b/tests/src/unit/react/formatConversion/parse/__snapshots__/markdown/diagramBlock.json new file mode 100644 index 0000000000..4c8f430455 --- /dev/null +++ b/tests/src/unit/react/formatConversion/parse/__snapshots__/markdown/diagramBlock.json @@ -0,0 +1,16 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "graph TD + A[Start] --> B[End]", + "type": "text", + }, + ], + "id": "1", + "props": {}, + "type": "diagram", + }, +] \ No newline at end of file diff --git a/tests/src/unit/react/formatConversion/parse/parseTestInstances.ts b/tests/src/unit/react/formatConversion/parse/parseTestInstances.ts index e20d60bc13..4cfd6479d0 100644 --- a/tests/src/unit/react/formatConversion/parse/parseTestInstances.ts +++ b/tests/src/unit/react/formatConversion/parse/parseTestInstances.ts @@ -1,5 +1,8 @@ import { ParseTestCase } from "../../../shared/formatConversion/parse/parseTestCase.js"; -import { testParseHTML } from "../../../shared/formatConversion/parse/parseTestExecutors.js"; +import { + testParseHTML, + testParseMarkdown, +} from "../../../shared/formatConversion/parse/parseTestExecutors.js"; import { TestInstance } from "../../../types.js"; import { TestBlockSchema, @@ -25,4 +28,37 @@ export const parseTestInstancesHTML: TestInstance< }, executeTest: testParseHTML, }, + { + testCase: { + name: "diagramBlock", + content: `
graph TD\n  A[Start] --> B[End]
`, + }, + executeTest: testParseHTML, + }, +]; + +// The diagram block's fenced-code representation should also round-trip from +// Markdown. +export const parseTestInstancesMarkdown: TestInstance< + ParseTestCase, + TestBlockSchema, + TestInlineContentSchema, + TestStyleSchema +>[] = [ + { + testCase: { + name: "diagramBlock", + content: "```mermaid\ngraph TD\n A[Start] --> B[End]\n```", + }, + executeTest: testParseMarkdown, + }, + // The reverse precedence check: with the diagram block registered, fences + // in other languages must still fall through to the code block. + { + testCase: { + name: "codeBlockNotDiagram", + content: "```javascript\nconsole.log('hi');\n```", + }, + executeTest: testParseMarkdown, + }, ]; diff --git a/tests/src/unit/react/formatConversion/parse/runTests.test.ts b/tests/src/unit/react/formatConversion/parse/runTests.test.ts index edafbafb74..7f13214bea 100644 --- a/tests/src/unit/react/formatConversion/parse/runTests.test.ts +++ b/tests/src/unit/react/formatConversion/parse/runTests.test.ts @@ -2,7 +2,10 @@ import { describe, it } from "vite-plus/test"; import { setupTestEditor } from "../../setupTestEditor.js"; import { testSchema } from "../../testSchema.js"; -import { parseTestInstancesHTML } from "./parseTestInstances.js"; +import { + parseTestInstancesHTML, + parseTestInstancesMarkdown, +} from "./parseTestInstances.js"; // Tests for verifying that the React math block and inline math implementations // are correctly parsed from external HTML (MathML). @@ -15,3 +18,13 @@ describe("React parse tests (HTML)", () => { }); } }); + +describe("React parse tests (Markdown)", () => { + const getEditor = setupTestEditor(testSchema); + + for (const { testCase, executeTest } of parseTestInstancesMarkdown) { + it(`${testCase.name}`, async () => { + await executeTest(getEditor(), testCase); + }); + } +}); diff --git a/tests/src/unit/react/testSchema.tsx b/tests/src/unit/react/testSchema.tsx index a18b4bd48b..8f696d1fd3 100644 --- a/tests/src/unit/react/testSchema.tsx +++ b/tests/src/unit/react/testSchema.tsx @@ -3,6 +3,7 @@ import { createPageBreakBlockSpec, defaultProps, } from "@blocknote/core"; +import { createReactDiagramBlockSpec } from "@blocknote/diagram-block"; import { createReactInlineMathSpec, createReactMathBlockSpec, @@ -173,6 +174,7 @@ export const testSchema = BlockNoteSchema.create().extend({ simpleCustomParagraph: createSimpleCustomParagraph(), contextParagraph: createContextParagraph(), math: createReactMathBlockSpec(), + diagram: createReactDiagramBlockSpec(), }, inlineContentSpecs: { mention: Mention, diff --git a/tests/vite.config.ts b/tests/vite.config.ts index b84629c147..b5f74c0135 100644 --- a/tests/vite.config.ts +++ b/tests/vite.config.ts @@ -57,6 +57,10 @@ export default defineConfig( __dirname, "../packages/math-block/src/", ), + "@blocknote/diagram-block": path.resolve( + __dirname, + "../packages/diagram-block/src/", + ), "@blocknote/server-util": path.resolve( __dirname, "../packages/server-util/src/",