\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/lists/nested.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/lists/nested.html
index b2497ae93..a708d5bb3 100644
--- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/lists/nested.html
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/lists/nested.html
@@ -15,8 +15,8 @@
Check List Item 1
-
-
+
+
Check List Item 2
From 0c01ed5874a22dceafe1e58ffffe76198b9f1454 Mon Sep 17 00:00:00 2001
From: Matthew Lipski
Date: Wed, 23 Apr 2025 21:16:15 +0200
Subject: [PATCH 3/6] Fixed tables in `partialBlockToBlockForTesting`
---
.../formatConversionTestUtil.ts | 34 +++++++++++++++++--
1 file changed, 31 insertions(+), 3 deletions(-)
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;
}
From 06b84af76709f16e4435a1474977176f754ab0f5 Mon Sep 17 00:00:00 2001
From: Matthew Lipski
Date: Thu, 24 Apr 2025 11:15:05 +0200
Subject: [PATCH 4/6] Added advanced table test
---
.../exportParseEqualityTestInstances.ts | 111 ++++++++++++++++++
tests/src/unit/core/setupTestEditor.ts | 6 +
2 files changed, 117 insertions(+)
diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts b/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts
index f3bad1bc7..74654d55e 100644
--- a/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts
+++ b/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts
@@ -200,4 +200,115 @@ export const exportParseEqualityTestInstancesHTML: TestInstance<
},
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/setupTestEditor.ts b/tests/src/unit/core/setupTestEditor.ts
index 5df422680..b98eaae56 100644
--- a/tests/src/unit/core/setupTestEditor.ts
+++ b/tests/src/unit/core/setupTestEditor.ts
@@ -36,6 +36,12 @@ export const setupTestEditor = <
},
},
schema,
+ tables: {
+ splitCells: true,
+ cellBackgroundColor: true,
+ cellTextColor: true,
+ headers: true,
+ },
trailingBlock: false,
uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY,
});
From ebe978556cf092c477d1ce1bb10889c1f54b50cf Mon Sep 17 00:00:00 2001
From: Matthew Lipski
Date: Thu, 24 Apr 2025 13:11:29 +0200
Subject: [PATCH 5/6] Added comment
---
.../src/api/exporters/html/util/serializeBlocksExternalHTML.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
index 4c57dcccd..dd0350483 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
@@ -141,6 +141,8 @@ function serializeBlock<
);
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) {
for (const attr of blockContentDataAttributes) {
(ret.dom.firstChild! as HTMLElement).setAttribute(
From 4c38f3c62200ffd86f5c3608c8ccf2a7119598c2 Mon Sep 17 00:00:00 2001
From: Matthew Lipski <50169049+matthewlipski@users.noreply.github.com>
Date: Mon, 28 Apr 2025 11:23:20 +0200
Subject: [PATCH 6/6] feat: Inline style props in external HTML (#1636)
* Made default props on default blocks get rendered to inline styles for lossy HTML
* Updated unit test snapshots
* Implemented PR feedback
* Small fix
---
.../html/util/serializeBlocksExternalHTML.ts | 28 ++
.../core/src/blocks/defaultBlockHelpers.ts | 76 ++-
packages/core/src/blocks/defaultProps.ts | 18 +
packages/core/src/schema/blocks/internal.ts | 3 +-
.../ServerBlockNoteEditor.test.ts.snap | 2 +-
.../text/html/basicBlocksWithProps.html | 8 +-
.../__snapshots__/html/complex/misc.html | 6 +-
.../__snapshots__/html/paragraph/styled.html | 4 +-
.../html/backgroundColorProp.json | 23 +-
.../html/backgroundColorStyle.json | 16 +-
.../__snapshots__/html/textColorProp.json | 23 +-
.../__snapshots__/html/textColorStyle.json | 16 +-
.../parse/__snapshots__/html/twoTables.json | 2 +-
.../parse/parseTestInstances.ts | 464 +++++++++---------
.../html/contextParagraph/basic.html | 2 +-
.../html/customParagraph/basic.html | 2 +-
.../html/customParagraph/lineBreaks.html | 2 +-
.../html/customParagraph/nested.html | 6 +-
.../html/customParagraph/styled.html | 1 +
.../html/simpleCustomParagraph/basic.html | 6 +-
.../html/simpleCustomParagraph/nested.html | 18 +-
.../html/simpleCustomParagraph/styled.html | 1 +
22 files changed, 403 insertions(+), 324 deletions(-)
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
index dd0350483..a39c2d7b9 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
@@ -144,6 +144,19 @@ function serializeBlock<
// 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,
@@ -179,9 +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,
- editor: BlockNoteEditor
+ editor: BlockNoteEditor,
+ external = false
): {
dom: HTMLElement;
contentDOM?: HTMLElement;
@@ -90,6 +94,74 @@ export const defaultBlockToHTML = <
);
}
+ // When exporting to external HTML, we convert from `data-*` attributes to
+ // inline styles properties which can be understood by external applications.
+ //
+ // Note: This is a bit hacky to do this here as we're just hardcoding this for
+ // props on default blocks. We should revisit this when we migrate internal
+ // blocks to use the custom blocks API.
+ if (external) {
+ const dom = renderSpec.dom as HTMLElement;
+
+ if (dom.hasAttribute("data-background-color")) {
+ const backgroundColor = dom.getAttribute("data-background-color")!;
+
+ // If the background color is one of the default colors, we set the
+ // color's hex code from the default theme, as this will look nicer than
+ // using regular CSS colors. For example, instead of
+ // `background-color: red`, we use `background-color: #fbe4e4`.
+ if (backgroundColor in COLORS_DEFAULT) {
+ const cssVariableName =
+ `--blocknote-background-${backgroundColor}` as any;
+
+ dom.style.setProperty(
+ cssVariableName,
+ COLORS_DEFAULT[backgroundColor as keyof typeof COLORS_DEFAULT]
+ .background
+ );
+ dom.style.backgroundColor = `var(${cssVariableName})`;
+ } else {
+ dom.style.backgroundColor = backgroundColor;
+ }
+
+ dom.removeAttribute("data-background-color");
+ }
+
+ if (dom.hasAttribute("data-text-color")) {
+ const textColor = dom.getAttribute("data-text-color")!;
+
+ // If the text color is one of the default colors, we set the color's hex
+ // code from the default theme, as this will look nicer than using regular
+ // CSS colors. For example, instead of `color: red`, we use
+ // `color: #e03e3e`.
+ if (textColor in COLORS_DEFAULT) {
+ const cssVariableName = `--blocknote-text-${textColor}` as any;
+
+ dom.style.setProperty(
+ cssVariableName,
+ COLORS_DEFAULT[textColor as keyof typeof COLORS_DEFAULT].text
+ );
+ dom.style.color = `var(${cssVariableName})`;
+ } else {
+ dom.style.color = textColor;
+ }
+
+ dom.removeAttribute("data-text-color");
+ }
+
+ if (dom.hasAttribute("data-text-alignment")) {
+ dom.style.textAlign = dom.getAttribute("data-text-alignment")!;
+ dom.removeAttribute("data-text-alignment");
+ }
+
+ // We also remove the `data-level` attribute for heading blocks, as this
+ // information can be inferred from whether a `h1`, `h2`, or `h3 tag is
+ // used.
+ if (dom.hasAttribute("data-level")) {
+ dom.removeAttribute("data-level");
+ }
+ }
+
return renderSpec as {
dom: HTMLElement;
contentDOM?: HTMLElement;
diff --git a/packages/core/src/blocks/defaultProps.ts b/packages/core/src/blocks/defaultProps.ts
index a524b19a9..44193cd01 100644
--- a/packages/core/src/blocks/defaultProps.ts
+++ b/packages/core/src/blocks/defaultProps.ts
@@ -30,6 +30,16 @@ const getBackgroundColorAttribute = (
}
if (element.style.backgroundColor) {
+ // Check if `element.style.backgroundColor` matches the string:
+ // `var(--blocknote-background-)`. If it does, return the color
+ // name only. Otherwise, return `element.style.backgroundColor`.
+ const match = element.style.backgroundColor.match(
+ /var\(--blocknote-background-(.+)\)/
+ );
+ if (match) {
+ return match[1];
+ }
+
return element.style.backgroundColor;
}
@@ -54,6 +64,14 @@ const getTextColorAttribute = (attributeName = "textColor"): Attribute => ({
}
if (element.style.color) {
+ // Check if `element.style.color` matches the string:
+ // `var(--blocknote-text-)`. If it does, return the color name
+ // only. Otherwise, return `element.style.color`.
+ const match = element.style.color.match(/var\(--blocknote-text-(.+)\)/);
+ if (match) {
+ return match[1];
+ }
+
return element.style.color;
}
diff --git a/packages/core/src/schema/blocks/internal.ts b/packages/core/src/schema/blocks/internal.ts
index 9669f3db9..bbbb75b44 100644
--- a/packages/core/src/schema/blocks/internal.ts
+++ b/packages/core/src/schema/blocks/internal.ts
@@ -267,7 +267,8 @@ export function createBlockSpecFromStronglyTypedTiptapNode<
node,
requiredExtensions,
toInternalHTML: defaultBlockToHTML,
- toExternalHTML: defaultBlockToHTML,
+ toExternalHTML: (block, editor) =>
+ defaultBlockToHTML(block, editor, true),
// parse: () => undefined, // parse rules are in node already
}
);
diff --git a/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap b/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap
index 7a991c453..c31fa3130 100644
--- a/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap
+++ b/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap
@@ -2,7 +2,7 @@
exports[`Test ServerBlockNoteEditor > converts to HTML (blocksToFullHTML) 1`] = `"
Heading 2
Paragraph
list item
Caption
Example
Caption
"`;
-exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 1`] = `"
\ No newline at end of file
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/basic.html b/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/basic.html
index 2971f1105..ff100b250 100644
--- a/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/basic.html
+++ b/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/basic.html
@@ -1 +1 @@
-
Hello World
\ No newline at end of file
+
Hello World
\ No newline at end of file
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/lineBreaks.html b/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/lineBreaks.html
index 2971f1105..ff100b250 100644
--- a/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/lineBreaks.html
+++ b/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/lineBreaks.html
@@ -1 +1 @@
-
Hello World
\ No newline at end of file
+
Hello World
\ No newline at end of file
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/nested.html b/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/nested.html
index 6b2e1554c..863e2204e 100644
--- a/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/nested.html
+++ b/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/nested.html
@@ -1,3 +1,3 @@
-
Hello World
-
Hello World
-
Hello World
\ No newline at end of file
+
Hello World
+
Hello World
+
Hello World
\ No newline at end of file
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/styled.html b/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/styled.html
index 7e7e60707..962c4cea9 100644
--- a/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/styled.html
+++ b/tests/src/unit/react/formatConversion/export/__snapshots__/html/customParagraph/styled.html
@@ -1,5 +1,6 @@
React Custom Paragraph
\ No newline at end of file
+
React Custom Paragraph
\ No newline at end of file
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/html/simpleCustomParagraph/nested.html b/tests/src/unit/react/formatConversion/export/__snapshots__/html/simpleCustomParagraph/nested.html
index 68e209191..677ec39d3 100644
--- a/tests/src/unit/react/formatConversion/export/__snapshots__/html/simpleCustomParagraph/nested.html
+++ b/tests/src/unit/react/formatConversion/export/__snapshots__/html/simpleCustomParagraph/nested.html
@@ -1,3 +1,15 @@
-
Custom React Paragraph
-
Nested React Custom Paragraph 1
-
Nested React Custom Paragraph 2
\ No newline at end of file
+
Custom React Paragraph
+
Nested React Custom Paragraph 1
+
Nested React Custom Paragraph 2
\ No newline at end of file
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/html/simpleCustomParagraph/styled.html b/tests/src/unit/react/formatConversion/export/__snapshots__/html/simpleCustomParagraph/styled.html
index aff3d3abc..4d0b88207 100644
--- a/tests/src/unit/react/formatConversion/export/__snapshots__/html/simpleCustomParagraph/styled.html
+++ b/tests/src/unit/react/formatConversion/export/__snapshots__/html/simpleCustomParagraph/styled.html
@@ -1,6 +1,7 @@