Skip to content

feat: Inline style props in external HTML #1636

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
76 changes: 74 additions & 2 deletions packages/core/src/blocks/defaultBlockHelpers.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<BSchema, I, S>,
editor: BlockNoteEditor<BSchema, I, S>
editor: BlockNoteEditor<BSchema, I, S>,
external = false
): {
dom: HTMLElement;
contentDOM?: HTMLElement;
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/blocks/defaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ const getBackgroundColorAttribute = (
}

if (element.style.backgroundColor) {
// Check if `element.style.backgroundColor` matches the string:
// `var(--blocknote-background-<color>)`. 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;
}

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

if (element.style.color) {
// Check if `element.style.color` matches the string:
// `var(--blocknote-text-<color>)`. 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;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/schema/blocks/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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>"`;

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>"`;
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>"`;

exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 2`] = `
[
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<p data-text-color="red">Paragraph 1</p>
<h2 data-level="2">Heading 1</h2>
<p style="--blocknote-text-red: #e03e3e; color: var(--blocknote-text-red);">Paragraph 1</p>
<h2>Heading 1</h2>
<ol start="2">
<li data-start="2">
<p>Numbered List Item 1</p>
</li>
</ol>
<ul>
<li data-background-color="red">
<li
style="--blocknote-background-red: #fbe4e4; background-color: var(--blocknote-background-red);"
>
<p>Bullet List Item 1</p>
</li>
<li data-checked="true">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<h2 data-text-color="yellow" data-background-color="blue" data-text-alignment="right" data-level="2">
<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 data-background-color="red">Paragraph</p>
<p
style="--blocknote-background-red: #fbe4e4; background-color: var(--blocknote-background-red);"
>Paragraph</p>
<ul>
<li>
<p></p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<p
data-text-color="orange"
data-background-color="pink"
data-text-alignment="center"
style="--blocknote-background-pink: #f4dfeb; background-color: var(--blocknote-background-pink); --blocknote-text-orange: #d9730d; color: var(--blocknote-text-orange); text-align: center;"
>
Plain
<span data-text-color="red">Red Text</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand All @@ -42,7 +25,7 @@
"type": "text",
},
],
"id": "3",
"id": "2",
"props": {
"backgroundColor": "blue",
"textAlignment": "left",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
"content": [
{
"styles": {},
"text": "Red Paragraph",
"text": "Blue Text",
"type": "text",
},
],
"id": "1",
"props": {
"backgroundColor": "default",
"textAlignment": "left",
"textColor": "red",
"textColor": "blue",
},
"type": "paragraph",
},
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
{
"styles": {},
"text": "

Name: [Company Representative]
Title: Chief Executive Officer",
"type": "text",
Expand Down
Loading
Loading