From 2660ad261bcc33168d5f5dbf81da258c858171f1 Mon Sep 17 00:00:00 2001
From: Matthew Lipski
Date: Mon, 15 Sep 2025 14:05:04 +0200
Subject: [PATCH 1/9] WIP styles API update
---
.../html/util/serializeBlocksInternalHTML.ts | 38 ++++++--
packages/core/src/blocks/defaultBlocks.ts | 77 ++++++++++++++-
packages/core/src/blocks/defaultProps.ts | 24 +----
packages/core/src/editor/Block.css | 18 ++++
packages/core/src/schema/styles/createSpec.ts | 96 +++++++++++++++----
packages/core/src/schema/styles/internal.ts | 7 +-
packages/core/src/schema/styles/types.ts | 22 ++++-
packages/react/src/editor/styles.css | 18 ++++
.../blocknoteHTML/paragraph/styled.html | 8 +-
.../__snapshots__/html/paragraph/styled.html | 28 +++++-
.../parse/parseTestInstances.ts | 4 +-
11 files changed, 278 insertions(+), 62 deletions(-)
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
index bb158ad68c..12e3756a55 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
@@ -48,7 +48,6 @@ export function serializeInlineContentInternalHTML<
// Check if this is a custom inline content node with toExternalHTML
if (
node.type.name !== "text" &&
- node.type.name !== "link" &&
editor.schema.inlineContentSchema[node.type.name]
) {
const inlineContentImplementation =
@@ -90,14 +89,37 @@ export function serializeInlineContentInternalHTML<
continue;
}
}
- }
+ } else if (node.type.name === "text") {
+ // We serialize text nodes manually as we need to serialize the styles/
+ // marks using `styleSpec.implementation.render`. When left up to
+ // ProseMirror, it'll use `toDOM` which is incorrect.
+ let dom: globalThis.Node | Text = document.createTextNode(
+ node.textContent,
+ );
+ for (const mark of node.marks) {
+ if (mark.type.name in editor.schema.styleSpecs) {
+ const newDom = editor.schema.styleSpecs[
+ mark.type.name
+ ].implementation.render(mark.attrs["stringValue"]);
+ newDom.contentDOM!.appendChild(dom);
+ dom = newDom.dom;
+ } else {
+ const domOutputSpec = mark.type.spec.toDOM!(mark, true);
+ const newDom = DOMSerializer.renderSpec(document, domOutputSpec);
+ newDom.contentDOM!.appendChild(dom);
+ dom = newDom.dom;
+ }
+ }
- // Fall back to default serialization for this node
- const nodeFragment = serializer.serializeFragment(
- Fragment.from([node]),
- options,
- );
- fragment.appendChild(nodeFragment);
+ fragment.appendChild(dom);
+ } else {
+ // Fall back to default serialization for this node
+ const nodeFragment = serializer.serializeFragment(
+ Fragment.from([node]),
+ options,
+ );
+ fragment.appendChild(nodeFragment);
+ }
}
return fragment;
diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts
index 916da1a7fe..a2d01b92df 100644
--- a/packages/core/src/blocks/defaultBlocks.ts
+++ b/packages/core/src/blocks/defaultBlocks.ts
@@ -16,9 +16,8 @@ import {
createQuoteBlockSpec,
createToggleListItemBlockSpec,
createVideoBlockSpec,
+ defaultProps,
} from "./index.js";
-import { BackgroundColor } from "../extensions/BackgroundColor/BackgroundColorMark.js";
-import { TextColor } from "../extensions/TextColor/TextColorMark.js";
import {
BlockNoDefaults,
BlockSchema,
@@ -27,11 +26,13 @@ import {
PartialBlockNoDefaults,
StyleSchema,
StyleSpecs,
+ createStyleSpec,
createStyleSpecFromTipTapMark,
getInlineContentSchemaFromSpecs,
getStyleSchemaFromSpecs,
} from "../schema/index.js";
import { createTableBlockSpec } from "./Table/block.js";
+import { COLORS_DEFAULT } from "../editor/defaultColors.js";
export const defaultBlockSpecs = {
audio: createAudioBlockSpec(),
@@ -56,6 +57,78 @@ export type _DefaultBlockSchema = {
};
export type DefaultBlockSchema = _DefaultBlockSchema;
+const TextColor = createStyleSpec(
+ {
+ type: "textColor",
+ propSchema: "string",
+ },
+ {
+ render: () => {
+ const span = document.createElement("span");
+
+ return {
+ dom: span,
+ contentDOM: span,
+ };
+ },
+ toExternalHTML: (value) => {
+ const span = document.createElement("span");
+ if (value !== defaultProps.textColor.default) {
+ span.style.color =
+ value in COLORS_DEFAULT ? COLORS_DEFAULT[value].text : value;
+ }
+
+ return {
+ dom: span,
+ contentDOM: span,
+ };
+ },
+ parse: (element) => {
+ if (element.tagName === "SPAN" && element.style.color) {
+ return element.style.color;
+ }
+
+ return undefined;
+ },
+ },
+);
+
+const BackgroundColor = createStyleSpec(
+ {
+ type: "backgroundColor",
+ propSchema: "string",
+ },
+ {
+ render: () => {
+ const span = document.createElement("span");
+
+ return {
+ dom: span,
+ contentDOM: span,
+ };
+ },
+ toExternalHTML: (value) => {
+ const span = document.createElement("span");
+ if (value !== defaultProps.backgroundColor.default) {
+ span.style.backgroundColor =
+ value in COLORS_DEFAULT ? COLORS_DEFAULT[value].background : value;
+ }
+
+ return {
+ dom: span,
+ contentDOM: span,
+ };
+ },
+ parse: (element) => {
+ if (element.tagName === "SPAN" && element.style.backgroundColor) {
+ return element.style.backgroundColor;
+ }
+
+ return undefined;
+ },
+ },
+);
+
export const defaultStyleSpecs = {
bold: createStyleSpecFromTipTapMark(Bold, "boolean"),
italic: createStyleSpecFromTipTapMark(Italic, "boolean"),
diff --git a/packages/core/src/blocks/defaultProps.ts b/packages/core/src/blocks/defaultProps.ts
index 46c5417a85..5d55d21d35 100644
--- a/packages/core/src/blocks/defaultProps.ts
+++ b/packages/core/src/blocks/defaultProps.ts
@@ -92,20 +92,10 @@ export const getBackgroundColorAttribute = (
default: defaultProps.backgroundColor.default,
parseHTML: (element) => {
if (element.hasAttribute("data-background-color")) {
- return element.getAttribute("data-background-color");
+ return element.getAttribute("data-background-color")!;
}
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;
}
@@ -128,18 +118,10 @@ export const getTextColorAttribute = (
default: defaultProps.textColor.default,
parseHTML: (element) => {
if (element.hasAttribute("data-text-color")) {
- return element.getAttribute("data-text-color");
+ return element.getAttribute("data-text-color")!;
}
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;
}
@@ -149,6 +131,7 @@ export const getTextColorAttribute = (
if (attributes[attributeName] === defaultProps.textColor.default) {
return {};
}
+
return {
"data-text-color": attributes[attributeName],
};
@@ -174,6 +157,7 @@ export const getTextAlignmentAttribute = (
if (attributes[attributeName] === defaultProps.textAlignment.default) {
return {};
}
+
return {
"data-text-alignment": attributes[attributeName],
};
diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css
index 6ed5361621..f882ba0559 100644
--- a/packages/core/src/editor/Block.css
+++ b/packages/core/src/editor/Block.css
@@ -559,92 +559,110 @@ NESTED BLOCKS
/* TODO: should this be here? */
/* TEXT COLORS */
+[data-style-type="textColor"][data-value="gray"],
[data-text-color="gray"],
.bn-block:has(> .bn-block-content[data-text-color="gray"]) {
color: #9b9a97;
}
+[data-style-type="textColor"][data-value="brown"],
[data-text-color="brown"],
.bn-block:has(> .bn-block-content[data-text-color="brown"]) {
color: #64473a;
}
+[data-style-type="textColor"][data-value="red"],
[data-text-color="red"],
.bn-block:has(> .bn-block-content[data-text-color="red"]) {
color: #e03e3e;
}
+[data-style-type="textColor"][data-value="orange"],
[data-text-color="orange"],
.bn-block:has(> .bn-block-content[data-text-color="orange"]) {
color: #d9730d;
}
+[data-style-type="textColor"][data-value="yellow"],
[data-text-color="yellow"],
.bn-block:has(> .bn-block-content[data-text-color="yellow"]) {
color: #dfab01;
}
+[data-style-type="textColor"][data-value="green"],
[data-text-color="green"],
.bn-block:has(> .bn-block-content[data-text-color="green"]) {
color: #4d6461;
}
+[data-style-type="textColor"][data-value="blue"],
[data-text-color="blue"],
.bn-block:has(> .bn-block-content[data-text-color="blue"]) {
color: #0b6e99;
}
+[data-style-type="textColor"][data-value="purple"],
[data-text-color="purple"],
.bn-block:has(> .bn-block-content[data-text-color="purple"]) {
color: #6940a5;
}
+[data-style-type="textColor"][data-value="pink"],
[data-text-color="pink"],
.bn-block:has(> .bn-block-content[data-text-color="pink"]) {
color: #ad1a72;
}
/* BACKGROUND COLORS */
+[data-style-type="backgroundColor"][data-value="gray"],
[data-background-color="gray"],
.bn-block:has(> .bn-block-content[data-background-color="gray"]) {
background-color: #ebeced;
}
+[data-style-type="backgroundColor"][data-value="brown"],
[data-background-color="brown"],
.bn-block:has(> .bn-block-content[data-background-color="brown"]) {
background-color: #e9e5e3;
}
+[data-style-type="backgroundColor"][data-value="red"],
[data-background-color="red"],
.bn-block:has(> .bn-block-content[data-background-color="red"]) {
background-color: #fbe4e4;
}
+[data-style-type="backgroundColor"][data-value="orange"],
[data-background-color="orange"],
.bn-block:has(> .bn-block-content[data-background-color="orange"]) {
background-color: #f6e9d9;
}
+[data-style-type="backgroundColor"][data-value="yellow"],
[data-background-color="yellow"],
.bn-block:has(> .bn-block-content[data-background-color="yellow"]) {
background-color: #fbf3db;
}
+[data-style-type="backgroundColor"][data-value="green"],
[data-background-color="green"],
.bn-block:has(> .bn-block-content[data-background-color="green"]) {
background-color: #ddedea;
}
+[data-style-type="backgroundColor"][data-value="blue"],
[data-background-color="blue"],
.bn-block:has(> .bn-block-content[data-background-color="blue"]) {
background-color: #ddebf1;
}
+[data-style-type="backgroundColor"][data-value="purple"],
[data-background-color="purple"],
.bn-block:has(> .bn-block-content[data-background-color="purple"]) {
background-color: #eae4f2;
}
+[data-style-type="backgroundColor"][data-value="pink"],
[data-background-color="pink"],
.bn-block:has(> .bn-block-content[data-background-color="pink"]) {
background-color: #f4dfeb;
diff --git a/packages/core/src/schema/styles/createSpec.ts b/packages/core/src/schema/styles/createSpec.ts
index 8051f9f833..ad73d77169 100644
--- a/packages/core/src/schema/styles/createSpec.ts
+++ b/packages/core/src/schema/styles/createSpec.ts
@@ -1,7 +1,6 @@
import { Mark } from "@tiptap/core";
-import { ParseRule } from "@tiptap/pm/model";
-import { UnreachableCaseError } from "../../util/typescript.js";
+import { ParseRule, TagParseRule } from "@tiptap/pm/model";
import {
addStyleAttributes,
createInternalStyleSpec,
@@ -19,12 +18,25 @@ export type CustomStyleImplementation = {
dom: HTMLElement;
contentDOM?: HTMLElement;
};
+ toExternalHTML?: T["propSchema"] extends "boolean"
+ ? () => {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ }
+ : (value: string) => {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
+ parse?: T["propSchema"] extends "boolean"
+ ? (element: HTMLElement) => string | undefined
+ : (element: HTMLElement) => true | undefined;
};
-// TODO: support serialization
-
-export function getStyleParseRules(config: StyleConfig): ParseRule[] {
- return [
+export function getStyleParseRules(
+ config: T,
+ customParseFunction?: CustomStyleImplementation["parse"],
+): ParseRule[] {
+ const rules: TagParseRule[] = [
{
tag: `[data-style-type="${config.type}"]`,
contentElement: (element) => {
@@ -38,6 +50,26 @@ export function getStyleParseRules(config: StyleConfig): ParseRule[] {
},
},
];
+
+ if (customParseFunction) {
+ rules.push({
+ tag: "*",
+ getAttrs(node: string | HTMLElement) {
+ if (typeof node === "string") {
+ return false;
+ }
+
+ const stringValue = customParseFunction?.(node);
+
+ if (stringValue === undefined) {
+ return false;
+ }
+
+ return { stringValue };
+ },
+ });
+ }
+ return rules;
}
export function createStyleSpec(
@@ -52,22 +84,13 @@ export function createStyleSpec(
},
parseHTML() {
- return getStyleParseRules(styleConfig);
+ return getStyleParseRules(styleConfig, styleImplementation.parse);
},
renderHTML({ mark }) {
- let renderResult: {
- dom: HTMLElement;
- contentDOM?: HTMLElement;
- };
-
- if (styleConfig.propSchema === "boolean") {
- renderResult = styleImplementation.render(mark.attrs.stringValue);
- } else if (styleConfig.propSchema === "string") {
- renderResult = styleImplementation.render(mark.attrs.stringValue);
- } else {
- throw new UnreachableCaseError(styleConfig.propSchema);
- }
+ const renderResult = (
+ styleImplementation.toExternalHTML || styleImplementation.render
+ )(mark.attrs.stringValue);
return addStyleAttributes(
renderResult,
@@ -76,9 +99,44 @@ export function createStyleSpec(
styleConfig.propSchema,
);
},
+
+ addMarkView() {
+ return ({ mark }) => {
+ const renderResult = styleImplementation.render(mark.attrs.stringValue);
+
+ return addStyleAttributes(
+ renderResult,
+ styleConfig.type,
+ mark.attrs.stringValue,
+ styleConfig.propSchema,
+ );
+ };
+ },
});
return createInternalStyleSpec(styleConfig, {
mark,
+ render: (value) => {
+ const renderResult = styleImplementation.render(value);
+
+ return addStyleAttributes(
+ renderResult,
+ styleConfig.type,
+ value,
+ styleConfig.propSchema,
+ );
+ },
+ toExternalHTML: (value) => {
+ const renderResult = (
+ styleImplementation.toExternalHTML || styleImplementation.render
+ )(value);
+
+ return addStyleAttributes(
+ renderResult,
+ styleConfig.type,
+ value,
+ styleConfig.propSchema,
+ );
+ },
});
}
diff --git a/packages/core/src/schema/styles/internal.ts b/packages/core/src/schema/styles/internal.ts
index 0446db701b..289fb58102 100644
--- a/packages/core/src/schema/styles/internal.ts
+++ b/packages/core/src/schema/styles/internal.ts
@@ -66,7 +66,7 @@ export function addStyleAttributes<
// config and implementation that conform to the type of Config
export function createInternalStyleSpec(
config: T,
- implementation: StyleImplementation,
+ implementation: StyleImplementation,
) {
return {
config,
@@ -85,6 +85,11 @@ export function createStyleSpecFromTipTapMark<
},
{
mark,
+ render: () =>
+ mark.config.renderHTML!({ mark, HTMLAttributes: {} }) as {
+ dom: HTMLElement;
+ contentDOM: HTMLElement;
+ },
},
);
}
diff --git a/packages/core/src/schema/styles/types.ts b/packages/core/src/schema/styles/types.ts
index 1a44e9ffd3..c817dcefa8 100644
--- a/packages/core/src/schema/styles/types.ts
+++ b/packages/core/src/schema/styles/types.ts
@@ -11,15 +11,33 @@ export type StyleConfig = {
// StyleImplementation contains the "implementation" info about a Style element.
// Currently, the implementation is always a TipTap Mark
-export type StyleImplementation = {
+export type StyleImplementation = {
mark: Mark;
+ render: T["propSchema"] extends "boolean"
+ ? () => {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ }
+ : (value: string) => {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
+ toExternalHTML?: T["propSchema"] extends "boolean"
+ ? () => {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ }
+ : (value: string) => {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
};
// Container for both the config and implementation of a Style,
// and the type of `implementation` is based on that of the config
export type StyleSpec = {
config: T;
- implementation: StyleImplementation;
+ implementation: StyleImplementation;
};
// A Schema contains all the types (Configs) supported in an editor
diff --git a/packages/react/src/editor/styles.css b/packages/react/src/editor/styles.css
index 37044b5df6..eb61a59fa2 100644
--- a/packages/react/src/editor/styles.css
+++ b/packages/react/src/editor/styles.css
@@ -140,74 +140,92 @@
}
/* Highlight color styling */
+[data-style-type="textColor"][data-value="gray"],
[data-text-color="gray"] {
color: var(--bn-colors-highlights-gray-text);
}
+[data-style-type="textColor"][data-value="brown"],
[data-text-color="brown"] {
color: var(--bn-colors-highlights-brown-text);
}
+[data-style-type="textColor"][data-value="red"],
[data-text-color="red"] {
color: var(--bn-colors-highlights-red-text);
}
+[data-style-type="textColor"][data-value="orange"],
[data-text-color="orange"] {
color: var(--bn-colors-highlights-orange-text);
}
+[data-style-type="textColor"][data-value="yellow"],
[data-text-color="yellow"] {
color: var(--bn-colors-highlights-yellow-text);
}
+[data-style-type="textColor"][data-value="green"],
[data-text-color="green"] {
color: var(--bn-colors-highlights-green-text);
}
+[data-style-type="textColor"][data-value="blue"],
[data-text-color="blue"] {
color: var(--bn-colors-highlights-blue-text);
}
+[data-style-type="textColor"][data-value="purple"],
[data-text-color="purple"] {
color: var(--bn-colors-highlights-purple-text);
}
+[data-style-type="textColor"][data-value="pink"],
[data-text-color="pink"] {
color: var(--bn-colors-highlights-pink-text);
}
+[data-style-type="backgroundColor"][data-value="gray"],
[data-background-color="gray"] {
background-color: var(--bn-colors-highlights-gray-background);
}
+[data-style-type="backgroundColor"][data-value="brown"],
[data-background-color="brown"] {
background-color: var(--bn-colors-highlights-brown-background);
}
+[data-style-type="backgroundColor"][data-value="red"],
[data-background-color="red"] {
background-color: var(--bn-colors-highlights-red-background);
}
+[data-style-type="backgroundColor"][data-value="orange"],
[data-background-color="orange"] {
background-color: var(--bn-colors-highlights-orange-background);
}
+[data-style-type="backgroundColor"][data-value="yellow"],
[data-background-color="yellow"] {
background-color: var(--bn-colors-highlights-yellow-background);
}
+[data-style-type="backgroundColor"][data-value="green"],
[data-background-color="green"] {
background-color: var(--bn-colors-highlights-green-background);
}
+[data-style-type="backgroundColor"][data-value="blue"],
[data-background-color="blue"] {
background-color: var(--bn-colors-highlights-blue-background);
}
+[data-style-type="backgroundColor"][data-value="purple"],
[data-background-color="purple"] {
background-color: var(--bn-colors-highlights-purple-background);
}
+[data-style-type="backgroundColor"][data-value="pink"],
[data-background-color="pink"] {
background-color: var(--bn-colors-highlights-pink-background);
}
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/styled.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/styled.html
index 699ad7f460..27f47f78b7 100644
--- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/styled.html
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/styled.html
@@ -10,10 +10,10 @@
>
Plain
- Red Text
- Blue Background
-
- Mixed Colors
+ Red Text
+ Blue Background
+
+ Mixed Colors
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/styled.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/styled.html
index fd10eacf1a..637d51b2e0 100644
--- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/styled.html
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/styled.html
@@ -5,9 +5,29 @@
data-background-color="pink"
>
Plain
- Red Text
- Blue Background
-
- Mixed Colors
+ Red Text
+ Blue Background
+
+ Mixed Colors
\ 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 0c89c74541..7a6781b938 100644
--- a/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
+++ b/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
@@ -870,14 +870,14 @@ With Hard Break
{
testCase: {
name: "textColorStyle",
- content: `Blue Text Blue Text
`,
+ content: `Blue Text Blue Text
`,
},
executeTest: testParseHTML,
},
{
testCase: {
name: "backgroundColorStyle",
- content: `Blue Background Blue Background
`,
+ content: `Blue Background Blue Background
`,
},
executeTest: testParseHTML,
},
From d5611a9ed301c73ccd6e19d93848ddfec62def8e Mon Sep 17 00:00:00 2001
From: Nick the Sick
Date: Mon, 15 Sep 2025 15:07:55 +0200
Subject: [PATCH 2/9] refactor: better typing
---
packages/core/src/schema/styles/createSpec.ts | 40 ++++++++-----------
packages/core/src/schema/styles/types.ts | 28 +++++--------
2 files changed, 26 insertions(+), 42 deletions(-)
diff --git a/packages/core/src/schema/styles/createSpec.ts b/packages/core/src/schema/styles/createSpec.ts
index ad73d77169..4bd83b3ae0 100644
--- a/packages/core/src/schema/styles/createSpec.ts
+++ b/packages/core/src/schema/styles/createSpec.ts
@@ -9,27 +9,19 @@ import {
import { StyleConfig, StyleSpec } from "./types.js";
export type CustomStyleImplementation = {
- render: T["propSchema"] extends "boolean"
- ? () => {
- dom: HTMLElement;
- contentDOM?: HTMLElement;
- }
- : (value: string) => {
- dom: HTMLElement;
- contentDOM?: HTMLElement;
- };
- toExternalHTML?: T["propSchema"] extends "boolean"
- ? () => {
- dom: HTMLElement;
- contentDOM?: HTMLElement;
- }
- : (value: string) => {
- dom: HTMLElement;
- contentDOM?: HTMLElement;
- };
- parse?: T["propSchema"] extends "boolean"
- ? (element: HTMLElement) => string | undefined
- : (element: HTMLElement) => true | undefined;
+ render: (value: T["propSchema"] extends "boolean" ? undefined : string) => {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
+ toExternalHTML?: (
+ value: T["propSchema"] extends "boolean" ? undefined : string,
+ ) => {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
+ parse?: (
+ element: HTMLElement,
+ ) => (T["propSchema"] extends "boolean" ? true : string) | undefined;
};
export function getStyleParseRules(
@@ -72,7 +64,7 @@ export function getStyleParseRules(
return rules;
}
-export function createStyleSpec(
+export function createStyleSpec(
styleConfig: T,
styleImplementation: CustomStyleImplementation,
): StyleSpec {
@@ -117,7 +109,7 @@ export function createStyleSpec(
return createInternalStyleSpec(styleConfig, {
mark,
render: (value) => {
- const renderResult = styleImplementation.render(value);
+ const renderResult = styleImplementation.render(value as any);
return addStyleAttributes(
renderResult,
@@ -129,7 +121,7 @@ export function createStyleSpec(
toExternalHTML: (value) => {
const renderResult = (
styleImplementation.toExternalHTML || styleImplementation.render
- )(value);
+ )(value as any);
return addStyleAttributes(
renderResult,
diff --git a/packages/core/src/schema/styles/types.ts b/packages/core/src/schema/styles/types.ts
index c817dcefa8..93aa192d82 100644
--- a/packages/core/src/schema/styles/types.ts
+++ b/packages/core/src/schema/styles/types.ts
@@ -13,24 +13,16 @@ export type StyleConfig = {
// Currently, the implementation is always a TipTap Mark
export type StyleImplementation = {
mark: Mark;
- render: T["propSchema"] extends "boolean"
- ? () => {
- dom: HTMLElement;
- contentDOM?: HTMLElement;
- }
- : (value: string) => {
- dom: HTMLElement;
- contentDOM?: HTMLElement;
- };
- toExternalHTML?: T["propSchema"] extends "boolean"
- ? () => {
- dom: HTMLElement;
- contentDOM?: HTMLElement;
- }
- : (value: string) => {
- dom: HTMLElement;
- contentDOM?: HTMLElement;
- };
+ render: (value: T["propSchema"] extends "boolean" ? undefined : string) => {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
+ toExternalHTML?: (
+ value: T["propSchema"] extends "boolean" ? undefined : string,
+ ) => {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
};
// Container for both the config and implementation of a Style,
From 433837550cd4e5b47f27a13a922eb699693c54d6 Mon Sep 17 00:00:00 2001
From: Nick the Sick
Date: Mon, 15 Sep 2025 16:13:40 +0200
Subject: [PATCH 3/9] fix: pass the editor instance & wire it up
---
.../html/util/serializeBlocksInternalHTML.ts | 2 +-
packages/core/src/schema/styles/internal.ts | 32 ++++++++++++++++---
packages/core/src/schema/styles/types.ts | 7 +++-
3 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
index 12e3756a55..48afe36337 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
@@ -100,7 +100,7 @@ export function serializeInlineContentInternalHTML<
if (mark.type.name in editor.schema.styleSpecs) {
const newDom = editor.schema.styleSpecs[
mark.type.name
- ].implementation.render(mark.attrs["stringValue"]);
+ ].implementation.render(mark.attrs["stringValue"], editor);
newDom.contentDOM!.appendChild(dom);
dom = newDom.dom;
} else {
diff --git a/packages/core/src/schema/styles/internal.ts b/packages/core/src/schema/styles/internal.ts
index 289fb58102..10b5e3a860 100644
--- a/packages/core/src/schema/styles/internal.ts
+++ b/packages/core/src/schema/styles/internal.ts
@@ -85,11 +85,35 @@ export function createStyleSpecFromTipTapMark<
},
{
mark,
- render: () =>
- mark.config.renderHTML!({ mark, HTMLAttributes: {} }) as {
+ render(value, editor) {
+ const toDOM = editor.pmSchema.marks[mark.name].spec.toDOM;
+
+ if (toDOM === undefined) {
+ throw new Error(
+ "This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`.",
+ );
+ }
+
+ const markInstance = editor.pmSchema.mark(mark.name, {
+ stringValue: value,
+ });
+
+ const renderSpec = toDOM(markInstance, true);
+
+ if (typeof renderSpec !== "object" || !("dom" in renderSpec)) {
+ throw new Error(
+ "Cannot use this block's default HTML serialization as its corresponding TipTap node's `renderHTML` function does not return an object with the `dom` property.",
+ );
+ }
+
+ return renderSpec as {
dom: HTMLElement;
- contentDOM: HTMLElement;
- },
+ contentDOM?: HTMLElement;
+ };
+ },
+ toExternalHTML(value, editor) {
+ return this.render(value, editor);
+ },
},
);
}
diff --git a/packages/core/src/schema/styles/types.ts b/packages/core/src/schema/styles/types.ts
index 93aa192d82..3be7cb9d6e 100644
--- a/packages/core/src/schema/styles/types.ts
+++ b/packages/core/src/schema/styles/types.ts
@@ -1,4 +1,5 @@
import { Mark } from "@tiptap/core";
+import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
export type StylePropSchema = "boolean" | "string"; // TODO: use PropSchema as name? Use objects as type similar to blocks?
@@ -13,12 +14,16 @@ export type StyleConfig = {
// Currently, the implementation is always a TipTap Mark
export type StyleImplementation = {
mark: Mark;
- render: (value: T["propSchema"] extends "boolean" ? undefined : string) => {
+ render: (
+ value: T["propSchema"] extends "boolean" ? undefined : string,
+ editor: BlockNoteEditor,
+ ) => {
dom: HTMLElement;
contentDOM?: HTMLElement;
};
toExternalHTML?: (
value: T["propSchema"] extends "boolean" ? undefined : string,
+ editor: BlockNoteEditor,
) => {
dom: HTMLElement;
contentDOM?: HTMLElement;
From 4a02f4614867eb7fb50a9a629d1d6bdf13e77589 Mon Sep 17 00:00:00 2001
From: Nick the Sick
Date: Mon, 15 Sep 2025 16:14:12 +0200
Subject: [PATCH 4/9] fix: serialize to external HTML
---
.../html/util/serializeBlocksExternalHTML.ts | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
index ef1eefb176..4b52effebc 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
@@ -93,6 +93,29 @@ export function serializeInlineContentExternalHTML<
continue;
}
}
+ } else if (node.type.name === "text") {
+ // We serialize text nodes manually as we need to serialize the styles/
+ // marks using `styleSpec.implementation.render`. When left up to
+ // ProseMirror, it'll use `toDOM` which is incorrect.
+ let dom: globalThis.Node | Text = document.createTextNode(
+ node.textContent,
+ );
+ for (const mark of node.marks) {
+ if (mark.type.name in editor.schema.styleSpecs) {
+ const newDom = editor.schema.styleSpecs[
+ mark.type.name
+ ].implementation.render(mark.attrs["stringValue"], editor);
+ newDom.contentDOM!.appendChild(dom);
+ dom = newDom.dom;
+ } else {
+ const domOutputSpec = mark.type.spec.toDOM!(mark, true);
+ const newDom = DOMSerializer.renderSpec(document, domOutputSpec);
+ newDom.contentDOM!.appendChild(dom);
+ dom = newDom.dom;
+ }
+ }
+
+ fragment.appendChild(dom);
}
// Fall back to default serialization for this node
From 8e0db667d01041ce1904575a7b9b214215c6c3e4 Mon Sep 17 00:00:00 2001
From: Nick the Sick
Date: Mon, 15 Sep 2025 16:16:16 +0200
Subject: [PATCH 5/9] feat: support react style specs
---
packages/react/src/schema/ReactStyleSpec.tsx | 50 ++++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/packages/react/src/schema/ReactStyleSpec.tsx b/packages/react/src/schema/ReactStyleSpec.tsx
index 53b9c85eba..b83f727319 100644
--- a/packages/react/src/schema/ReactStyleSpec.tsx
+++ b/packages/react/src/schema/ReactStyleSpec.tsx
@@ -117,5 +117,55 @@ export function createReactStyleSpec(
return createInternalStyleSpec(styleConfig, {
mark,
+ render(value, editor) {
+ const Content = styleImplementation.render;
+ const output = renderToDOMSpec(
+ (ref) => (
+ {
+ ref(element);
+ if (element) {
+ element.dataset.editable = "";
+ }
+ }}
+ />
+ ),
+ editor,
+ );
+
+ return addStyleAttributes(
+ output,
+ styleConfig.type,
+ value,
+ styleConfig.propSchema,
+ );
+ },
+ toExternalHTML(value, editor) {
+ const Content = styleImplementation.render;
+ const output = renderToDOMSpec(
+ (ref) => (
+ {
+ ref(element);
+ if (element) {
+ element.dataset.editable = "";
+ }
+ }}
+ />
+ ),
+ editor,
+ );
+
+ return addStyleAttributes(
+ output,
+ styleConfig.type,
+ value,
+ styleConfig.propSchema,
+ );
+ },
});
}
From b459270b76076e4e654dd918b375c3be52ab5cc9 Mon Sep 17 00:00:00 2001
From: Nick the Sick
Date: Mon, 15 Sep 2025 17:12:57 +0200
Subject: [PATCH 6/9] fix: use toExternalHTML
---
.../exporters/html/util/serializeBlocksExternalHTML.ts | 8 +++++---
packages/core/src/schema/styles/internal.ts | 2 +-
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
index 4b52effebc..16c8d7794a 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
@@ -102,9 +102,11 @@ export function serializeInlineContentExternalHTML<
);
for (const mark of node.marks) {
if (mark.type.name in editor.schema.styleSpecs) {
- const newDom = editor.schema.styleSpecs[
- mark.type.name
- ].implementation.render(mark.attrs["stringValue"], editor);
+ const newDom = (
+ editor.schema.styleSpecs[mark.type.name].implementation
+ .toExternalHTML ??
+ editor.schema.styleSpecs[mark.type.name].implementation.render
+ )(mark.attrs["stringValue"], editor);
newDom.contentDOM!.appendChild(dom);
dom = newDom.dom;
} else {
diff --git a/packages/core/src/schema/styles/internal.ts b/packages/core/src/schema/styles/internal.ts
index 10b5e3a860..e2c53f082d 100644
--- a/packages/core/src/schema/styles/internal.ts
+++ b/packages/core/src/schema/styles/internal.ts
@@ -102,7 +102,7 @@ export function createStyleSpecFromTipTapMark<
if (typeof renderSpec !== "object" || !("dom" in renderSpec)) {
throw new Error(
- "Cannot use this block's default HTML serialization as its corresponding TipTap node's `renderHTML` function does not return an object with the `dom` property.",
+ "Cannot use this block's default HTML serialization as its corresponding TipTap mark's `renderHTML` function does not return an object with the `dom` property.",
);
}
From e4fc5c5d7888527b06e8287baad866402935f8e5 Mon Sep 17 00:00:00 2001
From: Matthew Lipski
Date: Tue, 16 Sep 2025 10:36:33 +0200
Subject: [PATCH 7/9] Fixed unit tests
---
.../html/util/serializeBlocksExternalHTML.ts | 46 +++++++++++++------
.../html/util/serializeBlocksInternalHTML.ts | 3 +-
packages/core/src/schema/styles/internal.ts | 34 +++++++++++++-
.../blocknoteHTML/paragraph/styled.html | 4 +-
.../blocknoteHTML/customParagraph/styled.html | 8 ++--
.../simpleCustomParagraph/styled.html | 8 ++--
.../html/simpleCustomParagraph/styled.html | 28 +++++++++--
7 files changed, 99 insertions(+), 32 deletions(-)
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
index 16c8d7794a..17ab49fe69 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
@@ -60,11 +60,14 @@ export function serializeInlineContentExternalHTML<
for (const node of nodes) {
// Check if this is a custom inline content node with toExternalHTML
- if (editor.schema.inlineContentSchema[node.type.name]) {
+ if (
+ node.type.name !== "text" &&
+ editor.schema.inlineContentSchema[node.type.name]
+ ) {
const inlineContentImplementation =
editor.schema.inlineContentSpecs[node.type.name].implementation;
- if (inlineContentImplementation?.toExternalHTML) {
+ if (inlineContentImplementation) {
// Convert the node to inline content format
const inlineContent = nodeToCustomInlineContent(
node,
@@ -72,11 +75,23 @@ export function serializeInlineContentExternalHTML<
editor.schema.styleSchema,
);
- // Use the custom toExternalHTML method
- const output = inlineContentImplementation.toExternalHTML(
- inlineContent as any,
- editor as any,
- );
+ // Use the custom toExternalHTML method or fallback to `render`
+ const output = inlineContentImplementation.toExternalHTML
+ ? inlineContentImplementation.toExternalHTML(
+ inlineContent as any,
+ editor as any,
+ )
+ : inlineContentImplementation.render.call(
+ {
+ renderType: "dom",
+ props: undefined,
+ },
+ inlineContent as any,
+ () => {
+ // No-op
+ },
+ editor as any,
+ );
if (output) {
fragment.appendChild(output.dom);
@@ -100,7 +115,8 @@ export function serializeInlineContentExternalHTML<
let dom: globalThis.Node | Text = document.createTextNode(
node.textContent,
);
- for (const mark of node.marks) {
+ // Reverse the order of marks to maintain the correct priority.
+ for (const mark of node.marks.toReversed()) {
if (mark.type.name in editor.schema.styleSpecs) {
const newDom = (
editor.schema.styleSpecs[mark.type.name].implementation
@@ -118,14 +134,14 @@ export function serializeInlineContentExternalHTML<
}
fragment.appendChild(dom);
+ } else {
+ // Fall back to default serialization for this node
+ const nodeFragment = serializer.serializeFragment(
+ Fragment.from([node]),
+ options,
+ );
+ fragment.appendChild(nodeFragment);
}
-
- // Fall back to default serialization for this node
- const nodeFragment = serializer.serializeFragment(
- Fragment.from([node]),
- options,
- );
- fragment.appendChild(nodeFragment);
}
if (
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
index 48afe36337..bc39d70008 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
@@ -96,7 +96,8 @@ export function serializeInlineContentInternalHTML<
let dom: globalThis.Node | Text = document.createTextNode(
node.textContent,
);
- for (const mark of node.marks) {
+ // Reverse the order of marks to maintain the correct priority.
+ for (const mark of node.marks.toReversed()) {
if (mark.type.name in editor.schema.styleSpecs) {
const newDom = editor.schema.styleSpecs[
mark.type.name
diff --git a/packages/core/src/schema/styles/internal.ts b/packages/core/src/schema/styles/internal.ts
index e2c53f082d..9d5711bd6e 100644
--- a/packages/core/src/schema/styles/internal.ts
+++ b/packages/core/src/schema/styles/internal.ts
@@ -1,4 +1,5 @@
import { Attributes, Mark } from "@tiptap/core";
+import { DOMSerializer } from "@tiptap/pm/model";
import {
StyleConfig,
StyleImplementation,
@@ -98,7 +99,10 @@ export function createStyleSpecFromTipTapMark<
stringValue: value,
});
- const renderSpec = toDOM(markInstance, true);
+ const renderSpec = DOMSerializer.renderSpec(
+ document,
+ toDOM(markInstance, true),
+ );
if (typeof renderSpec !== "object" || !("dom" in renderSpec)) {
throw new Error(
@@ -112,7 +116,33 @@ export function createStyleSpecFromTipTapMark<
};
},
toExternalHTML(value, editor) {
- return this.render(value, editor);
+ const toDOM = editor.pmSchema.marks[mark.name].spec.toDOM;
+
+ if (toDOM === undefined) {
+ throw new Error(
+ "This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`.",
+ );
+ }
+
+ const markInstance = editor.pmSchema.mark(mark.name, {
+ stringValue: value,
+ });
+
+ const renderSpec = DOMSerializer.renderSpec(
+ document,
+ toDOM(markInstance, true),
+ );
+
+ if (typeof renderSpec !== "object" || !("dom" in renderSpec)) {
+ throw new Error(
+ "Cannot use this block's default HTML serialization as its corresponding TipTap mark's `renderHTML` function does not return an object with the `dom` property.",
+ );
+ }
+
+ return renderSpec as {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
},
},
);
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/styled.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/styled.html
index 27f47f78b7..b0e77ba748 100644
--- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/styled.html
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/styled.html
@@ -12,8 +12,8 @@
Plain
Red Text
Blue Background
-
- Mixed Colors
+
+ Mixed Colors
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/customParagraph/styled.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/customParagraph/styled.html
index 654ebfd316..df960b11f1 100644
--- a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/customParagraph/styled.html
+++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/customParagraph/styled.html
@@ -12,10 +12,10 @@
>
Plain
- Red Text
- Blue Background
-
- Mixed Colors
+ Red Text
+ Blue Background
+
+ Mixed Colors
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/simpleCustomParagraph/styled.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/simpleCustomParagraph/styled.html
index 6a0a83a4cb..ef3d75ab28 100644
--- a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/simpleCustomParagraph/styled.html
+++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/simpleCustomParagraph/styled.html
@@ -12,10 +12,10 @@
>
Plain
- Red Text
- Blue Background
-
- Mixed Colors
+ Red Text
+ Blue Background
+
+ Mixed Colors
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 c2a35ebaee..87efb71bf5 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
@@ -5,9 +5,29 @@
data-background-color="pink"
>
Plain
- Red Text
- Blue Background
-
- Mixed Colors
+ Red Text
+ Blue Background
+
+ Mixed Colors
\ No newline at end of file
From 53defdd7aca225c94d997553c4569c93586b6d5b Mon Sep 17 00:00:00 2001
From: Matthew Lipski
Date: Tue, 16 Sep 2025 10:50:01 +0200
Subject: [PATCH 8/9] Removed AI package TODO
---
.../formats/html-blocks/htmlBlocks.test.ts | 24 +++++++------------
.../src/api/formats/tests/sharedTestCases.ts | 2 --
2 files changed, 9 insertions(+), 17 deletions(-)
diff --git a/packages/xl-ai/src/api/formats/html-blocks/htmlBlocks.test.ts b/packages/xl-ai/src/api/formats/html-blocks/htmlBlocks.test.ts
index aafb57b6ad..20e7914c69 100644
--- a/packages/xl-ai/src/api/formats/html-blocks/htmlBlocks.test.ts
+++ b/packages/xl-ai/src/api/formats/html-blocks/htmlBlocks.test.ts
@@ -120,21 +120,15 @@ describe("Models", () => {
describe(`${params.model.provider}/${params.model.modelId} (${
params.stream ? "streaming" : "non-streaming"
})`, () => {
- generateSharedTestCases(
- (editor, options) =>
- doLLMRequest(editor, {
- ...options,
- dataFormat: htmlBlockLLMFormat,
- model: params.model,
- maxRetries: 0,
- stream: params.stream,
- withDelays: false,
- }),
- // TODO: remove when matthew's parsing PR is merged
- {
- textAlignment: true,
- blockColor: true,
- },
+ generateSharedTestCases((editor, options) =>
+ doLLMRequest(editor, {
+ ...options,
+ dataFormat: htmlBlockLLMFormat,
+ model: params.model,
+ maxRetries: 0,
+ stream: params.stream,
+ withDelays: false,
+ }),
);
});
}
diff --git a/packages/xl-ai/src/api/formats/tests/sharedTestCases.ts b/packages/xl-ai/src/api/formats/tests/sharedTestCases.ts
index a2026ff442..47bca14b01 100644
--- a/packages/xl-ai/src/api/formats/tests/sharedTestCases.ts
+++ b/packages/xl-ai/src/api/formats/tests/sharedTestCases.ts
@@ -38,8 +38,6 @@ export function generateSharedTestCases(
) => Promise,
skipTestsRequiringCapabilities?: {
mentions?: boolean;
- textAlignment?: boolean;
- blockColor?: boolean;
},
) {
function skipIfUnsupported(
From 22d06aa0910bd534e437f621acfef732402bf2f3 Mon Sep 17 00:00:00 2001
From: Matthew Lipski
Date: Tue, 16 Sep 2025 10:50:59 +0200
Subject: [PATCH 9/9] Small fix
---
packages/xl-ai/src/api/formats/tests/sharedTestCases.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packages/xl-ai/src/api/formats/tests/sharedTestCases.ts b/packages/xl-ai/src/api/formats/tests/sharedTestCases.ts
index 47bca14b01..a2026ff442 100644
--- a/packages/xl-ai/src/api/formats/tests/sharedTestCases.ts
+++ b/packages/xl-ai/src/api/formats/tests/sharedTestCases.ts
@@ -38,6 +38,8 @@ export function generateSharedTestCases(
) => Promise,
skipTestsRequiringCapabilities?: {
mentions?: boolean;
+ textAlignment?: boolean;
+ blockColor?: boolean;
},
) {
function skipIfUnsupported(