diff --git a/packages/core/package.json b/packages/core/package.json index 9fc8f212c6..731cfb4a46 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -113,6 +113,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..a7de2e3442 --- /dev/null +++ b/packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts @@ -0,0 +1,19 @@ +import { Parent as HASTParent } from "hast"; +import { visit } from "unist-util-visit"; + +// Originally, rehypeParse parses videos as links, which is incorrect. +export function convertVideoToMarkdown() { + return (tree: HASTParent) => { + visit(tree, "element", (node, index, parent) => { + if (parent && 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 7e87b13297..c445c71639 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3838,6 +3838,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 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__/html/video.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/video.html
new file mode 100644
index 0000000000..358091ae71
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/video.html
@@ -0,0 +1,4 @@
+
\ 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..3219bb9f00
--- /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..57202ff72c
--- /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..9fe0da9e74
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image.json
@@ -0,0 +1,22 @@
+[
+ {
+ "attrs": {
+ "id": "1",
+ },
+ "content": [
+ {
+ "attrs": {
+ "backgroundColor": "default",
+ "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..9ac9efab97
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/video.json
@@ -0,0 +1,22 @@
+[
+ {
+ "attrs": {
+ "id": "1",
+ },
+ "content": [
+ {
+ "attrs": {
+ "backgroundColor": "default",
+ "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 d27cbfad64..a452f84a5d 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,
+ },
{
testCase: {
name: "inlineContent/mentionWithToExternalHTML",
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 7a6781b938..d91910fe3c 100644
--- a/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
+++ b/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
@@ -1078,4 +1078,18 @@ Regular paragraph`,
},
executeTest: testParseMarkdown,
},
+ {
+ testCase: {
+ name: "image",
+ content: ``,
+ },
+ executeTest: testParseMarkdown,
+ },
+ {
+ testCase: {
+ name: "video",
+ content: ``,
+ },
+ executeTest: testParseMarkdown,
+ },
];
diff --git a/tests/src/unit/core/testSchema.ts b/tests/src/unit/core/testSchema.ts
index 0ccbc9733e..537350eb78 100644
--- a/tests/src/unit/core/testSchema.ts
+++ b/tests/src/unit/core/testSchema.ts
@@ -6,10 +6,7 @@ import {
createInlineContentSpec,
createPageBreakBlockSpec,
createStyleSpec,
- defaultBlockSpecs,
- defaultInlineContentSpecs,
defaultProps,
- defaultStyleSpecs,
} from "@blocknote/core";
// BLOCKS ----------------------------------------------------------------------
@@ -196,21 +193,18 @@ const FontSize = createStyleSpec(
// SCHEMA ----------------------------------------------------------------------
-export const testSchema = BlockNoteSchema.create({
+export const testSchema = BlockNoteSchema.create().extend({
blockSpecs: {
- ...defaultBlockSpecs,
pageBreak: createPageBreakBlockSpec(),
customParagraph: CustomParagraph,
simpleCustomParagraph: SimpleCustomParagraph,
simpleImage: SimpleImage,
},
inlineContentSpecs: {
- ...defaultInlineContentSpecs,
mention: Mention,
tag: Tag,
},
styleSpecs: {
- ...defaultStyleSpecs,
small: Small,
fontSize: FontSize,
},
diff --git a/tests/src/unit/react/testSchema.tsx b/tests/src/unit/react/testSchema.tsx
index f8db566489..aa6ebff0f8 100644
--- a/tests/src/unit/react/testSchema.tsx
+++ b/tests/src/unit/react/testSchema.tsx
@@ -1,10 +1,7 @@
import {
BlockNoteSchema,
createPageBreakBlockSpec,
- defaultBlockSpecs,
- defaultInlineContentSpecs,
defaultProps,
- defaultStyleSpecs,
} from "@blocknote/core";
import {
createReactBlockSpec,
@@ -15,7 +12,7 @@ import { createContext, useContext } from "react";
// BLOCKS ----------------------------------------------------------------------
-const CustomParagraph = createReactBlockSpec(
+const createCustomParagraph = createReactBlockSpec(
{
type: "customParagraph",
propSchema: defaultProps,
@@ -31,7 +28,7 @@ const CustomParagraph = createReactBlockSpec(
},
);
-const SimpleCustomParagraph = createReactBlockSpec(
+const createSimpleCustomParagraph = createReactBlockSpec(
{
type: "simpleCustomParagraph",
propSchema: defaultProps,
@@ -55,7 +52,7 @@ const ContextParagraphComponent = (props: any) => {
return ;
};
-const ContextParagraph = createReactBlockSpec(
+const createContextParagraph = createReactBlockSpec(
{
type: "contextParagraph",
propSchema: defaultProps,
@@ -165,21 +162,18 @@ const FontSize = createReactStyleSpec(
// SCHEMA ----------------------------------------------------------------------
-export const testSchema = BlockNoteSchema.create({
+export const testSchema = BlockNoteSchema.create().extend({
blockSpecs: {
- ...defaultBlockSpecs,
pageBreak: createPageBreakBlockSpec(),
- customParagraph: CustomParagraph(),
- simpleCustomParagraph: SimpleCustomParagraph(),
- contextParagraph: ContextParagraph(),
+ customParagraph: createCustomParagraph(),
+ simpleCustomParagraph: createSimpleCustomParagraph(),
+ contextParagraph: createContextParagraph(),
},
inlineContentSpecs: {
- ...defaultInlineContentSpecs,
mention: Mention,
tag: Tag,
},
styleSpecs: {
- ...defaultStyleSpecs,
small: Small,
fontSize: FontSize,
},