diff --git a/packages/core/package.json b/packages/core/package.json
index 782e8df22e..40960535c3 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -110,6 +110,7 @@
"remark-rehype": "^11.1.2",
"remark-stringify": "^11.0.0",
"unified": "^11.0.5",
+ "unist-util-visit": "^5.0.0",
"uuid": "^8.3.2",
"y-prosemirror": "^1.3.7",
"y-protocols": "^1.0.6",
diff --git a/packages/core/src/api/exporters/markdown/markdownExporter.ts b/packages/core/src/api/exporters/markdown/markdownExporter.ts
index d19d4a3888..23aad8db7c 100644
--- a/packages/core/src/api/exporters/markdown/markdownExporter.ts
+++ b/packages/core/src/api/exporters/markdown/markdownExporter.ts
@@ -13,13 +13,15 @@ import {
StyleSchema,
} from "../../../schema/index.js";
import { createExternalHTMLExporter } from "../html/externalHTMLExporter.js";
-import { removeUnderlines } from "./removeUnderlinesRehypePlugin.js";
+import { removeUnderlines } from "./util/removeUnderlinesRehypePlugin.js";
import { addSpacesToCheckboxes } from "./util/addSpacesToCheckboxesRehypePlugin.js";
+import { convertVideoToMarkdown } from "./util/convertVideoToMarkdownRehypePlugin.js";
// Needs to be sync because it's used in drag handler event (SideMenuPlugin)
export function cleanHTMLToMarkdown(cleanHTMLString: string) {
const markdownString = unified()
.use(rehypeParse, { fragment: true })
+ .use(convertVideoToMarkdown)
.use(removeUnderlines)
.use(addSpacesToCheckboxes)
.use(rehypeRemark)
diff --git a/packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts b/packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts
new file mode 100644
index 0000000000..34fbcec7d4
--- /dev/null
+++ b/packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts
@@ -0,0 +1,18 @@
+import { visit } from "unist-util-visit";
+
+// Originally, rehypeParse parses videos as links, which is incorrect.
+export function convertVideoToMarkdown() {
+ return (tree: any) => {
+ visit(tree, "element", (node, index, parent) => {
+ if (node.tagName === "video") {
+ const src = node.properties?.src || node.properties?.["data-url"] || "";
+ const name =
+ node.properties?.title || node.properties?.["data-name"] || "";
+ parent.children[index!] = {
+ type: "text",
+ value: ``,
+ };
+ }
+ });
+ };
+}
diff --git a/packages/core/src/api/exporters/markdown/removeUnderlinesRehypePlugin.ts b/packages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts
similarity index 100%
rename from packages/core/src/api/exporters/markdown/removeUnderlinesRehypePlugin.ts
rename to packages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts
diff --git a/packages/core/src/api/parsers/markdown/parseMarkdown.ts b/packages/core/src/api/parsers/markdown/parseMarkdown.ts
index f8f8a5831b..d329a5d19c 100644
--- a/packages/core/src/api/parsers/markdown/parseMarkdown.ts
+++ b/packages/core/src/api/parsers/markdown/parseMarkdown.ts
@@ -14,6 +14,7 @@ import {
StyleSchema,
} from "../../../schema/index.js";
import { HTMLToBlocks } from "../html/parseHTML.js";
+import { isVideoUrl } from "../../../util/string.js";
// modified version of https://github.com/syntax-tree/mdast-util-to-hast/blob/main/lib/handlers/code.js
// that outputs a data-language attribute instead of a CSS class (e.g.: language-typescript)
@@ -54,6 +55,27 @@ function code(state: any, node: any) {
return result;
}
+function video(state: any, node: any) {
+ const url = String(node?.url || "");
+ const title = node?.title ? String(node.title) : undefined;
+
+ let result: any = {
+ type: "element",
+ tagName: "video",
+ properties: {
+ src: url,
+ "data-name": title,
+ "data-url": url,
+ controls: true,
+ },
+ children: [],
+ };
+ state.patch?.(node, result);
+ result = state.applyData ? state.applyData(node, result) : result;
+
+ return result;
+}
+
export function markdownToHTML(markdown: string): string {
const htmlString = unified()
.use(remarkParse)
@@ -61,6 +83,15 @@ export function markdownToHTML(markdown: string): string {
.use(remarkRehype, {
handlers: {
...(remarkRehypeDefaultHandlers as any),
+ image: (state: any, node: any) => {
+ const url = String(node?.url || "");
+
+ if (isVideoUrl(url)) {
+ return video(state, node);
+ } else {
+ return remarkRehypeDefaultHandlers.image(state, node);
+ }
+ },
code,
},
})
diff --git a/packages/core/src/util/string.ts b/packages/core/src/util/string.ts
index a2bbc6822d..8f863af0a8 100644
--- a/packages/core/src/util/string.ts
+++ b/packages/core/src/util/string.ts
@@ -13,3 +13,24 @@ export function filenameFromURL(url: string): string {
}
return parts[parts.length - 1];
}
+
+export function isVideoUrl(url: string) {
+ const videoExtensions = [
+ "mp4",
+ "webm",
+ "ogg",
+ "mov",
+ "mkv",
+ "flv",
+ "avi",
+ "wmv",
+ "m4v",
+ ];
+ try {
+ const pathname = new URL(url).pathname;
+ const ext = pathname.split(".").pop()?.toLowerCase() || "";
+ return videoExtensions.includes(ext);
+ } catch (_) {
+ return false;
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b7d7c26e08..deaab336ac 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3659,6 +3659,9 @@ importers:
unified:
specifier: ^11.0.5
version: 11.0.5
+ unist-util-visit:
+ specifier: ^5.0.0
+ version: 5.0.0
uuid:
specifier: ^8.3.2
version: 8.3.2
@@ -23107,7 +23110,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
- '@types/node': 22.15.2
+ '@types/node': 20.17.50
merge-stream: 2.0.0
supports-color: 8.1.1
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image.html
new file mode 100644
index 0000000000..d1e3c44dcb
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image.html
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video.html
new file mode 100644
index 0000000000..17f854362d
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video.html
@@ -0,0 +1,24 @@
+
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image.html
new file mode 100644
index 0000000000..b87e8db4c2
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image.html
@@ -0,0 +1,4 @@
+https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/video.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/video.html
new file mode 100644
index 0000000000..7b47786586
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/video.html
@@ -0,0 +1,4 @@
+https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image.md
new file mode 100644
index 0000000000..cbc7a9c83a
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image.md
@@ -0,0 +1 @@
+
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/video.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/video.md
new file mode 100644
index 0000000000..c2dd43052b
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/video.md
@@ -0,0 +1 @@
+
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image.json
new file mode 100644
index 0000000000..e89bc81f5c
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image.json
@@ -0,0 +1,23 @@
+[
+ {
+ "attrs": {
+ "backgroundColor": "default",
+ "id": "1",
+ "textColor": "default",
+ },
+ "content": [
+ {
+ "attrs": {
+ "caption": "",
+ "name": "",
+ "previewWidth": undefined,
+ "showPreview": true,
+ "textAlignment": "left",
+ "url": "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png",
+ },
+ "type": "image",
+ },
+ ],
+ "type": "blockContainer",
+ },
+]
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/video.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/video.json
new file mode 100644
index 0000000000..a02c78123c
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/video.json
@@ -0,0 +1,23 @@
+[
+ {
+ "attrs": {
+ "backgroundColor": "default",
+ "id": "1",
+ "textColor": "default",
+ },
+ "content": [
+ {
+ "attrs": {
+ "caption": "",
+ "name": "",
+ "previewWidth": undefined,
+ "showPreview": true,
+ "textAlignment": "left",
+ "url": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+ },
+ "type": "video",
+ },
+ ],
+ "type": "blockContainer",
+ },
+]
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/exportTestInstances.ts b/tests/src/unit/core/formatConversion/export/exportTestInstances.ts
index f6f38ba522..f7293a5a38 100644
--- a/tests/src/unit/core/formatConversion/export/exportTestInstances.ts
+++ b/tests/src/unit/core/formatConversion/export/exportTestInstances.ts
@@ -1629,6 +1629,34 @@ export const exportTestInstancesBlockNoteHTML: TestInstance<
},
executeTest: testExportBlockNoteHTML,
},
+ {
+ testCase: {
+ name: "image",
+ content: [
+ {
+ type: "image",
+ props: {
+ url: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png",
+ },
+ },
+ ],
+ },
+ executeTest: testExportBlockNoteHTML,
+ },
+ {
+ testCase: {
+ name: "video",
+ content: [
+ {
+ type: "video",
+ props: {
+ url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+ },
+ },
+ ],
+ },
+ executeTest: testExportBlockNoteHTML,
+ },
];
export const exportTestInstancesHTML: TestInstance<
diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/image.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/image.json
new file mode 100644
index 0000000000..80317eaddd
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/image.json
@@ -0,0 +1,16 @@
+[
+ {
+ "children": [],
+ "content": undefined,
+ "id": "1",
+ "props": {
+ "backgroundColor": "default",
+ "caption": "",
+ "name": "Image",
+ "showPreview": true,
+ "textAlignment": "left",
+ "url": "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png",
+ },
+ "type": "image",
+ },
+]
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/video.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/video.json
new file mode 100644
index 0000000000..5070e1873e
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/video.json
@@ -0,0 +1,16 @@
+[
+ {
+ "children": [],
+ "content": undefined,
+ "id": "1",
+ "props": {
+ "backgroundColor": "default",
+ "caption": "",
+ "name": "",
+ "showPreview": true,
+ "textAlignment": "left",
+ "url": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+ },
+ "type": "video",
+ },
+]
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts b/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
index 26258e6124..9cda85970c 100644
--- a/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
+++ b/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
@@ -996,4 +996,18 @@ Regular paragraph`,
},
executeTest: testParseMarkdown,
},
+ {
+ testCase: {
+ name: "image",
+ content: ``,
+ },
+ executeTest: testParseMarkdown,
+ },
+ {
+ testCase: {
+ name: "video",
+ content: ``,
+ },
+ executeTest: testParseMarkdown,
+ },
];