@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
254 lines (224 loc) • 7.43 kB
text/typescript
import { DOMSerializer, Fragment, Node } from "prosemirror-model";
import { PartialBlock } from "../../../../blocks/defaultBlocks.js";
import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
import {
BlockSchema,
InlineContentSchema,
StyleSchema,
} from "../../../../schema/index.js";
import { UnreachableCaseError } from "../../../../util/typescript.js";
import {
inlineContentToNodes,
tableContentToNodes,
} from "../../../nodeConversions/blockToNode.js";
import { nodeToCustomInlineContent } from "../../../nodeConversions/nodeToBlock.js";
export function serializeInlineContentInternalHTML<
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema,
>(
editor: BlockNoteEditor<any, I, S>,
blockContent: PartialBlock<BSchema, I, S>["content"],
serializer: DOMSerializer,
blockType?: string,
options?: { document?: Document },
) {
let nodes: Node[];
// TODO: reuse function from nodeconversions?
if (!blockContent) {
throw new Error("blockContent is required");
} else if (typeof blockContent === "string") {
nodes = inlineContentToNodes([blockContent], editor.pmSchema, blockType);
} else if (Array.isArray(blockContent)) {
nodes = inlineContentToNodes(blockContent, editor.pmSchema, blockType);
} else if (blockContent.type === "tableContent") {
nodes = tableContentToNodes(blockContent, editor.pmSchema);
} else {
throw new UnreachableCaseError(blockContent.type);
}
// Check if any of the nodes are custom inline content with toExternalHTML
const doc = options?.document ?? document;
const fragment = doc.createDocumentFragment();
for (const node of nodes) {
// Check if this is a custom inline content node with toExternalHTML
if (
node.type.name !== "text" &&
editor.schema.inlineContentSchema[node.type.name]
) {
const inlineContentImplementation =
editor.schema.inlineContentSpecs[node.type.name].implementation;
if (inlineContentImplementation) {
// Convert the node to inline content format
const inlineContent = nodeToCustomInlineContent(
node,
editor.schema.inlineContentSchema,
editor.schema.styleSchema,
);
// Use the custom toExternalHTML method
const output = inlineContentImplementation.render.call(
{
renderType: "dom",
props: undefined,
},
inlineContent as any,
() => {
// No-op
},
editor as any,
);
if (output) {
fragment.appendChild(output.dom);
// If contentDOM exists, render the inline content into it
if (output.contentDOM) {
const contentFragment = serializer.serializeFragment(
node.content,
options,
);
output.contentDOM.dataset.editable = "";
output.contentDOM.appendChild(contentFragment);
}
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,
);
// 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.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);
} else {
// Fall back to default serialization for this node
const nodeFragment = serializer.serializeFragment(
Fragment.from([node]),
options,
);
fragment.appendChild(nodeFragment);
}
}
return fragment;
}
function serializeBlock<
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema,
>(
editor: BlockNoteEditor<BSchema, I, S>,
block: PartialBlock<BSchema, I, S>,
serializer: DOMSerializer,
options?: { document?: Document },
) {
const BC_NODE = editor.pmSchema.nodes["blockContainer"];
// set default props in case we were passed a partial block
const props = block.props || {};
for (const [name, spec] of Object.entries(
editor.schema.blockSchema[block.type as any].propSchema,
)) {
if (!(name in props) && spec.default !== undefined) {
(props as any)[name] = spec.default;
}
}
const children = block.children || [];
const impl = editor.blockImplementations[block.type as any].implementation;
const ret = impl.render.call(
{
renderType: "dom",
props: undefined,
},
{ ...block, props, children } as any,
editor as any,
);
if (ret.contentDOM && block.content) {
const ic = serializeInlineContentInternalHTML(
editor,
block.content as any, // TODO
serializer,
block.type,
options,
);
ret.contentDOM.appendChild(ic);
}
const pmType = editor.pmSchema.nodes[block.type as any];
if (pmType.isInGroup("bnBlock")) {
if (block.children && block.children.length > 0) {
const fragment = serializeBlocks(
editor,
block.children,
serializer,
options,
);
ret.contentDOM?.append(fragment);
}
return ret.dom;
}
// wrap the block in a blockContainer
const bc = BC_NODE.spec?.toDOM?.(
BC_NODE.create({
id: block.id,
...props,
}),
) as {
dom: HTMLElement;
contentDOM?: HTMLElement;
};
bc.contentDOM?.appendChild(ret.dom);
if (block.children && block.children.length > 0) {
bc.contentDOM?.appendChild(
serializeBlocksInternalHTML(editor, block.children, serializer, options),
);
}
return bc.dom;
}
function serializeBlocks<
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema,
>(
editor: BlockNoteEditor<BSchema, I, S>,
blocks: PartialBlock<BSchema, I, S>[],
serializer: DOMSerializer,
options?: { document?: Document },
) {
const doc = options?.document ?? document;
const fragment = doc.createDocumentFragment();
for (const block of blocks) {
const blockDOM = serializeBlock(editor, block, serializer, options);
fragment.appendChild(blockDOM);
}
return fragment;
}
export const serializeBlocksInternalHTML = <
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema,
>(
editor: BlockNoteEditor<BSchema, I, S>,
blocks: PartialBlock<BSchema, I, S>[],
serializer: DOMSerializer,
options?: { document?: Document },
) => {
const BG_NODE = editor.pmSchema.nodes["blockGroup"];
const bg = BG_NODE.spec!.toDOM!(BG_NODE.create({})) as {
dom: HTMLElement;
contentDOM?: HTMLElement;
};
const fragment = serializeBlocks(editor, blocks, serializer, options);
bg.contentDOM?.appendChild(fragment);
return bg.dom;
};