Skip to content

Commit 4c38f3c

Browse files
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
1 parent ebe9785 commit 4c38f3c

File tree

22 files changed

+403
-324
lines changed

22 files changed

+403
-324
lines changed

packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts

+28
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@ function serializeBlock<
144144
// We wrap the output in an `li` element for list items, and so we want to
145145
// add the attributes to that element instead as it is the "root".
146146
if (!listType) {
147+
// Copies the styles and prop-related attributes from the `blockContent`
148+
// element onto its first child, as the `blockContent` element is omitted
149+
// from external HTML. This is so prop data is preserved via `data-*`
150+
// attributes or inline styles.
151+
//
152+
// The styles are specifically for default props on default blocks, as
153+
// they get converted from `data-*` attributes for external HTML. Will
154+
// need to revisit this when we convert default blocks to use the custom
155+
// block API.
156+
const style = ret.dom.getAttribute("style");
157+
if (style) {
158+
(ret.dom.firstChild! as HTMLElement).setAttribute("style", style);
159+
}
147160
for (const attr of blockContentDataAttributes) {
148161
(ret.dom.firstChild! as HTMLElement).setAttribute(
149162
attr.name,
@@ -179,9 +192,24 @@ function serializeBlock<
179192
fragment.append(list);
180193
}
181194
const li = doc.createElement("li");
195+
196+
// Copies the styles and prop-related attributes from the `blockContent`
197+
// element onto its first child, as the `blockContent` element is omitted
198+
// from external HTML. This is so prop data is preserved via `data-*`
199+
// attributes or inline styles.
200+
//
201+
// The styles are specifically for default props on default blocks, as
202+
// they get converted from `data-*` attributes for external HTML. Will
203+
// need to revisit this when we convert default blocks to use the custom
204+
// block API.
205+
const style = ret.dom.getAttribute("style");
206+
if (style) {
207+
li.setAttribute("style", style);
208+
}
182209
for (const attr of blockContentDataAttributes) {
183210
li.setAttribute(attr.name, attr.value);
184211
}
212+
185213
li.append(elementFragment);
186214
fragment.lastChild!.appendChild(li);
187215
} else {

packages/core/src/blocks/defaultBlockHelpers.ts

+74-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { blockToNode } from "../api/nodeConversions/blockToNode.js";
22
import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
3+
import { COLORS_DEFAULT } from "../editor/defaultColors.js";
34
import type {
45
BlockNoDefaults,
56
BlockSchema,
@@ -55,14 +56,17 @@ export function createDefaultBlockDOMOutputSpec(
5556

5657
// Function used to convert default blocks to HTML. It uses the corresponding
5758
// node's `renderHTML` method to do the conversion by using a default
58-
// `DOMSerializer`.
59+
// `DOMSerializer`. The `external` flag is used to modify the resulting HTML for
60+
// external use. This just involves changing props being rendered from `data-*`
61+
// attributes to inline styles.
5962
export const defaultBlockToHTML = <
6063
BSchema extends BlockSchema,
6164
I extends InlineContentSchema,
6265
S extends StyleSchema
6366
>(
6467
block: BlockNoDefaults<BSchema, I, S>,
65-
editor: BlockNoteEditor<BSchema, I, S>
68+
editor: BlockNoteEditor<BSchema, I, S>,
69+
external = false
6670
): {
6771
dom: HTMLElement;
6872
contentDOM?: HTMLElement;
@@ -90,6 +94,74 @@ export const defaultBlockToHTML = <
9094
);
9195
}
9296

97+
// When exporting to external HTML, we convert from `data-*` attributes to
98+
// inline styles properties which can be understood by external applications.
99+
//
100+
// Note: This is a bit hacky to do this here as we're just hardcoding this for
101+
// props on default blocks. We should revisit this when we migrate internal
102+
// blocks to use the custom blocks API.
103+
if (external) {
104+
const dom = renderSpec.dom as HTMLElement;
105+
106+
if (dom.hasAttribute("data-background-color")) {
107+
const backgroundColor = dom.getAttribute("data-background-color")!;
108+
109+
// If the background color is one of the default colors, we set the
110+
// color's hex code from the default theme, as this will look nicer than
111+
// using regular CSS colors. For example, instead of
112+
// `background-color: red`, we use `background-color: #fbe4e4`.
113+
if (backgroundColor in COLORS_DEFAULT) {
114+
const cssVariableName =
115+
`--blocknote-background-${backgroundColor}` as any;
116+
117+
dom.style.setProperty(
118+
cssVariableName,
119+
COLORS_DEFAULT[backgroundColor as keyof typeof COLORS_DEFAULT]
120+
.background
121+
);
122+
dom.style.backgroundColor = `var(${cssVariableName})`;
123+
} else {
124+
dom.style.backgroundColor = backgroundColor;
125+
}
126+
127+
dom.removeAttribute("data-background-color");
128+
}
129+
130+
if (dom.hasAttribute("data-text-color")) {
131+
const textColor = dom.getAttribute("data-text-color")!;
132+
133+
// If the text color is one of the default colors, we set the color's hex
134+
// code from the default theme, as this will look nicer than using regular
135+
// CSS colors. For example, instead of `color: red`, we use
136+
// `color: #e03e3e`.
137+
if (textColor in COLORS_DEFAULT) {
138+
const cssVariableName = `--blocknote-text-${textColor}` as any;
139+
140+
dom.style.setProperty(
141+
cssVariableName,
142+
COLORS_DEFAULT[textColor as keyof typeof COLORS_DEFAULT].text
143+
);
144+
dom.style.color = `var(${cssVariableName})`;
145+
} else {
146+
dom.style.color = textColor;
147+
}
148+
149+
dom.removeAttribute("data-text-color");
150+
}
151+
152+
if (dom.hasAttribute("data-text-alignment")) {
153+
dom.style.textAlign = dom.getAttribute("data-text-alignment")!;
154+
dom.removeAttribute("data-text-alignment");
155+
}
156+
157+
// We also remove the `data-level` attribute for heading blocks, as this
158+
// information can be inferred from whether a `h1`, `h2`, or `h3 tag is
159+
// used.
160+
if (dom.hasAttribute("data-level")) {
161+
dom.removeAttribute("data-level");
162+
}
163+
}
164+
93165
return renderSpec as {
94166
dom: HTMLElement;
95167
contentDOM?: HTMLElement;

packages/core/src/blocks/defaultProps.ts

+18
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ const getBackgroundColorAttribute = (
3030
}
3131

3232
if (element.style.backgroundColor) {
33+
// Check if `element.style.backgroundColor` matches the string:
34+
// `var(--blocknote-background-<color>)`. If it does, return the color
35+
// name only. Otherwise, return `element.style.backgroundColor`.
36+
const match = element.style.backgroundColor.match(
37+
/var\(--blocknote-background-(.+)\)/
38+
);
39+
if (match) {
40+
return match[1];
41+
}
42+
3343
return element.style.backgroundColor;
3444
}
3545

@@ -54,6 +64,14 @@ const getTextColorAttribute = (attributeName = "textColor"): Attribute => ({
5464
}
5565

5666
if (element.style.color) {
67+
// Check if `element.style.color` matches the string:
68+
// `var(--blocknote-text-<color>)`. If it does, return the color name
69+
// only. Otherwise, return `element.style.color`.
70+
const match = element.style.color.match(/var\(--blocknote-text-(.+)\)/);
71+
if (match) {
72+
return match[1];
73+
}
74+
5775
return element.style.color;
5876
}
5977

packages/core/src/schema/blocks/internal.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ export function createBlockSpecFromStronglyTypedTiptapNode<
267267
node,
268268
requiredExtensions,
269269
toInternalHTML: defaultBlockToHTML,
270-
toExternalHTML: defaultBlockToHTML,
270+
toExternalHTML: (block, editor) =>
271+
defaultBlockToHTML(block, editor, true),
271272
// parse: () => undefined, // parse rules are in node already
272273
}
273274
);

packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
exports[`Test ServerBlockNoteEditor > converts to HTML (blocksToFullHTML) 1`] = `"<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="heading" data-text-color="yellow" data-background-color="blue" data-text-alignment="right" data-level="2"><h2 class="bn-inline-content"><strong><u>Heading </u></strong><em><s>2</s></em></h2></div><div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="2"><div class="bn-block" data-node-type="blockContainer" data-id="2"><div class="bn-block-content" data-content-type="paragraph" data-background-color="red"><p class="bn-inline-content">Paragraph</p></div></div></div><div class="bn-block-outer" data-node-type="blockOuter" data-id="3"><div class="bn-block" data-node-type="blockContainer" data-id="3"><div class="bn-block-content" data-content-type="bulletListItem"><p class="bn-inline-content">list item</p></div></div></div></div></div></div><div class="bn-block-outer" data-node-type="blockOuter" data-id="4"><div class="bn-block" data-node-type="blockContainer" data-id="4"><div class="bn-block-content" data-content-type="image" data-name="Example" data-url="exampleURL" data-caption="Caption" data-preview-width="256" data-file-block=""><div class="bn-file-block-content-wrapper" style="width: 256px;"><div class="bn-visual-media-wrapper"><img class="bn-visual-media" src="exampleURL" alt="Example" draggable="false"></div><p class="bn-file-caption">Caption</p></div></div></div></div><div class="bn-block-outer" data-node-type="blockOuter" data-id="5"><div class="bn-block" data-node-type="blockContainer" data-id="5"><div class="bn-block-content" data-content-type="image" data-name="Example" data-url="exampleURL" data-caption="Caption" data-show-preview="false" data-preview-width="256" data-file-block=""><div class="bn-file-block-content-wrapper"><div class="bn-file-name-with-icon"><div class="bn-file-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8ZM10 4V9H5V20H19V4H10Z"></path></svg></div><p class="bn-file-name">Example</p></div><p class="bn-file-caption">Caption</p></div></div></div></div></div>"`;
44

5-
exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 1`] = `"<h2 data-text-color="yellow" data-background-color="blue" data-text-alignment="right" data-level="2"><strong><u>Heading </u></strong><em><s>2</s></em></h2><p data-background-color="red">Paragraph</p><ul><li><p>list item</p></li></ul><figure data-name="Example" data-url="exampleURL" data-caption="Caption" data-preview-width="256"><img src="exampleURL" alt="Example" width="256"><figcaption>Caption</figcaption></figure><div data-name="Example" data-url="exampleURL" data-caption="Caption" data-show-preview="false" data-preview-width="256"><a href="exampleURL">Example</a><p>Caption</p></div>"`;
5+
exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 1`] = `"<h2 style="--blocknote-background-blue: #ddebf1; background-color: var(--blocknote-background-blue); --blocknote-text-yellow: #dfab01; color: var(--blocknote-text-yellow); text-align: right;"><strong><u>Heading </u></strong><em><s>2</s></em></h2><p style="--blocknote-background-red: #fbe4e4; background-color: var(--blocknote-background-red);">Paragraph</p><ul><li><p>list item</p></li></ul><figure data-name="Example" data-url="exampleURL" data-caption="Caption" data-preview-width="256"><img src="exampleURL" alt="Example" width="256"><figcaption>Caption</figcaption></figure><div data-name="Example" data-url="exampleURL" data-caption="Caption" data-show-preview="false" data-preview-width="256"><a href="exampleURL">Example</a><p>Caption</p></div>"`;
66
77
exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 2`] = `
88
[

tests/src/unit/core/clipboard/copy/__snapshots__/text/html/basicBlocksWithProps.html

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
<p data-text-color="red">Paragraph 1</p>
2-
<h2 data-level="2">Heading 1</h2>
1+
<p style="--blocknote-text-red: #e03e3e; color: var(--blocknote-text-red);">Paragraph 1</p>
2+
<h2>Heading 1</h2>
33
<ol start="2">
44
<li data-start="2">
55
<p>Numbered List Item 1</p>
66
</li>
77
</ol>
88
<ul>
9-
<li data-background-color="red">
9+
<li
10+
style="--blocknote-background-red: #fbe4e4; background-color: var(--blocknote-background-red);"
11+
>
1012
<p>Bullet List Item 1</p>
1113
</li>
1214
<li data-checked="true">

tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/misc.html

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
<h2 data-text-color="yellow" data-background-color="blue" data-text-alignment="right" data-level="2">
1+
<h2 style="--blocknote-background-blue: #ddebf1; background-color: var(--blocknote-background-blue); --blocknote-text-yellow: #dfab01; color: var(--blocknote-text-yellow); text-align: right;">
22
<strong>
33
<u>Heading</u>
44
</strong>
55
<em>
66
<s>2</s>
77
</em>
88
</h2>
9-
<p data-background-color="red">Paragraph</p>
9+
<p
10+
style="--blocknote-background-red: #fbe4e4; background-color: var(--blocknote-background-red);"
11+
>Paragraph</p>
1012
<ul>
1113
<li>
1214
<p></p>

tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/styled.html

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<p
2-
data-text-color="orange"
3-
data-background-color="pink"
4-
data-text-alignment="center"
2+
style="--blocknote-background-pink: #f4dfeb; background-color: var(--blocknote-background-pink); --blocknote-text-orange: #d9730d; color: var(--blocknote-text-orange); text-align: center;"
53
>
64
Plain
75
<span data-text-color="red">Red Text</span>

tests/src/unit/core/formatConversion/parse/__snapshots__/html/backgroundColorProp.json

+3-20
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,13 @@
44
"content": [
55
{
66
"styles": {},
7-
"text": "Red Background",
7+
"text": "Blue Background",
88
"type": "text",
99
},
1010
],
1111
"id": "1",
1212
"props": {
13-
"backgroundColor": "red",
14-
"textAlignment": "left",
15-
"textColor": "default",
16-
},
17-
"type": "paragraph",
18-
},
19-
{
20-
"children": [],
21-
"content": [
22-
{
23-
"styles": {},
24-
"text": "Green Background",
25-
"type": "text",
26-
},
27-
],
28-
"id": "2",
29-
"props": {
30-
"backgroundColor": "green",
13+
"backgroundColor": "blue",
3114
"textAlignment": "left",
3215
"textColor": "default",
3316
},
@@ -42,7 +25,7 @@
4225
"type": "text",
4326
},
4427
],
45-
"id": "3",
28+
"id": "2",
4629
"props": {
4730
"backgroundColor": "blue",
4831
"textAlignment": "left",

tests/src/unit/core/formatConversion/parse/__snapshots__/html/backgroundColorStyle.json

+2-14
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,9 @@
44
"content": [
55
{
66
"styles": {
7-
"backgroundColor": "red",
8-
},
9-
"text": "Red Background",
10-
"type": "text",
11-
},
12-
{
13-
"styles": {},
14-
"text": " ",
15-
"type": "text",
16-
},
17-
{
18-
"styles": {
19-
"backgroundColor": "green",
7+
"backgroundColor": "blue",
208
},
21-
"text": "Green Background",
9+
"text": "Blue Background",
2210
"type": "text",
2311
},
2412
{

tests/src/unit/core/formatConversion/parse/__snapshots__/html/textColorProp.json

+3-20
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
"content": [
55
{
66
"styles": {},
7-
"text": "Red Paragraph",
7+
"text": "Blue Text",
88
"type": "text",
99
},
1010
],
1111
"id": "1",
1212
"props": {
1313
"backgroundColor": "default",
1414
"textAlignment": "left",
15-
"textColor": "red",
15+
"textColor": "blue",
1616
},
1717
"type": "paragraph",
1818
},
@@ -21,28 +21,11 @@
2121
"content": [
2222
{
2323
"styles": {},
24-
"text": "Green Paragraph",
24+
"text": "Blue Text",
2525
"type": "text",
2626
},
2727
],
2828
"id": "2",
29-
"props": {
30-
"backgroundColor": "default",
31-
"textAlignment": "left",
32-
"textColor": "green",
33-
},
34-
"type": "paragraph",
35-
},
36-
{
37-
"children": [],
38-
"content": [
39-
{
40-
"styles": {},
41-
"text": "Blue Paragraph",
42-
"type": "text",
43-
},
44-
],
45-
"id": "3",
4629
"props": {
4730
"backgroundColor": "default",
4831
"textAlignment": "left",

tests/src/unit/core/formatConversion/parse/__snapshots__/html/textColorStyle.json

+2-14
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,9 @@
44
"content": [
55
{
66
"styles": {
7-
"textColor": "red",
8-
},
9-
"text": "Red Text",
10-
"type": "text",
11-
},
12-
{
13-
"styles": {},
14-
"text": " ",
15-
"type": "text",
16-
},
17-
{
18-
"styles": {
19-
"textColor": "green",
7+
"textColor": "blue",
208
},
21-
"text": "Green Text",
9+
"text": "Blue Text",
2210
"type": "text",
2311
},
2412
{

tests/src/unit/core/formatConversion/parse/__snapshots__/html/twoTables.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
{
4646
"styles": {},
4747
"text": "
48-
48+
4949
Name: [Company Representative]
5050
Title: Chief Executive Officer",
5151
"type": "text",

0 commit comments

Comments
 (0)