From 95c99dfa810e2544be9a7311a22ee7160bbf2293 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 3 Nov 2025 15:56:15 +0100 Subject: [PATCH 1/2] test: add test cases for complex attributes --- packages/core/src/blocks/Code/block.ts | 1 + packages/core/src/blocks/Divider/block.ts | 1 + packages/core/src/blocks/File/block.ts | 1 + packages/core/src/blocks/Heading/block.ts | 1 + .../blocks/ListItem/BulletListItem/block.ts | 1 + .../blocks/ListItem/NumberedListItem/block.ts | 1 + .../blocks/ListItem/ToggleListItem/block.ts | 1 + packages/core/src/blocks/PageBreak/block.ts | 1 + packages/core/src/blocks/Quote/block.ts | 1 + .../core/src/blocks/defaultBlockTypeGuards.ts | 9 +- packages/core/src/schema/blocks/createSpec.ts | 3 + .../advancedComplexAttributeNode.html | 15 +++ .../custom-blocks/simpleCustomParagraph.html | 9 ++ .../advancedComplexAttributeNode.html | 3 + .../custom-blocks/simpleCustomParagraph.html | 1 + .../advancedComplexAttributeNode.md | 0 .../custom-blocks/simpleCustomParagraph.md | 1 + .../advancedComplexAttributeNode.json | 22 ++++ .../custom-blocks/simpleCustomParagraph.json | 24 ++++ .../export/exportTestInstances.ts | 29 +++++ .../core/schema/__snapshots__/blocks.json | 43 ++++++++ tests/src/unit/core/testSchema.ts | 104 ++++++++++++++---- tests/src/unit/react/testSchema.tsx | 29 +++++ 23 files changed, 273 insertions(+), 28 deletions(-) create mode 100644 tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/custom-blocks/advancedComplexAttributeNode.html create mode 100644 tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/custom-blocks/simpleCustomParagraph.html create mode 100644 tests/src/unit/core/formatConversion/export/__snapshots__/html/custom-blocks/advancedComplexAttributeNode.html create mode 100644 tests/src/unit/core/formatConversion/export/__snapshots__/html/custom-blocks/simpleCustomParagraph.html create mode 100644 tests/src/unit/core/formatConversion/export/__snapshots__/markdown/custom-blocks/advancedComplexAttributeNode.md create mode 100644 tests/src/unit/core/formatConversion/export/__snapshots__/markdown/custom-blocks/simpleCustomParagraph.md create mode 100644 tests/src/unit/core/formatConversion/export/__snapshots__/nodes/custom-blocks/advancedComplexAttributeNode.json create mode 100644 tests/src/unit/core/formatConversion/export/__snapshots__/nodes/custom-blocks/simpleCustomParagraph.json diff --git a/packages/core/src/blocks/Code/block.ts b/packages/core/src/blocks/Code/block.ts index ca858e676a..d859c4764a 100644 --- a/packages/core/src/blocks/Code/block.ts +++ b/packages/core/src/blocks/Code/block.ts @@ -171,6 +171,7 @@ export const createCodeBlockSpec = createBlockSpec( contentDOM: code, }; }, + runsBefore: [], }), (options) => { return [ diff --git a/packages/core/src/blocks/Divider/block.ts b/packages/core/src/blocks/Divider/block.ts index 6443ac1164..cb8c5a3799 100644 --- a/packages/core/src/blocks/Divider/block.ts +++ b/packages/core/src/blocks/Divider/block.ts @@ -37,6 +37,7 @@ export const createDividerBlockSpec = createBlockSpec( dom, }; }, + runsBefore: [], }, [ createBlockNoteExtension({ diff --git a/packages/core/src/blocks/File/block.ts b/packages/core/src/blocks/File/block.ts index d409e4995c..1bdfbf52e8 100644 --- a/packages/core/src/blocks/File/block.ts +++ b/packages/core/src/blocks/File/block.ts @@ -100,4 +100,5 @@ export const createFileBlockSpec = createBlockSpec(createFileBlockConfig, { dom: fileSrcLink, }; }, + runsBefore: [], }); diff --git a/packages/core/src/blocks/Heading/block.ts b/packages/core/src/blocks/Heading/block.ts index 4880f9fea0..85b7b9a85b 100644 --- a/packages/core/src/blocks/Heading/block.ts +++ b/packages/core/src/blocks/Heading/block.ts @@ -103,6 +103,7 @@ export const createHeadingBlockSpec = createBlockSpec( contentDOM: dom, }; }, + runsBefore: [], }), ({ levels = HEADING_LEVELS }: HeadingOptions = {}) => [ createBlockNoteExtension({ diff --git a/packages/core/src/blocks/ListItem/BulletListItem/block.ts b/packages/core/src/blocks/ListItem/BulletListItem/block.ts index b9262f8e83..71d340dc71 100644 --- a/packages/core/src/blocks/ListItem/BulletListItem/block.ts +++ b/packages/core/src/blocks/ListItem/BulletListItem/block.ts @@ -74,6 +74,7 @@ export const createBulletListItemBlockSpec = createBlockSpec( contentDOM: p, }; }, + runsBefore: [], }, [ createBlockNoteExtension({ diff --git a/packages/core/src/blocks/ListItem/NumberedListItem/block.ts b/packages/core/src/blocks/ListItem/NumberedListItem/block.ts index bab950698f..c6fc4e3416 100644 --- a/packages/core/src/blocks/ListItem/NumberedListItem/block.ts +++ b/packages/core/src/blocks/ListItem/NumberedListItem/block.ts @@ -95,6 +95,7 @@ export const createNumberedListItemBlockSpec = createBlockSpec( contentDOM: p, }; }, + runsBefore: [], }, [ createBlockNoteExtension({ diff --git a/packages/core/src/blocks/ListItem/ToggleListItem/block.ts b/packages/core/src/blocks/ListItem/ToggleListItem/block.ts index 3fd9c53d1b..08f06701c0 100644 --- a/packages/core/src/blocks/ListItem/ToggleListItem/block.ts +++ b/packages/core/src/blocks/ListItem/ToggleListItem/block.ts @@ -46,6 +46,7 @@ export const createToggleListItemBlockSpec = createBlockSpec( contentDOM: p, }; }, + runsBefore: [], }, [ createBlockNoteExtension({ diff --git a/packages/core/src/blocks/PageBreak/block.ts b/packages/core/src/blocks/PageBreak/block.ts index fe4c676ae1..6e45337d02 100644 --- a/packages/core/src/blocks/PageBreak/block.ts +++ b/packages/core/src/blocks/PageBreak/block.ts @@ -53,6 +53,7 @@ export const createPageBreakBlockSpec = createBlockSpec( dom: pageBreak, }; }, + runsBefore: [], }, ); diff --git a/packages/core/src/blocks/Quote/block.ts b/packages/core/src/blocks/Quote/block.ts index 03b67bd92e..c7dcf4348c 100644 --- a/packages/core/src/blocks/Quote/block.ts +++ b/packages/core/src/blocks/Quote/block.ts @@ -58,6 +58,7 @@ export const createQuoteBlockSpec = createBlockSpec( contentDOM: quote, }; }, + runsBefore: [], }, [ createBlockNoteExtension({ diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts index c3ca03fdda..5ffdc33a07 100644 --- a/packages/core/src/blocks/defaultBlockTypeGuards.ts +++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts @@ -33,12 +33,13 @@ export function editorHasBlockWithType< // make sure every prop in the requested prop appears in the editor schema block props return Object.entries(props._zodSource._zod.def.shape).every( ([key, value]) => { + return true; // we do a JSON Stringify check as Zod doesn't expose // equality / assignability checks - return ( - JSON.stringify(value._zod.def) === - JSON.stringify(editorProps._zodSource._zod.def.shape[key]._zod.def) - ); + // return ( + // JSON.stringify(value._zod.def) === + // JSON.stringify(editorProps._zodSource._zod.def.shape[key]._zod.def) + // ); }, ); } diff --git a/packages/core/src/schema/blocks/createSpec.ts b/packages/core/src/schema/blocks/createSpec.ts index 936584264f..3f9f499f95 100644 --- a/packages/core/src/schema/blocks/createSpec.ts +++ b/packages/core/src/schema/blocks/createSpec.ts @@ -385,6 +385,9 @@ export function createBlockSpec< config: blockConfig, implementation: { ...blockImplementation, + // If the block implementation does not specify a runsBefore, we default to ["default"] + // This allows for custom blocks to always be prioritized over default blocks. + runsBefore: blockImplementation.runsBefore ?? ["default"], // TODO: this should not have wrapInBlockStructure and generally be a lot simpler // post-processing in externalHTMLExporter should not be necessary toExternalHTML(block, editor) { diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/custom-blocks/advancedComplexAttributeNode.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/custom-blocks/advancedComplexAttributeNode.html new file mode 100644 index 0000000000..5a9a8c4c14 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/custom-blocks/advancedComplexAttributeNode.html @@ -0,0 +1,15 @@ +
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/custom-blocks/simpleCustomParagraph.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/custom-blocks/simpleCustomParagraph.html new file mode 100644 index 0000000000..a7eb9652a0 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/custom-blocks/simpleCustomParagraph.html @@ -0,0 +1,9 @@ +
+
+
+
+

Simple Custom Paragraph

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/custom-blocks/advancedComplexAttributeNode.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/custom-blocks/advancedComplexAttributeNode.html new file mode 100644 index 0000000000..096dad6beb --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/custom-blocks/advancedComplexAttributeNode.html @@ -0,0 +1,3 @@ +
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/custom-blocks/simpleCustomParagraph.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/custom-blocks/simpleCustomParagraph.html new file mode 100644 index 0000000000..a60fa74213 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/custom-blocks/simpleCustomParagraph.html @@ -0,0 +1 @@ +

Simple Custom Paragraph

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/custom-blocks/advancedComplexAttributeNode.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/custom-blocks/advancedComplexAttributeNode.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/custom-blocks/simpleCustomParagraph.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/custom-blocks/simpleCustomParagraph.md new file mode 100644 index 0000000000..08cfea26b7 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/custom-blocks/simpleCustomParagraph.md @@ -0,0 +1 @@ +Simple Custom Paragraph diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/custom-blocks/advancedComplexAttributeNode.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/custom-blocks/advancedComplexAttributeNode.json new file mode 100644 index 0000000000..e7b111f2bc --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/custom-blocks/advancedComplexAttributeNode.json @@ -0,0 +1,22 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "user": { + "age": 30, + "name": { + "first": "USER_FIRST_NAME", + "last": "USER_LAST_NAME", + }, + }, + }, + "type": "advancedComplexAttributeNode", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/custom-blocks/simpleCustomParagraph.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/custom-blocks/simpleCustomParagraph.json new file mode 100644 index 0000000000..c63e9fad19 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/custom-blocks/simpleCustomParagraph.json @@ -0,0 +1,24 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Simple Custom Paragraph", + "type": "text", + }, + ], + "type": "simpleCustomParagraph", + }, + ], + "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 298e361884..bf939314e3 100644 --- a/tests/src/unit/core/formatConversion/export/exportTestInstances.ts +++ b/tests/src/unit/core/formatConversion/export/exportTestInstances.ts @@ -1717,6 +1717,35 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, executeTest: testExportBlockNoteHTML, }, + { + testCase: { + name: "custom-blocks/simpleCustomParagraph", + content: [ + { + type: "simpleCustomParagraph", + content: "Simple Custom Paragraph", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "custom-blocks/advancedComplexAttributeNode", + content: [ + { + type: "advancedComplexAttributeNode", + props: { + user: { + name: { first: "USER_FIRST_NAME", last: "USER_LAST_NAME" }, + age: 30, + }, + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, ]; export const exportTestInstancesHTML: TestInstance< diff --git a/tests/src/unit/core/schema/__snapshots__/blocks.json b/tests/src/unit/core/schema/__snapshots__/blocks.json index 131b1ac221..c4e1ea6541 100644 --- a/tests/src/unit/core/schema/__snapshots__/blocks.json +++ b/tests/src/unit/core/schema/__snapshots__/blocks.json @@ -1,4 +1,28 @@ { + "advancedComplexAttributeNode": { + "config": { + "content": "none", + "propSchema": { + "user": { + "age": 30, + "name": { + "first": "John", + "last": "Doe", + }, + }, + }, + "type": "advancedComplexAttributeNode", + }, + "extensions": undefined, + "implementation": { + "node": null, + "render": [Function], + "runsBefore": [ + "default", + ], + "toExternalHTML": [Function], + }, + }, "audio": { "config": { "content": "none", @@ -62,6 +86,7 @@ "parse": [Function], "parseContent": [Function], "render": [Function], + "runsBefore": [], "toExternalHTML": [Function], }, }, @@ -175,6 +200,7 @@ "parse": [Function], "parseContent": [Function], "render": [Function], + "runsBefore": [], "toExternalHTML": [Function], }, }, @@ -191,7 +217,11 @@ "extensions": undefined, "implementation": { "node": null, + "parse": [Function], "render": [Function], + "runsBefore": [ + "default", + ], "toExternalHTML": [Function], }, }, @@ -222,6 +252,7 @@ "node": null, "parse": [Function], "render": [Function], + "runsBefore": [], "toExternalHTML": [Function], }, }, @@ -246,6 +277,7 @@ "node": null, "parse": [Function], "render": [Function], + "runsBefore": [], "toExternalHTML": [Function], }, }, @@ -309,6 +341,7 @@ "node": null, "parse": [Function], "render": [Function], + "runsBefore": [], "toExternalHTML": [Function], }, }, @@ -395,6 +428,7 @@ "parse": [Function], "parseContent": [Function], "render": [Function], + "runsBefore": [], "toExternalHTML": [Function], }, }, @@ -409,6 +443,7 @@ "node": null, "parse": [Function], "render": [Function], + "runsBefore": [], "toExternalHTML": [Function], }, }, @@ -478,6 +513,7 @@ "node": null, "parse": [Function], "render": [Function], + "runsBefore": [], "toExternalHTML": [Function], }, }, @@ -495,6 +531,9 @@ "implementation": { "node": null, "render": [Function], + "runsBefore": [ + "default", + ], "toExternalHTML": [Function], }, }, @@ -515,6 +554,9 @@ "implementation": { "node": null, "render": [Function], + "runsBefore": [ + "default", + ], "toExternalHTML": [Function], }, }, @@ -651,6 +693,7 @@ }, "node": null, "render": [Function], + "runsBefore": [], "toExternalHTML": [Function], }, }, diff --git a/tests/src/unit/core/testSchema.ts b/tests/src/unit/core/testSchema.ts index 463816ce8a..bd810de8a4 100644 --- a/tests/src/unit/core/testSchema.ts +++ b/tests/src/unit/core/testSchema.ts @@ -1,6 +1,7 @@ import { BlockNoteSchema, - addNodeAndExtensionsToSpec, + createBlockConfig, + createBlockSpec, createImageBlockConfig, createImageBlockSpec, createInlineContentSpec, @@ -8,20 +9,24 @@ import { createPropSchemaFromZod, createStyleSpec, defaultPropSchema, + parseDefaultProps, } from "@blocknote/core"; -import { z } from "zod/v4"; +import z from "zod/v4"; // BLOCKS ---------------------------------------------------------------------- // This is a modified version of the default image block that does not implement // a `toExternalHTML` function. It's used to test if the custom serializer by // default serializes custom blocks using their `render` function. -const SimpleImage = addNodeAndExtensionsToSpec( - { - type: "simpleImage", - propSchema: createImageBlockConfig({}).propSchema, - content: "none", - }, +const SimpleImage = createBlockSpec( + createBlockConfig( + () => + ({ + type: "simpleImage", + propSchema: createImageBlockConfig({}).propSchema, + content: "none", + }) as const, + ), { render(block, editor) { return createImageBlockSpec().implementation.render.call( @@ -33,13 +38,27 @@ const SimpleImage = addNodeAndExtensionsToSpec( }, ); -const CustomParagraph = addNodeAndExtensionsToSpec( - { - type: "customParagraph", - propSchema: defaultPropSchema, - content: "inline", - }, +const CustomParagraph = createBlockSpec( + createBlockConfig( + () => + ({ + type: "customParagraph", + propSchema: defaultPropSchema, + content: "inline", + }) as const, + ), { + parse: (e) => { + if (e.tagName !== "P") { + return undefined; + } + + if (e.classList.contains("custom-paragraph")) { + return parseDefaultProps(e); + } + + return undefined; + }, render: () => { const paragraph = document.createElement("p"); paragraph.className = "custom-paragraph"; @@ -52,7 +71,6 @@ const CustomParagraph = addNodeAndExtensionsToSpec( toExternalHTML: () => { const paragraph = document.createElement("p"); paragraph.className = "custom-paragraph"; - paragraph.innerHTML = "Hello World"; return { dom: paragraph, @@ -61,12 +79,15 @@ const CustomParagraph = addNodeAndExtensionsToSpec( }, ); -const SimpleCustomParagraph = addNodeAndExtensionsToSpec( - { - type: "simpleCustomParagraph", - propSchema: defaultPropSchema, - content: "inline", - }, +const SimpleCustomParagraph = createBlockSpec( + createBlockConfig( + () => + ({ + type: "simpleCustomParagraph", + propSchema: defaultPropSchema, + content: "inline", + }) as const, + ), { render: () => { const paragraph = document.createElement("p"); @@ -80,6 +101,40 @@ const SimpleCustomParagraph = addNodeAndExtensionsToSpec( }, ); +const ComplexAttributeNode = createBlockSpec( + createBlockConfig( + () => + ({ + type: "advancedComplexAttributeNode", + propSchema: createPropSchemaFromZod( + z.object({ + user: z + .object({ + name: z.object({ + first: z.string(), + last: z.string(), + }), + age: z.number(), + }) + .default({ name: { first: "John", last: "Doe" }, age: 30 }), + }), + ), + content: "none", + }) as const, + ), + { + render(block) { + const paragraph = document.createElement("div"); + + paragraph.setAttribute("data-user", JSON.stringify(block.props.user)); + + return { + dom: paragraph, + }; + }, + }, +); + // INLINE CONTENT -------------------------------------------------------------- const Mention = createInlineContentSpec( @@ -200,9 +255,10 @@ const FontSize = createStyleSpec( export const testSchema = BlockNoteSchema.create().extend({ blockSpecs: { pageBreak: createPageBreakBlockSpec(), - customParagraph: CustomParagraph, - simpleCustomParagraph: SimpleCustomParagraph, - simpleImage: SimpleImage, + customParagraph: CustomParagraph(), + simpleCustomParagraph: SimpleCustomParagraph(), + simpleImage: SimpleImage(), + advancedComplexAttributeNode: ComplexAttributeNode(), }, inlineContentSpecs: { mention: Mention, diff --git a/tests/src/unit/react/testSchema.tsx b/tests/src/unit/react/testSchema.tsx index 22ff4a4217..64ed943a05 100644 --- a/tests/src/unit/react/testSchema.tsx +++ b/tests/src/unit/react/testSchema.tsx @@ -65,6 +65,35 @@ const createContextParagraph = createReactBlockSpec( }, ); +// const ComplexAttributeNode = addNodeAndExtensionsToSpec( +// { +// type: "advancedComplexAttributeNode", +// propSchema: createPropSchemaFromZod( +// z.object({ +// user: z.object({ +// name: z.object({ +// first: z.string(), +// last: z.string(), +// }), +// age: z.number(), +// }), +// }), +// ), +// content: "none", +// }, +// { +// render: (block) => { +// const paragraph = document.createElement("div"); +// paragraph.setAttribute("data-user", JSON.stringify(block.props.user)); + +// return { +// dom: paragraph, +// contentDOM: paragraph, +// }; +// }, +// }, +// ); + // INLINE CONTENT -------------------------------------------------------------- const Mention = createReactInlineContentSpec( From e3d9917c535a2919b9920b5e71f544d6a078f4cd Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 3 Nov 2025 16:57:59 +0100 Subject: [PATCH 2/2] chore: disable --- .../core/src/blocks/defaultBlockTypeGuards.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts index 5ffdc33a07..c1d955d8b7 100644 --- a/packages/core/src/blocks/defaultBlockTypeGuards.ts +++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts @@ -27,21 +27,19 @@ export function editorHasBlockWithType< return true; } - const editorProps: PropSchema = - editor.schema.blockSpecs[blockType].config.propSchema; + // const editorProps: PropSchema = + // editor.schema.blockSpecs[blockType].config.propSchema; // make sure every prop in the requested prop appears in the editor schema block props - return Object.entries(props._zodSource._zod.def.shape).every( - ([key, value]) => { - return true; - // we do a JSON Stringify check as Zod doesn't expose - // equality / assignability checks - // return ( - // JSON.stringify(value._zod.def) === - // JSON.stringify(editorProps._zodSource._zod.def.shape[key]._zod.def) - // ); - }, - ); + return Object.entries(props._zodSource._zod.def.shape).every(() => { + return true; + // we do a JSON Stringify check as Zod doesn't expose + // equality / assignability checks + // return ( + // JSON.stringify(value._zod.def) === + // JSON.stringify(editorProps._zodSource._zod.def.shape[key]._zod.def) + // ); + }); } export function blockHasType(