@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
193 lines (168 loc) • 5.52 kB
text/typescript
import { DOMSerializer, Fragment } 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";
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: any;
// 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);
}
// We call the prosemirror serializer here because it handles Marks and Inline Content nodes nicely.
// If we'd want to support custom serialization or externalHTML for Inline Content, we'd have to implement
// a custom serializer here.
const dom = serializer.serializeFragment(Fragment.from(nodes), options);
return dom;
}
function serializeBlock<
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema,
>(
editor: BlockNoteEditor<BSchema, I, S>,
block: PartialBlock<BSchema, I, S>,
serializer: DOMSerializer,
listIndex: number,
options?: { document?: Document },
) {
const BC_NODE = editor.pmSchema.nodes["blockContainer"];
let props = block.props;
// set default props in case we were passed a partial block
if (!block.props) {
props = {};
for (const [name, spec] of Object.entries(
editor.schema.blockSchema[block.type as any].propSchema,
)) {
if (spec.default !== undefined) {
(props as any)[name] = spec.default;
}
}
}
const impl = editor.blockImplementations[block.type as any].implementation;
const ret = impl.toInternalHTML({ ...block, props } as any, editor as any);
if (block.type === "numberedListItem") {
// This is a workaround to make sure there's a list index set.
// Normally, this is set on the internal prosemirror nodes by the NumberedListIndexingPlugin,
// but:
// - (a) this information is not available on the Blocks passed to the serializer. (we only have access to BlockNote Blocks)
// - (b) the NumberedListIndexingPlugin might not even have run, because we can manually call blocksToFullHTML
// with blocks that are not part of the active document
ret.dom.setAttribute("data-index", listIndex.toString());
}
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();
let listIndex = 0;
for (const block of blocks) {
if (block.type === "numberedListItem") {
listIndex++;
} else {
listIndex = 0;
}
const blockDOM = serializeBlock(
editor,
block,
serializer,
listIndex,
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;
};