Repository: TypeCellOS/BlockNote
Pull request #1490: feat: markdown pasting & custom paste handlers
Author: login: nperez0111
Created: 2025-03-04T20:49:41Z | Merged: 2025-03-31T14:52:01Z

This implements markdown pasting, and custom paste handlers.

Adds

 - `editor.pasteText` which will paste text content into the editor
 - `editor.pasteHTML` which will convert HTML into BlockNote HTML and then paste that into the editor
 - `editor.pasteMarkdown` which will convert the markdown into BlockNote HTML and then paste that into the editor
 - `editorOptions.pasteHandler` for allowing the developer the ability to customize how content is pasted into the editor while still allowing the default paste behavior.


Diff:
diff --git a/docs/pages/docs/advanced/paste-handling.mdx b/docs/pages/docs/advanced/paste-handling.mdx
new file mode 100644
index 0000000000..de3293a02f
--- /dev/null
+++ b/docs/pages/docs/advanced/paste-handling.mdx
@@ -0,0 +1,74 @@
+---
+title: Paste Handling
+description: This section explains how to handle paste events in BlockNote.
+imageTitle: Paste Handling
+---
+
+import { Example } from "@/components/example";
+
+# Paste Handling
+
+BlockNote, by default, attempts to paste content in the following order:
+
+-   VS Code compatible content
+-   Files
+-   BlockNote HTML
+-   Markdown
+-   HTML
+-   Plain text
+
+> In certain cases, BlockNote will attempt to detect markdown in the clipboard and paste that into the editor as rich text.
+
+You can change the default paste behavior by providing a custom paste handler, which will give you full control over how pasted content is inserted into the editor.
+
+## `pasteHandler` option
+
+The `pasteHandler` option is a function that receives the following arguments:
+
+```ts
+type PasteHandler = (context: {
+    event: ClipboardEvent;
+    editor: BlockNoteEditor;
+    defaultPasteHandler: (context?: {
+        prioritizeMarkdownOverHTML?: boolean;
+        plainTextAsMarkdown?: boolean;
+    }) => boolean;
+}) => boolean;
+```
+
+-   `event`: The paste event.
+-   `editor`: The current editor instance.
+-   `defaultPasteHandler`: The default paste handler. If you only need to customize the paste behavior a little bit, you can fall back on the default paste handler.
+
+The `defaultPasteHandler` function can be called with the following options:
+
+-   `prioritizeMarkdownOverHTML`: Whether to prioritize Markdown content in `text/plain` over `text/html` when pasting from the clipboard.
+-   `plainTextAsMarkdown`: Whether to interpret plain text as markdown and paste that as rich text or to paste the text directly into the editor.
+
+
+## Custom Paste Handler
+
+You can also provide your own paste handler by providing a function to the `pasteHandler` option.
+
+In this example, we handle the paste event if the clipboard data contains `text/my-custom-format`. If we don't handle the paste event, we call the default paste handler to do the default behavior.
+
+```ts
+const editor = new BlockNoteEditor({
+  pasteHandler: ({ event, editor, defaultPasteHandler }) => {
+    if (event.clipboardData?.types.includes("text/my-custom-format")) {
+      // You can do any custom logic here, for example you could transform the clipboard data before pasting it
+      const markdown = customToMarkdown(event.clipboardData.getData("text/my-custom-format"));
+      
+      // The editor is able paste markdown (`pasteMarkdown`), HTML (`pasteHTML`), or plain text (`pasteText`)
+      editor.pasteMarkdown(markdown);
+      // We handled the paste event, so return true, returning false will cancel the paste event
+      return true;
+    }
+
+    // If we didn't handle the paste event, call the default paste handler to do the default behavior
+    return defaultPasteHandler();
+  },
+});
+```
+
+See an example of this in the [Custom Paste Handler](/examples/basic/custom-paste-handler) example.
\ No newline at end of file
diff --git a/docs/pages/docs/editor-basics/setup.mdx b/docs/pages/docs/editor-basics/setup.mdx
index 5c76f60ea4..fbb8795f89 100644
--- a/docs/pages/docs/editor-basics/setup.mdx
+++ b/docs/pages/docs/editor-basics/setup.mdx
@@ -33,6 +33,13 @@ type BlockNoteEditorOptions = {
     class?: string;
   }) => Plugin;
   initialContent?: PartialBlock[];
+  pasteHandler?: (context: {
+    event: ClipboardEvent;
+    editor: BlockNoteEditor;
+    defaultPasteHandler: (context: {
+      pasteBehavior?: "prefer-markdown" | "prefer-html";
+    }) => boolean | undefined;
+  }) => boolean | undefined;
   resolveFileUrl: (url: string) => Promise<string>
   schema?: BlockNoteSchema;
   setIdAttribute?: boolean;
@@ -66,6 +73,8 @@ The hook takes two optional parameters:
 
 `initialContent:` The content that should be in the editor when it's created, represented as an array of [Partial Blocks](/docs/manipulating-blocks#partial-blocks).
 
+`pasteHandler`: A function that can be used to override the default paste behavior. See [Paste Handling](/docs/advanced/paste-handling) for more.
+
 `resolveFileUrl:` Function to resolve file URLs for display/download. Useful for creating authenticated URLs or implementing custom protocols.
 
 `resolveUsers`: Function to resolve user information for comments. See [Comments](/docs/collaboration/comments) for more.
diff --git a/examples/01-basic/13-custom-paste-handler/.bnexample.json b/examples/01-basic/13-custom-paste-handler/.bnexample.json
new file mode 100644
index 0000000000..5778b19ace
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/.bnexample.json
@@ -0,0 +1,6 @@
+{
+  "playground": true,
+  "docs": true,
+  "author": "nperez0111",
+  "tags": ["Basic"]
+}
diff --git a/examples/01-basic/13-custom-paste-handler/App.tsx b/examples/01-basic/13-custom-paste-handler/App.tsx
new file mode 100644
index 0000000000..7adafbc635
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/App.tsx
@@ -0,0 +1,105 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+import "./styles.css";
+
+export default function App() {
+  // Creates a new editor instance.
+  const editor = useCreateBlockNote({
+    initialContent: [
+      {
+        type: "paragraph",
+        content: [
+          {
+            styles: {},
+            type: "text",
+            text: "Paste some text here",
+          },
+        ],
+      },
+    ],
+    pasteHandler: ({ event, editor, defaultPasteHandler }) => {
+      if (event.clipboardData?.types.includes("text/plain")) {
+        editor.pasteMarkdown(
+          event.clipboardData.getData("text/plain") +
+            " - inserted by the custom paste handler"
+        );
+        return true;
+      }
+      return defaultPasteHandler();
+    },
+  });
+
+  // Renders the editor instance using a React component.
+  return (
+    <div>
+      <BlockNoteView editor={editor} />
+      <div className={"edit-buttons"}>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.writeText(
+                "**This is markdown in the plain text format**"
+              );
+            } catch (error) {
+              window.alert("Failed to copy plain text with markdown content");
+            }
+          }}>
+          Copy sample markdown to clipboard (text/plain)
+        </button>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.write([
+                new ClipboardItem({
+                  "text/html": "<p><strong>HTML</strong></p>",
+                }),
+              ]);
+            } catch (error) {
+              window.alert("Failed to copy HTML content");
+            }
+          }}>
+          Copy sample HTML to clipboard (text/html)
+        </button>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.writeText(
+                "This is plain text in the plain text format"
+              );
+            } catch (error) {
+              window.alert("Failed to copy plain text");
+            }
+          }}>
+          Copy sample plain text to clipboard (text/plain)
+        </button>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.write([
+                new ClipboardItem({
+                  "text/plain": "Plain text",
+                }),
+                new ClipboardItem({
+                  "text/html": "<p><strong>HTML</strong></p>",
+                }),
+                new ClipboardItem({
+                  "text/markdown": "**Markdown**",
+                }),
+              ]);
+            } catch (error) {
+              window.alert("Failed to copy multiple formats");
+            }
+          }}>
+          Copy sample markdown, HTML, and plain text to clipboard (Safari only)
+        </button>
+      </div>
+    </div>
+  );
+}
diff --git a/examples/01-basic/13-custom-paste-handler/README.md b/examples/01-basic/13-custom-paste-handler/README.md
new file mode 100644
index 0000000000..189248704e
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/README.md
@@ -0,0 +1,9 @@
+# Custom Paste Handler
+
+In this example, we change the default paste handler to append some text to the pasted content when the content is plain text.
+
+**Try it out:** Use the buttons to copy some content to the clipboard and paste it in the editor to trigger our custom paste handler.
+
+**Relevant Docs:**
+
+- [Paste Handling](/docs/advanced/paste-handling)
diff --git a/examples/01-basic/13-custom-paste-handler/index.html b/examples/01-basic/13-custom-paste-handler/index.html
new file mode 100644
index 0000000000..9b469194a1
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/index.html
@@ -0,0 +1,14 @@
+<html lang="en">
+  <head>
+    <script>
+      <!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
+    </script>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Custom Paste Handler</title>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script type="module" src="./main.tsx"></script>
+  </body>
+</html>
diff --git a/examples/01-basic/13-custom-paste-handler/main.tsx b/examples/01-basic/13-custom-paste-handler/main.tsx
new file mode 100644
index 0000000000..f88b490fbd
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/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 "./App";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>
+);
diff --git a/examples/01-basic/13-custom-paste-handler/package.json b/examples/01-basic/13-custom-paste-handler/package.json
new file mode 100644
index 0000000000..4c06198cde
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/package.json
@@ -0,0 +1,37 @@
+{
+  "name": "@blocknote/example-custom-paste-handler",
+  "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+  "private": true,
+  "version": "0.12.4",
+  "scripts": {
+    "start": "vite",
+    "dev": "vite",
+    "build": "tsc && vite build",
+    "preview": "vite preview",
+    "lint": "eslint . --max-warnings 0"
+  },
+  "dependencies": {
+    "@blocknote/core": "latest",
+    "@blocknote/react": "latest",
+    "@blocknote/ariakit": "latest",
+    "@blocknote/mantine": "latest",
+    "@blocknote/shadcn": "latest",
+    "react": "^18.3.1",
+    "react-dom": "^18.3.1"
+  },
+  "devDependencies": {
+    "@types/react": "^18.0.25",
+    "@types/react-dom": "^18.0.9",
+    "@vitejs/plugin-react": "^4.3.1",
+    "eslint": "^8.10.0",
+    "vite": "^5.3.4"
+  },
+  "eslintConfig": {
+    "extends": [
+      "../../../.eslintrc.js"
+    ]
+  },
+  "eslintIgnore": [
+    "dist"
+  ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/13-custom-paste-handler/styles.css b/examples/01-basic/13-custom-paste-handler/styles.css
new file mode 100644
index 0000000000..eaade15302
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/styles.css
@@ -0,0 +1,15 @@
+.edit-buttons {
+    display: flex;
+    justify-content: space-between;
+    margin-top: 8px;
+}
+
+.edit-button {
+    border: 1px solid gray;
+    border-radius: 4px;
+    padding-inline: 4px;
+}
+
+.edit-button:hover {
+    border: 1px solid lightgrey;
+}
\ No newline at end of file
diff --git a/examples/01-basic/13-custom-paste-handler/tsconfig.json b/examples/01-basic/13-custom-paste-handler/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/tsconfig.json
@@ -0,0 +1,36 @@
+{
+  "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+  "compilerOptions": {
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "lib": [
+      "DOM",
+      "DOM.Iterable",
+      "ESNext"
+    ],
+    "allowJs": false,
+    "skipLibCheck": true,
+    "esModuleInterop": false,
+    "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/"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/13-custom-paste-handler/vite.config.ts b/examples/01-basic/13-custom-paste-handler/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/vite.config.ts
@@ -0,0 +1,32 @@
+// 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";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+  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),
+  },
+}));
diff --git a/packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts b/packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts
index eabefb3c49..c79400c78a 100644
--- a/packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts
+++ b/packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts
@@ -1,6 +1,7 @@
 export const acceptedMIMETypes = [
   "vscode-editor-data",
   "blocknote/html",
+  "text/markdown",
   "text/html",
   "text/plain",
   "Files",
diff --git a/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts b/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
index 350cf2dad8..ecc17120ee 100644
--- a/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
+++ b/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
@@ -1,23 +1,100 @@
 import { Extension } from "@tiptap/core";
 import { Plugin } from "prosemirror-state";
 
-import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
+import type {
+  BlockNoteEditor,
+  BlockNoteEditorOptions,
+} from "../../../editor/BlockNoteEditor";
 import {
   BlockSchema,
   InlineContentSchema,
   StyleSchema,
 } from "../../../schema/index.js";
-import { nestedListsToBlockNoteStructure } from "../../parsers/html/util/nestedLists.js";
 import { acceptedMIMETypes } from "./acceptedMIMETypes.js";
 import { handleFileInsertion } from "./handleFileInsertion.js";
 import { handleVSCodePaste } from "./handleVSCodePaste.js";
+import { isMarkdown } from "../../parsers/markdown/detectMarkdown.js";
+
+function defaultPasteHandler({
+  event,
+  editor,
+  prioritizeMarkdownOverHTML,
+  plainTextAsMarkdown,
+}: {
+  event: ClipboardEvent;
+  editor: BlockNoteEditor<any, any, any>;
+  prioritizeMarkdownOverHTML: boolean;
+  plainTextAsMarkdown: boolean;
+}) {
+  let format: (typeof acceptedMIMETypes)[number] | undefined;
+  for (const mimeType of acceptedMIMETypes) {
+    if (event.clipboardData!.types.includes(mimeType)) {
+      format = mimeType;
+      break;
+    }
+  }
+
+  if (!format) {
+    return true;
+  }
+
+  if (format === "vscode-editor-data") {
+    handleVSCodePaste(event, editor.prosemirrorView!);
+    return true;
+  }
+
+  if (format === "Files") {
+    handleFileInsertion(event, editor);
+    return true;
+  }
+
+  const data = event.clipboardData!.getData(format);
+
+  if (format === "blocknote/html") {
+    // Is blocknote/html, so no need to convert it
+    editor.pasteHTML(data, true);
+    return true;
+  }
+
+  if (format === "text/markdown") {
+    editor.pasteMarkdown(data);
+    return true;
+  }
+
+  if (prioritizeMarkdownOverHTML) {
+    // Use plain text instead of HTML if it looks like Markdown
+    const plainText = event.clipboardData!.getData("text/plain");
+
+    if (isMarkdown(plainText)) {
+      editor.pasteMarkdown(plainText);
+      return true;
+    }
+  }
+
+  if (format === "text/html") {
+    editor.pasteHTML(data);
+    return true;
+  }
+
+  if (plainTextAsMarkdown) {
+    editor.pasteMarkdown(data);
+    return true;
+  }
+
+  editor.pasteText(data);
+  return true;
+}
 
 export const createPasteFromClipboardExtension = <
   BSchema extends BlockSchema,
   I extends InlineContentSchema,
   S extends StyleSchema
 >(
-  editor: BlockNoteEditor<BSchema, I, S>
+  editor: BlockNoteEditor<BSchema, I, S>,
+  pasteHandler: Exclude<
+    BlockNoteEditorOptions<any, any, any>["pasteHandler"],
+    undefined
+  >
 ) =>
   Extension.create({
     name: "pasteFromClipboard",
@@ -26,51 +103,28 @@ export const createPasteFromClipboardExtension = <
         new Plugin({
           props: {
             handleDOMEvents: {
-              paste(view, event) {
+              paste(_view, event) {
                 event.preventDefault();
 
                 if (!editor.isEditable) {
                   return;
                 }
 
-                let format: (typeof acceptedMIMETypes)[number] | undefined;
-                for (const mimeType of acceptedMIMETypes) {
-                  if (event.clipboardData!.types.includes(mimeType)) {
-                    format = mimeType;
-                    break;
-                  }
-                }
-                if (!format) {
-                  return true;
-                }
-
-                if (format === "vscode-editor-data") {
-                  handleVSCodePaste(event, view);
-                  return true;
-                }
-
-                if (format === "Files") {
-                  handleFileInsertion(event, editor);
-                  return true;
-                }
-
-                let data = event.clipboardData!.getData(format);
-
-                if (format === "blocknote/html") {
-                  view.pasteHTML(data);
-                  return true;
-                }
-
-                if (format === "text/html") {
-                  const htmlNode = nestedListsToBlockNoteStructure(data.trim());
-                  data = htmlNode.innerHTML;
-                  view.pasteHTML(data);
-                  return true;
-                }
-
-                view.pasteText(data);
-
-                return true;
+                return pasteHandler({
+                  event,
+                  editor,
+                  defaultPasteHandler: ({
+                    prioritizeMarkdownOverHTML = true,
+                    plainTextAsMarkdown = true,
+                  } = {}) => {
+                    return defaultPasteHandler({
+                      event,
+                      editor,
+                      prioritizeMarkdownOverHTML,
+                      plainTextAsMarkdown,
+                    });
+                  },
+                });
               },
             },
           },
diff --git a/packages/core/src/api/parsers/markdown/detectMarkdown.ts b/packages/core/src/api/parsers/markdown/detectMarkdown.ts
new file mode 100644
index 0000000000..92734face4
--- /dev/null
+++ b/packages/core/src/api/parsers/markdown/detectMarkdown.ts
@@ -0,0 +1,60 @@
+// Headings H1-H6.
+const h1 = /(^|\n) {0,3}#{1,6} {1,8}[^\n]{1,64}\r?\n\r?\n\s{0,32}\S/;
+
+// Bold, italic, underline, strikethrough, highlight.
+const bold = /(?:\s|^)(_|__|\*|\*\*|~~|==|\+\+)(?!\s).{1,64}(?<!\s)(?=\1)/;
+
+// Basic inline link (also captures images).
+const link = /\[[^\]]{1,128}\]\(https?:\/\/\S{1,999}\)/;
+
+// Inline code.
+const code = /(?:\s|^)`(?!\s)[^`]{1,48}(?<!\s)`([^\w]|$)/;
+
+// Unordered list.
+const ul = /(?:^|\n)\s{0,5}-\s{1}[^\n]+\n\s{0,15}-\s/;
+
+// Ordered list.
+const ol = /(?:^|\n)\s{0,5}\d+\.\s{1}[^\n]+\n\s{0,15}\d+\.\s/;
+
+// Horizontal rule.
+const hr = /\n{2} {0,3}-{2,48}\n{2}/;
+
+// Fenced code block.
+const fences =
+  /(?:\n|^)(```|~~~|\$\$)(?!`|~)[^\s]{0,64} {0,64}[^\n]{0,64}\n[\s\S]{0,9999}?\s*\1 {0,64}(?:\n+|$)/;
+
+// Classical underlined H1 and H2 headings.
+const title = /(?:\n|^)(?!\s)\w[^\n]{0,64}\r?\n(-|=)\1{0,64}\n\n\s{0,64}(\w|$)/;
+
+// Blockquote.
+const blockquote =
+  /(?:^|(\r?\n\r?\n))( {0,3}>[^\n]{1,333}\n){1,999}($|(\r?\n))/;
+
+// Table Header
+const tableHeader = /^\s*\|(.+\|)+\s*$/m;
+
+// Table Divider
+const tableDivider = /^\s*\|(\s*[-:]+[-:]\s*\|)+\s*$/m;
+
+// Table Row
+const tableRow = /^\s*\|(.+\|)+\s*$/m;
+
+/**
+ * Returns `true` if the source text might be a markdown document.
+ *
+ * @param src Source text to analyze.
+ */
+export const isMarkdown = (src: string): boolean =>
+  h1.test(src) ||
+  bold.test(src) ||
+  link.test(src) ||
+  code.test(src) ||
+  ul.test(src) ||
+  ol.test(src) ||
+  hr.test(src) ||
+  fences.test(src) ||
+  title.test(src) ||
+  blockquote.test(src) ||
+  tableHeader.test(src) ||
+  tableDivider.test(src) ||
+  tableRow.test(src);
diff --git a/packages/core/src/api/parsers/markdown/parseMarkdown.ts b/packages/core/src/api/parsers/markdown/parseMarkdown.ts
index 2d6b81873e..1c348d8ffe 100644
--- a/packages/core/src/api/parsers/markdown/parseMarkdown.ts
+++ b/packages/core/src/api/parsers/markdown/parseMarkdown.ts
@@ -48,17 +48,7 @@ function code(state: any, node: any) {
   return result;
 }
 
-export async function markdownToBlocks<
-  BSchema extends BlockSchema,
-  I extends InlineContentSchema,
-  S extends StyleSchema
->(
-  markdown: string,
-  blockSchema: BSchema,
-  icSchema: I,
-  styleSchema: S,
-  pmSchema: Schema
-): Promise<Block<BSchema, I, S>[]> {
+export async function markdownToHTML(markdown: string): Promise<string> {
   const deps = await initializeESMDependencies();
 
   const htmlString = deps.unified
@@ -74,11 +64,21 @@ export async function markdownToBlocks<
     .use(deps.rehypeStringify.default)
     .processSync(markdown);
 
-  return HTMLToBlocks(
-    htmlString.value as string,
-    blockSchema,
-    icSchema,
-    styleSchema,
-    pmSchema
-  );
+  return htmlString.value as string;
+}
+
+export async function markdownToBlocks<
+  BSchema extends BlockSchema,
+  I extends InlineContentSchema,
+  S extends StyleSchema
+>(
+  markdown: string,
+  blockSchema: BSchema,
+  icSchema: I,
+  styleSchema: S,
+  pmSchema: Schema
+): Promise<Block<BSchema, I, S>[]> {
+  const htmlString = await markdownToHTML(markdown);
+
+  return HTMLToBlocks(htmlString, blockSchema, icSchema, styleSchema, pmSchema);
 }
diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts
index c0e9185629..b1caec71b0 100644
--- a/packages/core/src/editor/BlockNoteEditor.ts
+++ b/packages/core/src/editor/BlockNoteEditor.ts
@@ -43,7 +43,10 @@ import {
 import { createExternalHTMLExporter } from "../api/exporters/html/externalHTMLExporter.js";
 import { blocksToMarkdown } from "../api/exporters/markdown/markdownExporter.js";
 import { HTMLToBlocks } from "../api/parsers/html/parseHTML.js";
-import { markdownToBlocks } from "../api/parsers/markdown/parseMarkdown.js";
+import {
+  markdownToBlocks,
+  markdownToHTML,
+} from "../api/parsers/markdown/parseMarkdown.js";
 import {
   Block,
   DefaultBlockSchema,
@@ -102,6 +105,7 @@ import type { ThreadStore, User } from "../comments/index.js";
 import "../style.css";
 import { EventEmitter } from "../util/EventEmitter.js";
 import { CodeBlockOptions } from "../blocks/CodeBlockContent/CodeBlockContent.js";
+import { nestedListsToBlockNoteStructure } from "../api/parsers/html/util/nestedLists.js";
 
 export type BlockNoteExtensionFactory = (
   editor: BlockNoteEditor<any, any, any>
@@ -218,6 +222,39 @@ export type BlockNoteEditorOptions<
     string | undefined
   >;
 
+  /**
+   * Custom paste handler that can be used to override the default paste behavior.
+   * @returns The function should return `true` if the paste event was handled, otherwise it should return `false` if it should be canceled or `undefined` if it should be handled by another handler.
+   *
+   * @example
+   * ```ts
+   * pasteHandler: ({ defaultPasteHandler }) => {
+   *   return defaultPasteHandler({ pasteBehavior: "prefer-html" });
+   * }
+   * ```
+   */
+  pasteHandler?: (context: {
+    event: ClipboardEvent;
+    editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
+    /**
+     * The default paste handler
+     * @param context The context object
+     * @returns Whether the paste event was handled or not
+     */
+    defaultPasteHandler: (context?: {
+      /**
+       * Whether to prioritize Markdown content in `text/plain` over `text/html` when pasting from the clipboard.
+       * @default true
+       */
+      prioritizeMarkdownOverHTML?: boolean;
+      /**
+       * Whether to parse `text/plain` content from the clipboard as Markdown content.
+       * @default true
+       */
+      plainTextAsMarkdown?: boolean;
+    }) => boolean | undefined;
+  }) => boolean | undefined;
+
   /**
    * Resolve a URL of a file block to one that can be displayed or downloaded. This can be used for creating authenticated URL or
    * implementing custom protocols / schemes
@@ -545,6 +582,7 @@ export class BlockNoteEditor<
       tabBehavior: newOptions.tabBehavior,
       sideMenuDetection: newOptions.sideMenuDetection || "viewport",
       comments: newOptions.comments,
+      pasteHandler: newOptions.pasteHandler,
     });
 
     // add extensions from _tiptapOptions
@@ -1445,4 +1483,41 @@ export class BlockNoteEditor<
   public setForceSelectionVisible(forceSelectionVisible: boolean) {
     this.showSelectionPlugin.setEnabled(forceSelectionVisible);
   }
+
+  /**
+   * This will convert HTML into a format that is compatible with BlockNote.
+   */
+  private convertHtmlToBlockNoteHtml(html: string) {
+    const htmlNode = nestedListsToBlockNoteStructure(html.trim());
+    return htmlNode.innerHTML;
+  }
+
+  /**
+   * Paste HTML into the editor. Defaults to converting HTML to BlockNote HTML.
+   * @param html The HTML to paste.
+   * @param raw Whether to paste the HTML as is, or to convert it to BlockNote HTML.
+   */
+  public pasteHTML(html: string, raw = false) {
+    let htmlToPaste = html;
+    if (!raw) {
+      htmlToPaste = this.convertHtmlToBlockNoteHtml(html);
+    }
+    this.prosemirrorView?.pasteHTML(htmlToPaste);
+  }
+
+  /**
+   * Paste text into the editor. Defaults to interpreting text as markdown.
+   * @param text The text to paste.
+   */
+  public pasteText(text: string) {
+    return this.prosemirrorView?.pasteText(text);
+  }
+
+  /**
+   * Paste markdown into the editor.
+   * @param markdown The markdown to paste.
+   */
+  public async pasteMarkdown(markdown: string) {
+    return this.pasteHTML(await markdownToHTML(markdown));
+  }
 }
diff --git a/packages/core/src/editor/BlockNoteExtensions.ts b/packages/core/src/editor/BlockNoteExtensions.ts
index a5233f11f6..932ca91287 100644
--- a/packages/core/src/editor/BlockNoteExtensions.ts
+++ b/packages/core/src/editor/BlockNoteExtensions.ts
@@ -44,7 +44,11 @@ import {
   StyleSchema,
   StyleSpecs,
 } from "../schema/index.js";
-import type { BlockNoteEditor, BlockNoteExtension } from "./BlockNoteEditor.js";
+import type {
+  BlockNoteEditor,
+  BlockNoteEditorOptions,
+  BlockNoteExtension,
+} from "./BlockNoteEditor.js";
 
 type ExtensionOptions<
   BSchema extends BlockSchema,
@@ -82,6 +86,7 @@ type ExtensionOptions<
   comments?: {
     threadStore: ThreadStore;
   };
+  pasteHandler: BlockNoteEditorOptions<any, any, any>["pasteHandler"];
 };
 
 /**
@@ -258,7 +263,16 @@ const getTipTapExtensions = <
       ];
     }),
     createCopyToClipboardExtension(opts.editor),
-    createPasteFromClipboardExtension(opts.editor),
+    createPasteFromClipboardExtension(
+      opts.editor,
+      opts.pasteHandler ||
+        ((context: {
+          defaultPasteHandler: (context?: {
+            prioritizeMarkdownOverHTML?: boolean;
+            plainTextAsMarkdown?: boolean;
+          }) => boolean | undefined;
+        }) => context.defaultPasteHandler())
+    ),
     createDropFileExtension(opts.editor),
 
     // This needs to be at the bottom of this list, because Key events (such as enter, when selecting a /command),
diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx
index 5d1fc918c2..a793470da8 100644
--- a/playground/src/examples.gen.tsx
+++ b/playground/src/examples.gen.tsx
@@ -232,6 +232,24 @@
           "slug": "basic"
         }
       },
+      {
+        "projectSlug": "custom-paste-handler",
+        "fullSlug": "basic/custom-paste-handler",
+        "pathFromRoot": "examples/01-basic/13-custom-paste-handler",
+        "config": {
+          "playground": true,
+          "docs": true,
+          "author": "nperez0111",
+          "tags": [
+            "Basic"
+          ]
+        },
+        "title": "Custom Paste Handler",
+        "group": {
+          "pathFromRoot": "examples/01-basic",
+          "slug": "basic"
+        }
+      },
       {
         "projectSlug": "testing",
         "fullSlug": "basic/testing",


Reviews:
user: login: YousefED
body: Great stuff!

What do you think about making this pluggable? i.e.: you could register a paste handler / preprocessor, and our "default preprocessor" changes the mimetype to `text/markdown` if we think it's a better fit.


(just realized; we actually also have a client requesting `Event listeners for "paste" events so we can do our own manipulation before inserting into the document`)
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T06:30:20Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T03:24:27Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T03:31:22Z
user: login: YousefED
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T10:29:12Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T14:28:31Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T14:29:32Z
user: login: YousefED
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T13:01:18Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T14:34:57Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T14:46:41Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T14:50:14Z
user: login: matthewlipski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T16:44:22Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T16:54:18Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T16:58:02Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:00:18Z
user: login: nperez0111
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:01:34Z
user: login: matthewlipski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T17:03:16Z

Review comments:
diff_hunk: @@ -61,15 +64,37 @@ export const createPasteFromClipboardExtension = <
                   return true;
                 }
 
+                if (format === "text/markdown") {
+                  markdownToHTML(data).then((html) => {
+                    view.pasteHTML(html);
+                  });
+                  return true;
+                }
+
                 if (format === "text/html") {
+                  if (editor.settings.pasteBehavior === "prefer-markdown") {
+                    // Use plain text instead of HTML if it looks like Markdown
+                    const plainText =
+                      event.clipboardData!.getData("text/plain");
+
+                    if (is(plainText)) {
path: packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
user: login: YousefED
body: better name for `is` 😅?
created_at: 2025-03-12T06:15:41Z
author_association: COLLABORATOR
diff_hunk: @@ -61,15 +64,37 @@ export const createPasteFromClipboardExtension = <
                   return true;
                 }
 
+                if (format === "text/markdown") {
+                  markdownToHTML(data).then((html) => {
+                    view.pasteHTML(html);
+                  });
+                  return true;
+                }
+
                 if (format === "text/html") {
+                  if (editor.settings.pasteBehavior === "prefer-markdown") {
+                    // Use plain text instead of HTML if it looks like Markdown
+                    const plainText =
+                      event.clipboardData!.getData("text/plain");
+
+                    if (is(plainText)) {
+                      // Convert Markdown to HTML first, then paste as HTML
+                      markdownToHTML(plainText).then((html) => {
+                        view.pasteHTML(html);
+                      });
+                      return true;
+                    }
+                  }
                   const htmlNode = nestedListsToBlockNoteStructure(data.trim());
                   data = htmlNode.innerHTML;
                   view.pasteHTML(data);
                   return true;
                 }
 
-                view.pasteText(data);
-
+                // Convert Markdown to HTML first, then paste as HTML
+                markdownToHTML(data).then((html) => {
path: packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
user: login: YousefED
body: should this be behind a flag as well? (paste as text vs markdown)
created_at: 2025-03-12T06:20:55Z
author_association: COLLABORATOR
diff_hunk: @@ -61,15 +64,37 @@ export const createPasteFromClipboardExtension = <
                   return true;
                 }
 
+                if (format === "text/markdown") {
+                  markdownToHTML(data).then((html) => {
+                    view.pasteHTML(html);
+                  });
+                  return true;
+                }
+
                 if (format === "text/html") {
+                  if (editor.settings.pasteBehavior === "prefer-markdown") {
+                    // Use plain text instead of HTML if it looks like Markdown
+                    const plainText =
+                      event.clipboardData!.getData("text/plain");
+
+                    if (is(plainText)) {
+                      // Convert Markdown to HTML first, then paste as HTML
+                      markdownToHTML(plainText).then((html) => {
+                        view.pasteHTML(html);
+                      });
+                      return true;
+                    }
+                  }
                   const htmlNode = nestedListsToBlockNoteStructure(data.trim());
path: packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
user: login: YousefED
body: Might be worth checking whether we need this for the other cases as well (`nestedListsToBlockNoteStructure`)
created_at: 2025-03-12T06:21:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,60 @@
+// Headings H1-H6.
path: packages/core/src/api/parsers/markdown/is.ts
user: login: YousefED
body: curious how many false positives we get. I didn't review the regexpes obviously.

Figured this might be useful: https://chatgpt.com/share/67d12910-3d78-8009-8bc0-ddd23dd7cba9
created_at: 2025-03-12T06:26:58Z
author_association: COLLABORATOR
diff_hunk: @@ -66,6 +76,8 @@ The hook takes two optional parameters:
 
 `initialContent:` The content that should be in the editor when it's created, represented as an array of [Partial Blocks](/docs/manipulating-blocks#partial-blocks).
 
+`pasteHandler`: A function that can be used to override the default paste behavior. See [Custom Paste Behavior](/docs/advanced/custom-paste-behavior) for more.
path: docs/pages/docs/editor-basics/setup.mdx
user: login: nperez0111
body: Want to add a demo
created_at: 2025-03-14T03:24:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -212,6 +212,51 @@ export type BlockNoteEditorOptions<
     string | undefined
   >;
 
+  /**
+   * Custom paste handler that can be used to override the default paste behavior.
+   * @returns The function should return `true` if the paste event was handled, otherwise it should return `false` if it should be canceled or `undefined` if it should be handled by another handler.
+   *
+   * @example
+   * ```ts
+   * pasteHandler: ({ defaultPasteHandler }) => {
+   *   return defaultPasteHandler({ pasteBehavior: "prefer-html" });
+   * }
+   * ```
+   */
+  pasteHandler?: (context: {
path: packages/core/src/editor/BlockNoteEditor.ts
user: login: nperez0111
body: I also was considering making this into an onPaste, and expose it on the blocknoteview, but it would make it a lot harder to implement than the existing onChange stuff, because you expect there to only be one entity which is handling a paste.

I started doing it here:

```diff
diff --git i/docs/pages/docs/editor-basics/setup.mdx w/docs/pages/docs/editor-basics/setup.mdx
index 9c8bfef1f..07aa4e5cc 100644
--- i/docs/pages/docs/editor-basics/setup.mdx
+++ w/docs/pages/docs/editor-basics/setup.mdx
@@ -33,16 +33,6 @@ type BlockNoteEditorOptions = {
     class?: string;
   }) => Plugin;
   initialContent?: PartialBlock[];
-  pasteHandler?: (context: {
-    view: EditorView;
-    event: ClipboardEvent;
-    editor: BlockNoteEditor;
-    defaultPasteHandler: (context: {
-      pasteBehavior?: "prefer-markdown" | "prefer-html";
-    }) => boolean | undefined;
-    convertHtmlToBlockNoteHtml: (html: string) => string;
-    convertMarkdownToBlockNoteHtml: (markdown: string) => Promise<string>;
-  }) => boolean | undefined;
   resolveFileUrl: (url: string) => Promise<string>
   schema?: BlockNoteSchema;
   setIdAttribute?: boolean;
@@ -76,8 +66,6 @@ The hook takes two optional parameters:
 
 `initialContent:` The content that should be in the editor when it's created, represented as an array of [Partial Blocks](/docs/manipulating-blocks#partial-blocks).
 
-`pasteHandler`: A function that can be used to override the default paste behavior. See [Custom Paste Behavior](/docs/advanced/custom-paste-behavior) for more.
-
 `resolveFileUrl:` Function to resolve file URLs for display/download. Useful for creating authenticated URLs or implementing custom protocols.
 
 `resolveUsers`: Function to resolve user information for comments. See [Comments](/docs/collaboration/comments) for more.
@@ -130,6 +118,16 @@ export type BlockNoteViewProps = {
   editable?: boolean;
   onSelectionChange?: () => void;
   onChange?: () => void;
+  onPaste?: (context: {
+    view: EditorView;
+    event: ClipboardEvent;
+    editor: BlockNoteEditor;
+    defaultPasteHandler: (context: {
+      pasteBehavior?: "prefer-markdown" | "prefer-html";
+    }) => boolean | undefined;
+    convertHtmlToBlockNoteHtml: (html: string) => string;
+    convertMarkdownToBlockNoteHtml: (markdown: string) => Promise<string>;
+  }) => boolean | undefined;
   theme?:
     | "light"
     | "dark"
@@ -158,6 +156,8 @@ export type BlockNoteViewProps = {
 
 `onChange`: Callback fired when the editor content (document) changes.
 
+`onPaste`: A function that can be used to override the default paste behavior.
+
 `theme`: The editor's theme, see [Themes](/docs/styling-theming/themes) for more about this.
 
 `formattingToolbar`: Whether the [Formatting Toolbar](/docs/ui-components/formatting-toolbar) should be enabled.
diff --git i/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts w/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
index 1f6e8bd1c..40901a3e7 100644
--- i/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
+++ w/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
@@ -125,7 +125,7 @@ export const createPasteFromClipboardExtension = <
                   return;
                 }
 
-                return editor.settings.pasteHandler({
+                return editor.onPaste({
                   view,
                   event,
                   editor,
diff --git i/packages/core/src/editor/BlockNoteEditor.ts w/packages/core/src/editor/BlockNoteEditor.ts
index aab8824c0..2c07f1d52 100644
--- i/packages/core/src/editor/BlockNoteEditor.ts
+++ w/packages/core/src/editor/BlockNoteEditor.ts
@@ -218,12 +218,12 @@ export type BlockNoteEditorOptions<
    *
    * @example
    * ```ts
-   * pasteHandler: ({ defaultPasteHandler }) => {
+   * onPaste: ({ defaultPasteHandler }) => {
    *   return defaultPasteHandler({ pasteBehavior: "prefer-html" });
    * }
    * ```
    */
-  pasteHandler?: (context: {
+  onPaste?: (context: {
     view: EditorView;
     event: ClipboardEvent;
     editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
@@ -475,6 +475,14 @@ export class BlockNoteEditor<
   private onUploadStartCallbacks: ((blockId?: string) => void)[] = [];
   private onUploadEndCallbacks: ((blockId?: string) => void)[] = [];
 
+  /**
+   * Custom paste handler that can be used to override the default paste behavior.
+   */
+  public readonly onPaste: Exclude<
+    BlockNoteEditorOptions<BSchema, ISchema, SSchema>["onPaste"],
+    undefined
+  >;
+
   public readonly resolveFileUrl?: (url: string) => Promise<string>;
   public readonly resolveUsers?: (userIds: string[]) => Promise<User[]>;
   /**
@@ -487,10 +495,6 @@ export class BlockNoteEditor<
       cellTextColor: boolean;
       headers: boolean;
     };
-    pasteHandler: Exclude<
-      BlockNoteEditorOptions<any, any, any>["pasteHandler"],
-      undefined
-    >;
   };
 
   public static create<
@@ -538,14 +542,6 @@ export class BlockNoteEditor<
         cellTextColor: options?.tables?.cellTextColor ?? false,
         headers: options?.tables?.headers ?? false,
       },
-      pasteHandler:
-        options.pasteHandler ||
-        ((context: {
-          defaultPasteHandler: (context: {
-            pasteBehavior?: "prefer-markdown" | "prefer-html";
-          }) => boolean | undefined;
-        }) =>
-          context.defaultPasteHandler({ pasteBehavior: "prefer-markdown" })),
     };
 
     // apply defaults
@@ -630,6 +626,11 @@ export class BlockNoteEditor<
       };
     }
 
+    this.onPaste =
+      newOptions.onPaste ||
+      ((context) =>
+        context.defaultPasteHandler({ pasteBehavior: "prefer-markdown" }));
+
     this.resolveFileUrl = newOptions.resolveFileUrl;
     this.headless = newOptions._headless;

```
created_at: 2025-03-14T03:26:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -212,6 +212,51 @@ export type BlockNoteEditorOptions<
     string | undefined
   >;
 
+  /**
+   * Custom paste handler that can be used to override the default paste behavior.
+   * @returns The function should return `true` if the paste event was handled, otherwise it should return `false` if it should be canceled or `undefined` if it should be handled by another handler.
+   *
+   * @example
+   * ```ts
+   * pasteHandler: ({ defaultPasteHandler }) => {
+   *   return defaultPasteHandler({ pasteBehavior: "prefer-html" });
+   * }
+   * ```
+   */
+  pasteHandler?: (context: {
path: packages/core/src/editor/BlockNoteEditor.ts
user: login: nperez0111
body: Like, it is somewhere between the purely runtime event of onChange, and something you'd need to know upfront like uploadFile. 

This distinction isn't super clear to me what should be on the view side or not
created_at: 2025-03-14T03:31:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,6 +33,16 @@ type BlockNoteEditorOptions = {
     class?: string;
   }) => Plugin;
   initialContent?: PartialBlock[];
+  pasteHandler?: (context: {
+    view: EditorView;
+    event: ClipboardEvent;
+    editor: BlockNoteEditor;
+    defaultPasteHandler: (context: {
+      pasteBehavior?: "prefer-markdown" | "prefer-html";
+    }) => boolean | undefined;
+    convertHtmlToBlockNoteHtml: (html: string) => string;
path: docs/pages/docs/editor-basics/setup.mdx
user: login: YousefED
body: API feedback:

- what about exposing `pasteHTML` `pasteMarkdown` and `pasteText` on the editor instead of the conversion functions? this way consumers also don't need to call into prosemirror `view.paste...` functions.
- I'm not sure I like passing the `defaultPasteHandler` vs exporting that function directly (still in doubt)
created_at: 2025-03-14T10:07:26Z
author_association: COLLABORATOR
diff_hunk: @@ -8,9 +8,101 @@ import {
   StyleSchema,
 } from "../../../schema/index.js";
 import { nestedListsToBlockNoteStructure } from "../../parsers/html/util/nestedLists.js";
+import { markdownToHTML } from "../../parsers/markdown/parseMarkdown.js";
 import { acceptedMIMETypes } from "./acceptedMIMETypes.js";
 import { handleFileInsertion } from "./handleFileInsertion.js";
 import { handleVSCodePaste } from "./handleVSCodePaste.js";
+import { isMarkdown } from "../../parsers/markdown/detectMarkdown.js";
+import { EditorView } from "prosemirror-view";
+
+function convertHtmlToBlockNoteHtml(html: string) {
+  const htmlNode = nestedListsToBlockNoteStructure(html.trim());
+  return htmlNode.innerHTML;
+}
+
+function convertMarkdownToBlockNoteHtml(markdown: string) {
+  return markdownToHTML(markdown).then((html) => {
+    return convertHtmlToBlockNoteHtml(html);
+  });
+}
+
+function defaultPasteHandler({
+  view,
+  event,
+  editor,
+  pasteBehavior = "prefer-markdown",
+}: {
+  view: EditorView;
+  event: ClipboardEvent;
+  editor: BlockNoteEditor<any, any, any>;
+  pasteBehavior?: "prefer-markdown" | "prefer-html";
+}) {
+  let format: (typeof acceptedMIMETypes)[number] | undefined;
+  for (const mimeType of acceptedMIMETypes) {
+    if (event.clipboardData!.types.includes(mimeType)) {
+      format = mimeType;
+      break;
+    }
+  }
+
+  if (!format) {
+    return true;
+  }
+
+  if (format === "vscode-editor-data") {
+    handleVSCodePaste(event, view);
+    return true;
+  }
+
+  if (format === "Files") {
+    handleFileInsertion(event, editor);
+    return true;
+  }
+
+  const data = event.clipboardData!.getData(format);
+
+  if (format === "blocknote/html") {
+    // Is blocknote/html, so no need to convert it
+    view.pasteHTML(data);
+    return true;
+  }
+
+  if (format === "text/markdown") {
+    convertMarkdownToBlockNoteHtml(data).then((html) => {
+      view.pasteHTML(html);
+    });
+    return true;
+  }
+
+  if (format === "text/html") {
+    if (pasteBehavior === "prefer-markdown") {
path: packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
user: login: YousefED
body: code style feedback:

instead of having these checks spread here and below (L95), I feel like it would be cleaner to check the flag earlier (or even in a separate "middleware" function) that would just set the format to "text/markdown" and then let the default behavior paste that. makes sense / wdyt? (I didn't completely think it out)
created_at: 2025-03-14T10:15:33Z
author_association: COLLABORATOR
diff_hunk: @@ -8,9 +8,101 @@ import {
   StyleSchema,
 } from "../../../schema/index.js";
 import { nestedListsToBlockNoteStructure } from "../../parsers/html/util/nestedLists.js";
+import { markdownToHTML } from "../../parsers/markdown/parseMarkdown.js";
 import { acceptedMIMETypes } from "./acceptedMIMETypes.js";
 import { handleFileInsertion } from "./handleFileInsertion.js";
 import { handleVSCodePaste } from "./handleVSCodePaste.js";
+import { isMarkdown } from "../../parsers/markdown/detectMarkdown.js";
+import { EditorView } from "prosemirror-view";
+
+function convertHtmlToBlockNoteHtml(html: string) {
+  const htmlNode = nestedListsToBlockNoteStructure(html.trim());
+  return htmlNode.innerHTML;
+}
+
+function convertMarkdownToBlockNoteHtml(markdown: string) {
+  return markdownToHTML(markdown).then((html) => {
+    return convertHtmlToBlockNoteHtml(html);
+  });
+}
+
+function defaultPasteHandler({
+  view,
+  event,
+  editor,
+  pasteBehavior = "prefer-markdown",
+}: {
+  view: EditorView;
+  event: ClipboardEvent;
+  editor: BlockNoteEditor<any, any, any>;
+  pasteBehavior?: "prefer-markdown" | "prefer-html";
+}) {
+  let format: (typeof acceptedMIMETypes)[number] | undefined;
+  for (const mimeType of acceptedMIMETypes) {
+    if (event.clipboardData!.types.includes(mimeType)) {
+      format = mimeType;
+      break;
+    }
+  }
+
+  if (!format) {
+    return true;
+  }
+
+  if (format === "vscode-editor-data") {
+    handleVSCodePaste(event, view);
+    return true;
+  }
+
+  if (format === "Files") {
+    handleFileInsertion(event, editor);
+    return true;
+  }
+
+  const data = event.clipboardData!.getData(format);
+
+  if (format === "blocknote/html") {
+    // Is blocknote/html, so no need to convert it
+    view.pasteHTML(data);
+    return true;
+  }
+
+  if (format === "text/markdown") {
+    convertMarkdownToBlockNoteHtml(data).then((html) => {
+      view.pasteHTML(html);
+    });
+    return true;
+  }
+
+  if (format === "text/html") {
+    if (pasteBehavior === "prefer-markdown") {
+      // Use plain text instead of HTML if it looks like Markdown
+      const plainText = event.clipboardData!.getData("text/plain");
+
+      if (isMarkdown(plainText)) {
+        // Convert Markdown to HTML first, then paste as HTML
+        convertMarkdownToBlockNoteHtml(plainText).then((html) => {
+          view.pasteHTML(html);
+        });
+        return true;
+      }
+    }
+
+    view.pasteHTML(convertHtmlToBlockNoteHtml(data));
+    return true;
+  }
+
+  if (pasteBehavior === "prefer-markdown" && isMarkdown(data)) {
path: packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
user: login: YousefED
body: I'm not sure if this single flag is what we want. We now always turn on "detection" and for both markdown and HTML. is that desirable? 

Other options I can think of what the user might want:
- always paste plain text as markdown (i.e.: paste markdown instead of plain text at L103), regardless of detection
- Only run the detection for plain text, not for HTML (i.e.: always prefer regular HTML paste)?
- (Other combinations like only run the detection for HTML, not for plain text. Not sure if that makes sense)

Happy to think this through together. I don't think we need all options to be possible via different flags, especially since users can now add their own paste handlers. But I do think there should at least be a way to not rely on the auto-detection yet still paste text as markdown 
created_at: 2025-03-14T10:24:45Z
author_association: COLLABORATOR
diff_hunk: @@ -212,6 +212,51 @@ export type BlockNoteEditorOptions<
     string | undefined
   >;
 
+  /**
+   * Custom paste handler that can be used to override the default paste behavior.
+   * @returns The function should return `true` if the paste event was handled, otherwise it should return `false` if it should be canceled or `undefined` if it should be handled by another handler.
+   *
+   * @example
+   * ```ts
+   * pasteHandler: ({ defaultPasteHandler }) => {
+   *   return defaultPasteHandler({ pasteBehavior: "prefer-html" });
+   * }
+   * ```
+   */
+  pasteHandler?: (context: {
path: packages/core/src/editor/BlockNoteEditor.ts
user: login: YousefED
body: I think current approach is ok. You could do something like `return true / false` to enable chaining but at this point I'm not sure it's worthwhile (don't see a lot of scenarios where that would be useful)
created_at: 2025-03-14T10:29:10Z
author_association: COLLABORATOR
diff_hunk: @@ -33,6 +33,16 @@ type BlockNoteEditorOptions = {
     class?: string;
   }) => Plugin;
   initialContent?: PartialBlock[];
+  pasteHandler?: (context: {
+    view: EditorView;
+    event: ClipboardEvent;
+    editor: BlockNoteEditor;
+    defaultPasteHandler: (context: {
+      pasteBehavior?: "prefer-markdown" | "prefer-html";
+    }) => boolean | undefined;
+    convertHtmlToBlockNoteHtml: (html: string) => string;
path: docs/pages/docs/editor-basics/setup.mdx
user: login: nperez0111
body:  - I like exposing the methods, to keep it simple for users, I exposed pasteHTML, pasteText & pasteMarkdown methods
 - I think having too many things exposed on a top-level API make it hard for discoverability, this function would only be useful for pasting content, so it makes sense to expose on the handler you'd use it at.
created_at: 2025-03-24T14:28:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,9 +8,101 @@ import {
   StyleSchema,
 } from "../../../schema/index.js";
 import { nestedListsToBlockNoteStructure } from "../../parsers/html/util/nestedLists.js";
+import { markdownToHTML } from "../../parsers/markdown/parseMarkdown.js";
 import { acceptedMIMETypes } from "./acceptedMIMETypes.js";
 import { handleFileInsertion } from "./handleFileInsertion.js";
 import { handleVSCodePaste } from "./handleVSCodePaste.js";
+import { isMarkdown } from "../../parsers/markdown/detectMarkdown.js";
+import { EditorView } from "prosemirror-view";
+
+function convertHtmlToBlockNoteHtml(html: string) {
+  const htmlNode = nestedListsToBlockNoteStructure(html.trim());
+  return htmlNode.innerHTML;
+}
+
+function convertMarkdownToBlockNoteHtml(markdown: string) {
+  return markdownToHTML(markdown).then((html) => {
+    return convertHtmlToBlockNoteHtml(html);
+  });
+}
+
+function defaultPasteHandler({
+  view,
+  event,
+  editor,
+  pasteBehavior = "prefer-markdown",
+}: {
+  view: EditorView;
+  event: ClipboardEvent;
+  editor: BlockNoteEditor<any, any, any>;
+  pasteBehavior?: "prefer-markdown" | "prefer-html";
+}) {
+  let format: (typeof acceptedMIMETypes)[number] | undefined;
+  for (const mimeType of acceptedMIMETypes) {
+    if (event.clipboardData!.types.includes(mimeType)) {
+      format = mimeType;
+      break;
+    }
+  }
+
+  if (!format) {
+    return true;
+  }
+
+  if (format === "vscode-editor-data") {
+    handleVSCodePaste(event, view);
+    return true;
+  }
+
+  if (format === "Files") {
+    handleFileInsertion(event, editor);
+    return true;
+  }
+
+  const data = event.clipboardData!.getData(format);
+
+  if (format === "blocknote/html") {
+    // Is blocknote/html, so no need to convert it
+    view.pasteHTML(data);
+    return true;
+  }
+
+  if (format === "text/markdown") {
+    convertMarkdownToBlockNoteHtml(data).then((html) => {
+      view.pasteHTML(html);
+    });
+    return true;
+  }
+
+  if (format === "text/html") {
+    if (pasteBehavior === "prefer-markdown") {
+      // Use plain text instead of HTML if it looks like Markdown
+      const plainText = event.clipboardData!.getData("text/plain");
+
+      if (isMarkdown(plainText)) {
+        // Convert Markdown to HTML first, then paste as HTML
+        convertMarkdownToBlockNoteHtml(plainText).then((html) => {
+          view.pasteHTML(html);
+        });
+        return true;
+      }
+    }
+
+    view.pasteHTML(convertHtmlToBlockNoteHtml(data));
+    return true;
+  }
+
+  if (pasteBehavior === "prefer-markdown" && isMarkdown(data)) {
path: packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
user: login: nperez0111
body: I refactored it to make it clearer how it was being used, if prefer-markdown is used, it will try to use the plaintext version if it can, otherwise it falls back to the previous behavior.
created_at: 2025-03-24T14:29:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,59 @@
+---
path: docs/pages/docs/advanced/paste-handling.mdx
user: login: YousefED
body: needs to be added to sidebar
created_at: 2025-03-25T12:06:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,59 @@
+---
+title: Paste Handling
+description: This section explains how to handle paste events in BlockNote.
+imageTitle: Paste Handling
+---
+
+import { Example } from "@/components/example";
+
+# Paste Handling
+
+BlockNote, by default, attempts to paste content in the following order:
+
+-   VS Code compatible content
+-   BlockNote HTML
+-   Markdown
+-   HTML
+-   Plain text
+-   Files
+
+By default, in certain cases, BlockNote will attempt to convert the content of the clipboard from plain text to HTML by interpreting the plain text as markdown.
path: docs/pages/docs/advanced/paste-handling.mdx
user: login: YousefED
body: 🤯
created_at: 2025-03-25T12:09:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,59 @@
+---
+title: Paste Handling
+description: This section explains how to handle paste events in BlockNote.
+imageTitle: Paste Handling
+---
+
+import { Example } from "@/components/example";
+
+# Paste Handling
+
+BlockNote, by default, attempts to paste content in the following order:
+
+-   VS Code compatible content
+-   BlockNote HTML
+-   Markdown
+-   HTML
+-   Plain text
+-   Files
+
+By default, in certain cases, BlockNote will attempt to convert the content of the clipboard from plain text to HTML by interpreting the plain text as markdown.
+
+You can change this behavior by providing a custom paste handler.
+
+```ts
+const editor = new BlockNoteEditor({
path: docs/pages/docs/advanced/paste-handling.mdx
user: login: YousefED
body: I see you probably want to provide a simple example first - but I found it a bit confusing.

The thought I had as a reader: "what can I do in a paste handler" and "what is defaultPasteHandler"?

I think it might be clearer in this case to first explain the signature (like [here](https://www.blocknotejs.org/docs/editor-basics/setup)) or if we think that's overkill move the custom paste handler that you have below up (and explain the defaultpastehandler later)
created_at: 2025-03-25T12:14:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,59 @@
+---
+title: Paste Handling
+description: This section explains how to handle paste events in BlockNote.
+imageTitle: Paste Handling
+---
+
+import { Example } from "@/components/example";
+
+# Paste Handling
+
+BlockNote, by default, attempts to paste content in the following order:
+
+-   VS Code compatible content
+-   BlockNote HTML
+-   Markdown
+-   HTML
+-   Plain text
+-   Files
+
+By default, in certain cases, BlockNote will attempt to convert the content of the clipboard from plain text to HTML by interpreting the plain text as markdown.
+
+You can change this behavior by providing a custom paste handler.
+
+```ts
+const editor = new BlockNoteEditor({
+  pasteHandler: ({ defaultPasteHandler }) => {
+    return defaultPasteHandler({
+      pasteBehavior: "prefer-html",
+    });
+  },
+});
+```
+
+In this example, we change the default paste behavior to prefer HTML.
+
+
+<Example name="basic/custom-paste-handler" />
+
+## Custom Paste Handler
+
+You can also provide your own paste handler by providing a function to the `pasteHandler` option.
+
+```ts
+const editor = new BlockNoteEditor({
+  pasteHandler: ({ event, editor, defaultPasteHandler }) => {
+    if (event.clipboardData?.types.includes("text/my-custom-format")) {
+      const markdown = myCustomTransformer(event.clipboardData.getData("text/my-custom-format"));
+      editor.pasteText(markdown);
+      // We handled the paste event, so return true
+      return true;
path: docs/pages/docs/advanced/paste-handling.mdx
user: login: YousefED
body: we don't explain anywhere what return true vs return false does
created_at: 2025-03-25T12:15:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,105 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+import "./styles.css";
+
+export default function App() {
+  // Creates a new editor instance.
+  const editor = useCreateBlockNote({
+    initialContent: [
+      {
+        type: "paragraph",
+        content: [
+          {
+            styles: {},
+            type: "text",
+            text: "Paste some text here",
+          },
+        ],
+      },
+    ],
+    pasteHandler: ({ event, editor, defaultPasteHandler }) => {
+      if (event.clipboardData?.types.includes("text/plain")) {
+        editor.pasteMarkdown(
+          event.clipboardData.getData("text/plain") +
+            " - inserted by the custom paste handler"
+        );
+        return true;
+      }
+      return defaultPasteHandler();
+    },
+  });
+
+  // Renders the editor instance using a React component.
+  return (
+    <div>
+      <BlockNoteView editor={editor} />
+      <div className={"edit-buttons"}>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.writeText(
+                "**This is markdown in the plain text format**"
+              );
+            } catch (error) {
+              window.alert("Failed to copy plain text with markdown content");
+            }
+          }}>
+          Copy text/plain with markdown content
path: examples/01-basic/13-custom-paste-handler/App.tsx
user: login: YousefED
body: suggestion: "Copy sample markdown to clipboard (text/plain)"
created_at: 2025-03-25T12:17:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,105 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+import "./styles.css";
+
+export default function App() {
+  // Creates a new editor instance.
+  const editor = useCreateBlockNote({
+    initialContent: [
+      {
+        type: "paragraph",
+        content: [
+          {
+            styles: {},
+            type: "text",
+            text: "Paste some text here",
+          },
+        ],
+      },
+    ],
+    pasteHandler: ({ event, editor, defaultPasteHandler }) => {
+      if (event.clipboardData?.types.includes("text/plain")) {
+        editor.pasteMarkdown(
+          event.clipboardData.getData("text/plain") +
+            " - inserted by the custom paste handler"
+        );
+        return true;
+      }
+      return defaultPasteHandler();
+    },
+  });
+
+  // Renders the editor instance using a React component.
+  return (
+    <div>
+      <BlockNoteView editor={editor} />
+      <div className={"edit-buttons"}>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.writeText(
+                "**This is markdown in the plain text format**"
+              );
+            } catch (error) {
+              window.alert("Failed to copy plain text with markdown content");
+            }
+          }}>
+          Copy text/plain with markdown content
path: examples/01-basic/13-custom-paste-handler/App.tsx
user: login: YousefED
body: (and similarly for the other buttons)
created_at: 2025-03-25T12:18:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,105 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+import "./styles.css";
+
+export default function App() {
+  // Creates a new editor instance.
+  const editor = useCreateBlockNote({
+    initialContent: [
+      {
+        type: "paragraph",
+        content: [
+          {
+            styles: {},
+            type: "text",
+            text: "Paste some text here",
+          },
+        ],
+      },
+    ],
+    pasteHandler: ({ event, editor, defaultPasteHandler }) => {
+      if (event.clipboardData?.types.includes("text/plain")) {
+        editor.pasteMarkdown(
+          event.clipboardData.getData("text/plain") +
+            " - inserted by the custom paste handler"
+        );
+        return true;
+      }
+      return defaultPasteHandler();
+    },
+  });
+
+  // Renders the editor instance using a React component.
+  return (
+    <div>
+      <BlockNoteView editor={editor} />
+      <div className={"edit-buttons"}>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.writeText(
+                "**This is markdown in the plain text format**"
+              );
+            } catch (error) {
+              window.alert("Failed to copy plain text with markdown content");
+            }
+          }}>
+          Copy text/plain with markdown content
+        </button>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.write([
+                new ClipboardItem({
+                  "text/html": "<p><strong>HTML</strong></p>",
+                }),
+              ]);
+            } catch (error) {
+              window.alert("Failed to copy HTML content");
+            }
+          }}>
+          Copy text/html with HTML content
+        </button>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.writeText(
+                "This is plain text in the plain text format"
+              );
+            } catch (error) {
+              window.alert("Failed to copy plain text");
+            }
+          }}>
+          Copy plain text
+        </button>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.write([
+                new ClipboardItem({
+                  "text/plain": "Plain text",
+                }),
+                new ClipboardItem({
+                  "text/html": "<p><strong>HTML</strong></p>",
+                }),
+                new ClipboardItem({
+                  "text/markdown": "**Markdown**",
+                }),
+              ]);
+            } catch (error) {
+              window.alert("Failed to copy multiple formats");
path: examples/01-basic/13-custom-paste-handler/App.tsx
user: login: YousefED
body: I'm getting this error when I  click this button :/
created_at: 2025-03-25T12:18:39Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,7 @@
+# Custom Paste Handler
+
+In this example, we show how to change the default paste handler to handle paste events in your own way.
path: examples/01-basic/13-custom-paste-handler/README.md
user: login: YousefED
body: - suggestion would be to explain what it does (so the user knows what to pay attention to): `In this example, we change the default paste handler to append some text to the pasted content`
- let's add a "try it out" line ("use the buttons to copy some content to the clipboard and paste it in the editor to trigger our custom paste handler")
created_at: 2025-03-25T12:21:25Z
author_association: COLLABORATOR
diff_hunk: @@ -1437,4 +1472,41 @@ export class BlockNoteEditor<
   public setForceSelectionVisible(forceSelectionVisible: boolean) {
     this.showSelectionPlugin.setEnabled(forceSelectionVisible);
   }
+
+  /**
+   * This will convert HTML into a format that is compatible with BlockNote.
+   */
+  private convertHtmlToBlockNoteHtml(html: string) {
+    const htmlNode = nestedListsToBlockNoteStructure(html.trim());
+    return htmlNode.innerHTML;
+  }
+
+  /**
+   * Paste HTML into the editor. Defaults to converting HTML to BlockNote HTML.
+   * @param html The HTML to paste.
+   * @param raw Whether to paste the HTML as is, or to convert it to BlockNote HTML.
+   */
+  public pasteHTML(html: string, raw = false) {
+    let htmlToPaste = html;
+    if (!raw) {
path: packages/core/src/editor/BlockNoteEditor.ts
user: login: YousefED
body: is there a use for the `raw` flag? (i.e.: do we every call it with `raw=true` or do we see good usecases for this?)
created_at: 2025-03-25T12:27:33Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,93 @@
 import { Extension } from "@tiptap/core";
 import { Plugin } from "prosemirror-state";
 
-import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
+import type {
+  BlockNoteEditor,
+  BlockNoteEditorOptions,
+} from "../../../editor/BlockNoteEditor";
 import {
   BlockSchema,
   InlineContentSchema,
   StyleSchema,
 } from "../../../schema/index.js";
-import { nestedListsToBlockNoteStructure } from "../../parsers/html/util/nestedLists.js";
 import { acceptedMIMETypes } from "./acceptedMIMETypes.js";
 import { handleFileInsertion } from "./handleFileInsertion.js";
 import { handleVSCodePaste } from "./handleVSCodePaste.js";
+import { isMarkdown } from "../../parsers/markdown/detectMarkdown.js";
+
+function defaultPasteHandler({
+  event,
+  editor,
+  pasteBehavior = "prefer-markdown",
+}: {
+  event: ClipboardEvent;
+  editor: BlockNoteEditor<any, any, any>;
+  pasteBehavior?: "prefer-markdown" | "prefer-html";
+}) {
+  let format: (typeof acceptedMIMETypes)[number] | undefined;
+  for (const mimeType of acceptedMIMETypes) {
+    if (event.clipboardData!.types.includes(mimeType)) {
+      format = mimeType;
+      break;
+    }
+  }
+
+  if (!format) {
+    return true;
+  }
+
+  if (format === "vscode-editor-data") {
+    handleVSCodePaste(event, editor.prosemirrorView!);
+    return true;
+  }
+
+  if (format === "Files") {
+    handleFileInsertion(event, editor);
+    return true;
+  }
+
+  const data = event.clipboardData!.getData(format);
+
+  if (format === "blocknote/html") {
+    // Is blocknote/html, so no need to convert it
+    editor.pasteHTML(data, true);
+    return true;
+  }
+
+  if (format === "text/markdown") {
+    editor.pasteMarkdown(data);
+    return true;
+  }
+
+  if (pasteBehavior === "prefer-markdown") {
path: packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
user: login: YousefED
body: // when this is enabled, BlockNote tries to detect whether the `text/plain` content on the clipboard is actual markdown content. If so, it will prefer this over pasting the `text/html` content on the clipboard. This is useful because .....
`autoDetectMarkdown: boolean = true`
created_at: 2025-03-25T12:52:00Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,93 @@
 import { Extension } from "@tiptap/core";
 import { Plugin } from "prosemirror-state";
 
-import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
+import type {
+  BlockNoteEditor,
+  BlockNoteEditorOptions,
+} from "../../../editor/BlockNoteEditor";
 import {
   BlockSchema,
   InlineContentSchema,
   StyleSchema,
 } from "../../../schema/index.js";
-import { nestedListsToBlockNoteStructure } from "../../parsers/html/util/nestedLists.js";
 import { acceptedMIMETypes } from "./acceptedMIMETypes.js";
 import { handleFileInsertion } from "./handleFileInsertion.js";
 import { handleVSCodePaste } from "./handleVSCodePaste.js";
+import { isMarkdown } from "../../parsers/markdown/detectMarkdown.js";
+
+function defaultPasteHandler({
+  event,
+  editor,
+  pasteBehavior = "prefer-markdown",
+}: {
+  event: ClipboardEvent;
+  editor: BlockNoteEditor<any, any, any>;
+  pasteBehavior?: "prefer-markdown" | "prefer-html";
+}) {
+  let format: (typeof acceptedMIMETypes)[number] | undefined;
+  for (const mimeType of acceptedMIMETypes) {
+    if (event.clipboardData!.types.includes(mimeType)) {
+      format = mimeType;
+      break;
+    }
+  }
+
+  if (!format) {
+    return true;
+  }
+
+  if (format === "vscode-editor-data") {
+    handleVSCodePaste(event, editor.prosemirrorView!);
+    return true;
+  }
+
+  if (format === "Files") {
+    handleFileInsertion(event, editor);
+    return true;
+  }
+
+  const data = event.clipboardData!.getData(format);
+
+  if (format === "blocknote/html") {
+    // Is blocknote/html, so no need to convert it
+    editor.pasteHTML(data, true);
+    return true;
+  }
+
+  if (format === "text/markdown") {
+    editor.pasteMarkdown(data);
+    return true;
+  }
+
+  if (pasteBehavior === "prefer-markdown") {
+    // Use plain text instead of HTML if it looks like Markdown
+    const plainText = event.clipboardData!.getData("text/plain");
+
+    if (isMarkdown(plainText)) {
+      editor.pasteMarkdown(plainText);
+      return true;
+    }
+  }
+
+  if (format === "text/html") {
+    editor.pasteHTML(data);
+    return true;
+  }
+
+  editor.pasteText(data);
path: packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
user: login: YousefED
body: `plainTextAsMarkdown: boolean = true`
created_at: 2025-03-25T12:52:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,105 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+import "./styles.css";
+
+export default function App() {
+  // Creates a new editor instance.
+  const editor = useCreateBlockNote({
+    initialContent: [
+      {
+        type: "paragraph",
+        content: [
+          {
+            styles: {},
+            type: "text",
+            text: "Paste some text here",
+          },
+        ],
+      },
+    ],
+    pasteHandler: ({ event, editor, defaultPasteHandler }) => {
+      if (event.clipboardData?.types.includes("text/plain")) {
+        editor.pasteMarkdown(
+          event.clipboardData.getData("text/plain") +
+            " - inserted by the custom paste handler"
+        );
+        return true;
+      }
+      return defaultPasteHandler();
+    },
+  });
+
+  // Renders the editor instance using a React component.
+  return (
+    <div>
+      <BlockNoteView editor={editor} />
+      <div className={"edit-buttons"}>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.writeText(
+                "**This is markdown in the plain text format**"
+              );
+            } catch (error) {
+              window.alert("Failed to copy plain text with markdown content");
+            }
+          }}>
+          Copy text/plain with markdown content
+        </button>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.write([
+                new ClipboardItem({
+                  "text/html": "<p><strong>HTML</strong></p>",
+                }),
+              ]);
+            } catch (error) {
+              window.alert("Failed to copy HTML content");
+            }
+          }}>
+          Copy text/html with HTML content
+        </button>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.writeText(
+                "This is plain text in the plain text format"
+              );
+            } catch (error) {
+              window.alert("Failed to copy plain text");
+            }
+          }}>
+          Copy plain text
+        </button>
+        <button
+          className={"edit-button"}
+          onClick={async () => {
+            try {
+              await navigator.clipboard.write([
+                new ClipboardItem({
+                  "text/plain": "Plain text",
+                }),
+                new ClipboardItem({
+                  "text/html": "<p><strong>HTML</strong></p>",
+                }),
+                new ClipboardItem({
+                  "text/markdown": "**Markdown**",
+                }),
+              ]);
+            } catch (error) {
+              window.alert("Failed to copy multiple formats");
path: examples/01-basic/13-custom-paste-handler/App.tsx
user: login: nperez0111
body: Not supported by most browsers, but works in Safari. I marked as such
created_at: 2025-03-25T14:34:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1437,4 +1472,41 @@ export class BlockNoteEditor<
   public setForceSelectionVisible(forceSelectionVisible: boolean) {
     this.showSelectionPlugin.setEnabled(forceSelectionVisible);
   }
+
+  /**
+   * This will convert HTML into a format that is compatible with BlockNote.
+   */
+  private convertHtmlToBlockNoteHtml(html: string) {
+    const htmlNode = nestedListsToBlockNoteStructure(html.trim());
+    return htmlNode.innerHTML;
+  }
+
+  /**
+   * Paste HTML into the editor. Defaults to converting HTML to BlockNote HTML.
+   * @param html The HTML to paste.
+   * @param raw Whether to paste the HTML as is, or to convert it to BlockNote HTML.
+   */
+  public pasteHTML(html: string, raw = false) {
+    let htmlToPaste = html;
+    if (!raw) {
path: packages/core/src/editor/BlockNoteEditor.ts
user: login: nperez0111
body: We use it here: https://github.com/TypeCellOS/BlockNote/blob/af0c5ffaf90d979b72b4697806b2402c1eb117ee/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts#L55
created_at: 2025-03-25T14:46:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,59 @@
+---
path: docs/pages/docs/advanced/paste-handling.mdx
user: login: nperez0111
body: I think it is in the sidebar?
created_at: 2025-03-25T14:50:14Z
author_association: CONTRIBUTOR
path: examples/01-basic/13-custom-paste-handler/.bnexample.json
user: login: matthewlipski
body: I would move this example to the "interoperability" section tbh, especially since the docs for paste handling are in the "advanced" section, it's a bit weird to have the example in the "basic" section
created_at: 2025-03-27T16:30:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,9 @@
+# Custom Paste Handler
+
+In this example, we change the default paste handler to append some text to the pasted content when the content is plain text.
+
+**Try it out:** Use the buttons to copy some content to the clipboard and paste it in the editor to trigger our custom paste handler
path: examples/01-basic/13-custom-paste-handler/README.md
user: login: matthewlipski
body: Missing period at end of sentence
created_at: 2025-03-27T16:31:07Z
author_association: COLLABORATOR
diff_hunk: @@ -212,6 +216,39 @@ export type BlockNoteEditorOptions<
     string | undefined
   >;
 
+  /**
+   * Custom paste handler that can be used to override the default paste behavior.
+   * @returns The function should return `true` if the paste event was handled, otherwise it should return `false` if it should be canceled or `undefined` if it should be handled by another handler.
+   *
+   * @example
+   * ```ts
+   * pasteHandler: ({ defaultPasteHandler }) => {
+   *   return defaultPasteHandler({ pasteBehavior: "prefer-html" });
+   * }
+   * ```
+   */
+  pasteHandler?: (context: {
+    event: ClipboardEvent;
+    editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
+    /**
+     * The default paste handler
+     * @param context The context object
+     * @returns Whether the paste event was handled or not
+     */
+    defaultPasteHandler: (context?: {
+      /**
+       * Whether to auto-detect markdown in the clipboard and paste that as rich text.
+       * @default true
+       */
+      autoDetectMarkdown?: boolean;
+      /**
+       * Whether to interpret plain text as markdown and paste that as rich text.
+       * @default true
+       */
+      plainTextAsMarkdown?: boolean;
path: packages/core/src/editor/BlockNoteEditor.ts
user: login: matthewlipski
body: Imo the difference between `autoDetectMarkdown` and `plainTextAsMarkdown` is not super clear. Don't they kind of do the same thing, except `autoDetectMarkdown` will prioritize `text/plain` as Markdown over `text/html`?
created_at: 2025-03-27T16:38:17Z
author_association: COLLABORATOR
diff_hunk: @@ -1,6 +1,7 @@
 export const acceptedMIMETypes = [
   "vscode-editor-data",
   "blocknote/html",
+  "text/markdown",
path: packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts
user: login: matthewlipski
body: Out of curiosity, do you know any apps that use `text/markdown` for the clipboard? Since AFAIK it's not an official MIME type. I know Notion for example just puts Markdown in `text/plain`
created_at: 2025-03-27T16:42:52Z
author_association: COLLABORATOR
diff_hunk: @@ -1,6 +1,7 @@
 export const acceptedMIMETypes = [
   "vscode-editor-data",
   "blocknote/html",
+  "text/markdown",
path: packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts
user: login: nperez0111
body: It's got an RFC: https://www.rfc-editor.org/rfc/rfc7763

It is trivial for us to implement, and makes it explicit rather than having to parse plain text.

I don't have example apps that use this. And, it'd be kind of annoying to check haha
created_at: 2025-03-27T16:54:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -212,6 +216,39 @@ export type BlockNoteEditorOptions<
     string | undefined
   >;
 
+  /**
+   * Custom paste handler that can be used to override the default paste behavior.
+   * @returns The function should return `true` if the paste event was handled, otherwise it should return `false` if it should be canceled or `undefined` if it should be handled by another handler.
+   *
+   * @example
+   * ```ts
+   * pasteHandler: ({ defaultPasteHandler }) => {
+   *   return defaultPasteHandler({ pasteBehavior: "prefer-html" });
+   * }
+   * ```
+   */
+  pasteHandler?: (context: {
+    event: ClipboardEvent;
+    editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
+    /**
+     * The default paste handler
+     * @param context The context object
+     * @returns Whether the paste event was handled or not
+     */
+    defaultPasteHandler: (context?: {
+      /**
+       * Whether to auto-detect markdown in the clipboard and paste that as rich text.
+       * @default true
+       */
+      autoDetectMarkdown?: boolean;
+      /**
+       * Whether to interpret plain text as markdown and paste that as rich text.
+       * @default true
+       */
+      plainTextAsMarkdown?: boolean;
path: packages/core/src/editor/BlockNoteEditor.ts
user: login: nperez0111
body: Unsure how to make the comment clearer. Maybe you can help?

`autoDetectMarkdown` enables skipping HTML, if the content in `text/plain` MIME is detected to be markdown. (This is helpful for situations like copying what is actually markdown from a web page like GitHub, you'd expect the markdown to turn to rich text, but because it was copied from an HTML page, that is prioritized)

`plainTextAsMarkdown` is whether to interpret pasting plain text as actually being markdown. This would allow someone to opt out of the markdown conversion behavior if they wanted to preserve the plain text verbatim.
created_at: 2025-03-27T16:58:01Z
author_association: CONTRIBUTOR
path: examples/01-basic/13-custom-paste-handler/.bnexample.json
user: login: nperez0111
body: idk, I don't really like the distinction between basic and advanced. I'd prefer this to just be an example of the option, not really it's own whole thing.
created_at: 2025-03-27T17:00:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,9 @@
+# Custom Paste Handler
+
+In this example, we change the default paste handler to append some text to the pasted content when the content is plain text.
+
+**Try it out:** Use the buttons to copy some content to the clipboard and paste it in the editor to trigger our custom paste handler
path: examples/01-basic/13-custom-paste-handler/README.md
user: login: nperez0111
body: ```suggestion
**Try it out:** Use the buttons to copy some content to the clipboard and paste it in the editor to trigger our custom paste handler.
```
created_at: 2025-03-27T17:01:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -212,6 +216,39 @@ export type BlockNoteEditorOptions<
     string | undefined
   >;
 
+  /**
+   * Custom paste handler that can be used to override the default paste behavior.
+   * @returns The function should return `true` if the paste event was handled, otherwise it should return `false` if it should be canceled or `undefined` if it should be handled by another handler.
+   *
+   * @example
+   * ```ts
+   * pasteHandler: ({ defaultPasteHandler }) => {
+   *   return defaultPasteHandler({ pasteBehavior: "prefer-html" });
+   * }
+   * ```
+   */
+  pasteHandler?: (context: {
+    event: ClipboardEvent;
+    editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
+    /**
+     * The default paste handler
+     * @param context The context object
+     * @returns Whether the paste event was handled or not
+     */
+    defaultPasteHandler: (context?: {
+      /**
+       * Whether to auto-detect markdown in the clipboard and paste that as rich text.
+       * @default true
+       */
+      autoDetectMarkdown?: boolean;
+      /**
+       * Whether to interpret plain text as markdown and paste that as rich text.
+       * @default true
+       */
+      plainTextAsMarkdown?: boolean;
path: packages/core/src/editor/BlockNoteEditor.ts
user: login: matthewlipski
body: I think I would change the options slightly to this:

`plainTextAsMarkdown`: Whether to parse `text/plain` content from the clipboard as Markdown content.

`prioritizeMarkdownOverHTML`: Whether to prioritize Markdown content in `text/plain` over `text/html` when pasting from the clipboard. `plainTextAsMarkdown` must be set to `true` for this to have any effect.

Thoughts on this?
created_at: 2025-03-27T17:03:16Z
author_association: COLLABORATOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-04T20:49:44Z
body: [vc]: #EQF4jPmbvNp0VrSnnArRIuQ4XftTdjNRnU3eXJEt9Hg=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJibG9ja25vdGUtd2Vic2l0ZSIsInJvb3REaXJlY3RvcnkiOiJkb2NzLyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90eXBlY2VsbC9ibG9ja25vdGUtd2Vic2l0ZS83NGl2QkFmTW03eXV2dnhHclN1VUh0c2NXMXl5IiwicHJldmlld1VybCI6ImJsb2Nrbm90ZS13ZWJzaXRlLWdpdC1tYXJrZG93bi1wYXN0ZS10eXBlY2VsbC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn0seyJuYW1lIjoiYmxvY2tub3RlIiwicm9vdERpcmVjdG9yeSI6InBsYXlncm91bmQvIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3R5cGVjZWxsL2Jsb2Nrbm90ZS9BU3VDS3FHc0xSMVBvRVFvb1hVN2ZvY2c1Z3l1IiwicHJldmlld1VybCI6IiIsIm5leHRDb21taXRTdGF0dXMiOiJDQU5DRUxFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **blocknote** | 🛑 Canceled ([Inspect](https://vercel.com/typecell/blocknote/ASuCKqGsLR1PoEQooXU7focg5gyu)) |  | Mar 31, 2025 3:37pm |
| **blocknote-website** | ✅ Ready ([Inspect](https://vercel.com/typecell/blocknote-website/74ivBAfMm7yuvvxGrSuUHtscW1yy)) | [Visit Preview](https://blocknote-website-git-markdown-paste-typecell.vercel.app) | Mar 31, 2025 3:37pm |



author_association: NONE
user: login: YousefED
created_at: 2025-03-24T20:15:23Z
body: @nperez0111 didn't review yet; but I just noticed (working on sth unrelated) that actually inline content markdown gets parsed. try pasting `xx \*\*hello world\*\*`. Did you know this / know where this is done? Tiptap feature?
author_association: COLLABORATOR
user: login: nperez0111
created_at: 2025-03-25T11:21:48Z
body: > @nperez0111 didn't review yet; but I just noticed (working on sth unrelated) that actually inline content markdown gets parsed. try pasting `xx \*\*hello world\*\*`. Did you know this / know where this is done? Tiptap feature?

https://github.com/ueberdosis/tiptap/blob/dd0a25f5b48867992dd68d3effd5c98ddafcab69/packages/extension-bold/src/bold.tsx#L134-L145

Bold already has a paste rule set up. As well as a number of other Tiptap extensions. What does not work (in blocknote) though would be things across paragraphs or code blocks for example
author_association: CONTRIBUTOR