diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
index 61a657ed7..a39c2d7b9 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
@@ -117,28 +117,55 @@ function serializeBlock<
].implementation.toExternalHTML({ ...block, props } as any, editor as any);
const elementFragment = doc.createDocumentFragment();
- if (ret.dom.classList.contains("bn-block-content")) {
- const blockContentDataAttributes = [
- ...attrs,
- ...Array.from(ret.dom.attributes),
- ].filter(
- (attr) =>
- attr.name.startsWith("data") &&
- attr.name !== "data-content-type" &&
- attr.name !== "data-file-block" &&
- attr.name !== "data-node-view-wrapper" &&
- attr.name !== "data-node-type" &&
- attr.name !== "data-id" &&
- attr.name !== "data-index" &&
- attr.name !== "data-editable"
- );
- // ret.dom = ret.dom.firstChild! as any;
- for (const attr of blockContentDataAttributes) {
- (ret.dom.firstChild! as HTMLElement).setAttribute(attr.name, attr.value);
+ let listType = undefined;
+ if (orderedListItemBlockTypes.has(block.type!)) {
+ listType = "OL";
+ } else if (unorderedListItemBlockTypes.has(block.type!)) {
+ listType = "UL";
+ }
+
+ const blockContentDataAttributes = [
+ ...attrs,
+ ...Array.from(ret.dom.attributes),
+ ].filter(
+ (attr) =>
+ attr.name.startsWith("data") &&
+ attr.name !== "data-content-type" &&
+ attr.name !== "data-file-block" &&
+ attr.name !== "data-node-view-wrapper" &&
+ attr.name !== "data-node-type" &&
+ attr.name !== "data-id" &&
+ attr.name !== "data-index" &&
+ attr.name !== "data-editable"
+ );
+
+ if (ret.dom.classList.contains("bn-block-content")) {
+ // We wrap the output in an `li` element for list items, and so we want to
+ // add the attributes to that element instead as it is the "root".
+ if (!listType) {
+ // Copies the styles and prop-related attributes from the `blockContent`
+ // element onto its first child, as the `blockContent` element is omitted
+ // from external HTML. This is so prop data is preserved via `data-*`
+ // attributes or inline styles.
+ //
+ // The styles are specifically for default props on default blocks, as
+ // they get converted from `data-*` attributes for external HTML. Will
+ // need to revisit this when we convert default blocks to use the custom
+ // block API.
+ const style = ret.dom.getAttribute("style");
+ if (style) {
+ (ret.dom.firstChild! as HTMLElement).setAttribute("style", style);
+ }
+ for (const attr of blockContentDataAttributes) {
+ (ret.dom.firstChild! as HTMLElement).setAttribute(
+ attr.name,
+ attr.value
+ );
+ }
}
- addAttributesAndRemoveClasses(ret.dom.firstChild! as HTMLElement);
+ addAttributesAndRemoveClasses(ret.dom.firstChild as HTMLElement);
elementFragment.append(...Array.from(ret.dom.childNodes));
} else {
elementFragment.append(ret.dom);
@@ -155,13 +182,6 @@ function serializeBlock<
ret.contentDOM.appendChild(ic);
}
- let listType = undefined;
- if (orderedListItemBlockTypes.has(block.type!)) {
- listType = "OL";
- } else if (unorderedListItemBlockTypes.has(block.type!)) {
- listType = "UL";
- }
-
if (listType) {
if (fragment.lastChild?.nodeName !== listType) {
const list = doc.createElement(listType);
@@ -172,6 +192,24 @@ function serializeBlock<
fragment.append(list);
}
const li = doc.createElement("li");
+
+ // Copies the styles and prop-related attributes from the `blockContent`
+ // element onto its first child, as the `blockContent` element is omitted
+ // from external HTML. This is so prop data is preserved via `data-*`
+ // attributes or inline styles.
+ //
+ // The styles are specifically for default props on default blocks, as
+ // they get converted from `data-*` attributes for external HTML. Will
+ // need to revisit this when we convert default blocks to use the custom
+ // block API.
+ const style = ret.dom.getAttribute("style");
+ if (style) {
+ li.setAttribute("style", style);
+ }
+ for (const attr of blockContentDataAttributes) {
+ li.setAttribute(attr.name, attr.value);
+ }
+
li.append(elementFragment);
fragment.lastChild!.appendChild(li);
} else {
diff --git a/packages/core/src/blocks/defaultBlockHelpers.ts b/packages/core/src/blocks/defaultBlockHelpers.ts
index fbba956c8..b8220e310 100644
--- a/packages/core/src/blocks/defaultBlockHelpers.ts
+++ b/packages/core/src/blocks/defaultBlockHelpers.ts
@@ -1,5 +1,6 @@
import { blockToNode } from "../api/nodeConversions/blockToNode.js";
import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
+import { COLORS_DEFAULT } from "../editor/defaultColors.js";
import type {
BlockNoDefaults,
BlockSchema,
@@ -55,14 +56,17 @@ export function createDefaultBlockDOMOutputSpec(
// Function used to convert default blocks to HTML. It uses the corresponding
// node's `renderHTML` method to do the conversion by using a default
-// `DOMSerializer`.
+// `DOMSerializer`. The `external` flag is used to modify the resulting HTML for
+// external use. This just involves changing props being rendered from `data-*`
+// attributes to inline styles.
export const defaultBlockToHTML = <
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema
>(
block: BlockNoDefaults Paragraph list item Caption Caption Paragraph list item Caption Paragraph list item Caption Paragraph 1 Paragraph 1 Numbered List Item 1 Numbered List Item 1 Bullet List Item 1 Bullet List Item 1 Check List Item 1Heading
2Heading
2Heading
2Heading 1
+Heading 1
-
-
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/misc.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/misc.html
index 0df8a1834..a5fd6fe7e 100644
--- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/misc.html
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/misc.html
@@ -1,4 +1,4 @@
-
+
Heading
@@ -6,7 +6,9 @@
Paragraph
Paragraph
Check List Item 1
Check List Item 2
Check List Item 1
-Check List Item 2
Plain
Red Text
diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts b/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts
index 63d3552df..74654d55e 100644
--- a/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts
+++ b/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts
@@ -1,5 +1,8 @@
import { ExportParseEqualityTestCase } from "../../../shared/formatConversion/exportParseEquality/exportParseEqualityTestCase.js";
-import { testExportParseEqualityBlockNoteHTML } from "../../../shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.js";
+import {
+ testExportParseEqualityBlockNoteHTML,
+ testExportParseEqualityHTML,
+} from "../../../shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.js";
import { TestInstance } from "../../../types.js";
import {
TestBlockSchema,
@@ -21,3 +24,291 @@ export const exportParseEqualityTestInstancesBlockNoteHTML: TestInstance<
testCase,
executeTest: testExportParseEqualityBlockNoteHTML,
}));
+
+export const exportParseEqualityTestInstancesHTML: TestInstance<
+ ExportParseEqualityTestCase<
+ TestBlockSchema,
+ TestInlineContentSchema,
+ TestStyleSchema
+ >,
+ TestBlockSchema,
+ TestInlineContentSchema,
+ TestStyleSchema
+>[] = [
+ {
+ testCase: {
+ name: "schema/blocks",
+ content: [
+ {
+ type: "paragraph",
+ content: "Paragraph",
+ },
+ {
+ type: "heading",
+ content: "Heading",
+ },
+ {
+ type: "quote",
+ content: "Quote",
+ },
+ {
+ type: "bulletListItem",
+ content: "Bullet List Item",
+ },
+ {
+ type: "numberedListItem",
+ content: "Numbered List Item",
+ },
+ {
+ type: "checkListItem",
+ content: "Check List Item",
+ },
+ {
+ type: "codeBlock",
+ content: "Code",
+ },
+ {
+ type: "table",
+ content: {
+ type: "tableContent",
+ rows: [
+ {
+ cells: ["Table Cell", "Table Cell"],
+ },
+ {
+ cells: ["Table Cell", "Table Cell"],
+ },
+ ],
+ },
+ },
+ ],
+ },
+ executeTest: testExportParseEqualityHTML,
+ },
+ {
+ testCase: {
+ name: "schema/blockProps",
+ content: [
+ {
+ type: "paragraph",
+ content: "Paragraph",
+ props: {
+ textColor: "red",
+ backgroundColor: "blue",
+ textAlignment: "center",
+ },
+ },
+ {
+ type: "heading",
+ content: "Heading",
+ props: {
+ level: 2,
+ },
+ },
+ {
+ type: "checkListItem",
+ content: "Check List Item",
+ props: {
+ checked: true,
+ textColor: "red",
+ backgroundColor: "blue",
+ textAlignment: "center",
+ },
+ },
+ {
+ type: "codeBlock",
+ content: "Code",
+ props: { language: "javascript" },
+ },
+ ],
+ },
+ executeTest: testExportParseEqualityHTML,
+ },
+ {
+ testCase: {
+ name: "schema/inlineContent",
+ content: [
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "Text ",
+ styles: {},
+ },
+ {
+ type: "link",
+ content: "Link",
+ href: "https://example.com",
+ },
+ ],
+ },
+ ],
+ },
+ executeTest: testExportParseEqualityHTML,
+ },
+ {
+ testCase: {
+ name: "schema/styles",
+ content: [
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "T",
+ styles: {
+ bold: true,
+ italic: true,
+ underline: true,
+ strike: true,
+ // Code cannot be applied on top of other styles.
+ // code: true,
+ textColor: "red",
+ backgroundColor: "blue",
+ },
+ },
+ ],
+ },
+ ],
+ },
+ executeTest: testExportParseEqualityHTML,
+ },
+ {
+ testCase: {
+ name: "lists/nested",
+ content: [
+ {
+ type: "bulletListItem",
+ content: "List Item 1",
+ children: [
+ {
+ type: "bulletListItem",
+ content: "Nested List Item 1",
+ },
+ {
+ type: "bulletListItem",
+ content: "Nested List Item 2",
+ },
+ ],
+ },
+ {
+ type: "bulletListItem",
+ content: "List Item 2",
+ },
+ ],
+ },
+ executeTest: testExportParseEqualityHTML,
+ },
+ {
+ testCase: {
+ name: "tables/advanced",
+ content: [
+ {
+ type: "table",
+ content: {
+ type: "tableContent",
+ columnWidths: [199, 148, 201],
+ headerRows: 1,
+ rows: [
+ {
+ cells: [
+ {
+ type: "tableCell",
+ content: "This row has headers",
+ props: {
+ textAlignment: "center",
+ },
+ },
+ {
+ type: "tableCell",
+ content: [
+ {
+ type: "text",
+ text: "This is ",
+ styles: {},
+ },
+ {
+ type: "text",
+ text: "RED",
+ styles: {
+ bold: true,
+ },
+ },
+ ],
+ props: {
+ backgroundColor: "red",
+ textAlignment: "center",
+ },
+ },
+ {
+ type: "tableCell",
+ content: "Text is Blue",
+ props: {
+ textColor: "blue",
+ textAlignment: "center",
+ },
+ },
+ ],
+ },
+ {
+ cells: [
+ {
+ type: "tableCell",
+ content: "This spans 2 columns\nand 2 rows",
+ props: {
+ colspan: 2,
+ rowspan: 2,
+ backgroundColor: "yellow",
+ },
+ },
+ {
+ type: "tableCell",
+ content: "Sooo many features",
+ props: {
+ backgroundColor: "gray",
+ textColor: "default",
+ textAlignment: "left",
+ },
+ },
+ ],
+ },
+ {
+ cells: [
+ {
+ type: "tableCell",
+ content: [],
+ props: {
+ backgroundColor: "gray",
+ textColor: "purple",
+ },
+ },
+ ],
+ },
+ {
+ cells: [
+ {
+ type: "tableCell",
+ content: "A cell",
+ },
+ {
+ type: "tableCell",
+ content: "Another Cell",
+ },
+ {
+ type: "tableCell",
+ content: "Aligned center",
+ props: {
+ textAlignment: "center",
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ ],
+ },
+ executeTest: testExportParseEqualityHTML,
+ },
+];
diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/runTests.test.ts b/tests/src/unit/core/formatConversion/exportParseEquality/runTests.test.ts
index 3970bb02d..948a80d4e 100644
--- a/tests/src/unit/core/formatConversion/exportParseEquality/runTests.test.ts
+++ b/tests/src/unit/core/formatConversion/exportParseEquality/runTests.test.ts
@@ -2,7 +2,10 @@ import { describe, it } from "vitest";
import { setupTestEditor } from "../../setupTestEditor.js";
import { testSchema } from "../../testSchema.js";
-import { exportParseEqualityTestInstancesBlockNoteHTML } from "./exportParseEqualityTestInstances.js";
+import {
+ exportParseEqualityTestInstancesBlockNoteHTML,
+ exportParseEqualityTestInstancesHTML,
+} from "./exportParseEqualityTestInstances.js";
// Tests for verifying that exporting blocks to another format, then importing
// them back results in the same blocks as the original. Used for as many cases
@@ -20,3 +23,16 @@ describe("Export/parse equality tests (BlockNote HTML)", () => {
});
}
});
+
+describe("Export/parse equality tests (HTML)", () => {
+ const getEditor = setupTestEditor(testSchema);
+
+ for (const {
+ testCase,
+ executeTest,
+ } of exportParseEqualityTestInstancesHTML) {
+ it(`${testCase.name}`, async () => {
+ await executeTest(getEditor(), testCase);
+ });
+ }
+});
diff --git a/tests/src/unit/core/formatConversion/formatConversionTestUtil.ts b/tests/src/unit/core/formatConversion/formatConversionTestUtil.ts
index a71cdbda0..70ec3fc34 100644
--- a/tests/src/unit/core/formatConversion/formatConversionTestUtil.ts
+++ b/tests/src/unit/core/formatConversion/formatConversionTestUtil.ts
@@ -9,6 +9,7 @@ import {
PartialBlock,
PartialInlineContent,
PartialTableCell,
+ PartialTableContent,
StyledText,
StyleSchema,
TableCell,
@@ -153,10 +154,10 @@ export function partialBlockToBlockForTesting<
);
if (contentType === "inline") {
- const content = withDefaults.content as InlineContent[] | undefined;
+ const content = withDefaults.content as PartialInlineContent;
withDefaults.content = partialContentToInlineContent(content) as any;
} else if (contentType === "table") {
- const content = withDefaults.content as TableContent | undefined;
+ const content = withDefaults.content as PartialTableContent;
withDefaults.content = {
type: "tableContent",
columnWidths:
@@ -167,7 +168,34 @@ export function partialBlockToBlockForTesting<
headerCols: content?.headerCols || undefined,
rows:
content?.rows.map((row) => ({
- cells: row.cells.map((cell) => partialContentToInlineContent(cell)),
+ cells: row.cells.map((cell) =>
+ typeof cell === "object" &&
+ "type" in cell &&
+ cell.type === "tableCell"
+ ? {
+ type: "tableCell",
+ props: {
+ backgroundColor: "default",
+ colspan: 1,
+ rowspan: 1,
+ textAlignment: "left",
+ textColor: "default",
+ ...cell.props,
+ },
+ content: partialContentToInlineContent(cell.content),
+ }
+ : {
+ type: "tableCell",
+ props: {
+ backgroundColor: "default",
+ colspan: 1,
+ rowspan: 1,
+ textAlignment: "left",
+ textColor: "default",
+ },
+ content: partialContentToInlineContent(cell),
+ }
+ ),
})) || [],
} as any;
}
diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/html/backgroundColorProp.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/html/backgroundColorProp.json
index f2ebc5ed9..15f5d0729 100644
--- a/tests/src/unit/core/formatConversion/parse/__snapshots__/html/backgroundColorProp.json
+++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/html/backgroundColorProp.json
@@ -4,30 +4,13 @@
"content": [
{
"styles": {},
- "text": "Red Background",
+ "text": "Blue Background",
"type": "text",
},
],
"id": "1",
"props": {
- "backgroundColor": "red",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
- {
- "styles": {},
- "text": "Green Background",
- "type": "text",
- },
- ],
- "id": "2",
- "props": {
- "backgroundColor": "green",
+ "backgroundColor": "blue",
"textAlignment": "left",
"textColor": "default",
},
@@ -42,7 +25,7 @@
"type": "text",
},
],
- "id": "3",
+ "id": "2",
"props": {
"backgroundColor": "blue",
"textAlignment": "left",
diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/html/backgroundColorStyle.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/html/backgroundColorStyle.json
index 0f1dfc15e..8afe915ba 100644
--- a/tests/src/unit/core/formatConversion/parse/__snapshots__/html/backgroundColorStyle.json
+++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/html/backgroundColorStyle.json
@@ -4,21 +4,9 @@
"content": [
{
"styles": {
- "backgroundColor": "red",
- },
- "text": "Red Background",
- "type": "text",
- },
- {
- "styles": {},
- "text": " ",
- "type": "text",
- },
- {
- "styles": {
- "backgroundColor": "green",
+ "backgroundColor": "blue",
},
- "text": "Green Background",
+ "text": "Blue Background",
"type": "text",
},
{
diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/html/textColorProp.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/html/textColorProp.json
index 7480fcab3..4211362bf 100644
--- a/tests/src/unit/core/formatConversion/parse/__snapshots__/html/textColorProp.json
+++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/html/textColorProp.json
@@ -4,7 +4,7 @@
"content": [
{
"styles": {},
- "text": "Red Paragraph",
+ "text": "Blue Text",
"type": "text",
},
],
@@ -12,7 +12,7 @@
"props": {
"backgroundColor": "default",
"textAlignment": "left",
- "textColor": "red",
+ "textColor": "blue",
},
"type": "paragraph",
},
@@ -21,28 +21,11 @@
"content": [
{
"styles": {},
- "text": "Green Paragraph",
+ "text": "Blue Text",
"type": "text",
},
],
"id": "2",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "green",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
- {
- "styles": {},
- "text": "Blue Paragraph",
- "type": "text",
- },
- ],
- "id": "3",
"props": {
"backgroundColor": "default",
"textAlignment": "left",
diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/html/textColorStyle.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/html/textColorStyle.json
index faeeee527..e78e1a4b3 100644
--- a/tests/src/unit/core/formatConversion/parse/__snapshots__/html/textColorStyle.json
+++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/html/textColorStyle.json
@@ -4,21 +4,9 @@
"content": [
{
"styles": {
- "textColor": "red",
- },
- "text": "Red Text",
- "type": "text",
- },
- {
- "styles": {},
- "text": " ",
- "type": "text",
- },
- {
- "styles": {
- "textColor": "green",
+ "textColor": "blue",
},
- "text": "Green Text",
+ "text": "Blue Text",
"type": "text",
},
{
diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/html/twoTables.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/html/twoTables.json
index 3ffbb3ead..1def7a8e3 100644
--- a/tests/src/unit/core/formatConversion/parse/__snapshots__/html/twoTables.json
+++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/html/twoTables.json
@@ -45,7 +45,7 @@
{
"styles": {},
"text": "
-
+
Name: [Company Representative]
Title: Chief Executive Officer",
"type": "text",
diff --git a/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts b/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
index 375bfd098..2ec30e07d 100644
--- a/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
+++ b/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
@@ -20,11 +20,11 @@ export const parseTestInstancesHTML: TestInstance<
testCase: {
name: "basicBlockTypes",
content: `Heading 1
-Heading 2
-Heading 3
-
Paragraph
-None Bold Italic Underline Strikethrough All
Paragraph
+None Bold Italic Underline Strikethrough All
Bullet List Item
-Bullet List Item
-Nested Bullet List Item
-Nested Bullet List Item
-Bullet List Item
-Numbered List Item
-Numbered List Item
-Nested Numbered List Item
-Nested Numbered List Item
-Numbered List Item
-Checked List Item
-Checked List Item
-Nested Checked List Item
-Nested Checked List Item
-Checked List Item
-Bullet List Item
+Bullet List Item
+Nested Bullet List Item
+Nested Bullet List Item
+Bullet List Item
+Numbered List Item
+Numbered List Item
+Nested Numbered List Item
+Nested Numbered List Item
+Numbered List Item
+Checked List Item
+Checked List Item
+Nested Checked List Item
+Nested Checked List Item
+Checked List Item
+
-
-
Image Caption
-Image Caption
+ `, }, executeTest: testParseHTML, }, @@ -276,27 +276,27 @@ export const parseTestInstancesHTML: TestInstance< testCase: { name: "deepNestedContent", content: `Paragraph
-Bold Italic Underline Strikethrough All
Paragraph
+Bold Italic Underline Strikethrough All
Nested Paragraph
-Nested Paragraph
+ `, }, executeTest: testParseHTML, }, @@ -315,21 +315,21 @@ export const parseTestInstancesHTML: TestInstance< testCase: { name: "twoTables", content: `
- Company - |
-
- Example Company Inc. -- Name: [Company Representative] - -Title: Chief Executive Officer - |
-
Company
+Example Company Inc.
++
Name: [Company Representative]
+ +Title: Chief Executive Officer
+