diff --git a/packages/core/src/api/clipboard/__snapshots__/tableAllCells.html b/packages/core/src/api/clipboard/__snapshots__/tableAllCells.html
index 1de956c176..2f5ba018cf 100644
--- a/packages/core/src/api/clipboard/__snapshots__/tableAllCells.html
+++ b/packages/core/src/api/clipboard/__snapshots__/tableAllCells.html
@@ -1 +1 @@
-
Table Cell | Table Cell |
Table Cell | Table Cell |
\ No newline at end of file
+Table Cell | Table Cell |
Table Cell | Table Cell |
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/clipboard.test.ts b/packages/core/src/api/clipboard/clipboard.test.ts
index 58cf9e8927..df974c55ea 100644
--- a/packages/core/src/api/clipboard/clipboard.test.ts
+++ b/packages/core/src/api/clipboard/clipboard.test.ts
@@ -5,8 +5,8 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { PartialBlock } from "../../blocks/defaultBlocks";
import { BlockNoteEditor } from "../../editor/BlockNoteEditor";
-import { doPaste } from "../testUtil/paste";
import { initializeESMDependencies } from "../../util/esmDependencies";
+import { doPaste } from "../testUtil/paste";
import { selectedFragmentToHTML } from "./toClipboard/copyExtension";
type SelectionTestCase = {
@@ -269,7 +269,6 @@ describe("Test ProseMirror selection clipboard HTML", () => {
createSelection: (doc) => CellSelection.create(doc, 214, 228),
},
// Selection spans all cells of the table.
- // TODO: External HTML is wrapped in unnecessary `blockContent` element.
{
testName: "tableAllCells",
createSelection: (doc) => CellSelection.create(doc, 214, 258),
diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts
index ed3446ac3f..b27801ce2f 100644
--- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts
+++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts
@@ -1,5 +1,5 @@
import { Extension } from "@tiptap/core";
-import { Node } from "prosemirror-model";
+import { Fragment, Node } from "prosemirror-model";
import { NodeSelection, Plugin } from "prosemirror-state";
import { CellSelection } from "prosemirror-tables";
import * as pmView from "prosemirror-view";
@@ -10,48 +10,28 @@ import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema";
import { initializeESMDependencies } from "../../../util/esmDependencies";
import { createExternalHTMLExporter } from "../../exporters/html/externalHTMLExporter";
import { cleanHTMLToMarkdown } from "../../exporters/markdown/markdownExporter";
+import { fragmentToBlocks } from "../../nodeConversions/fragmentToBlocks";
+import {
+ contentNodeToInlineContent,
+ contentNodeToTableContent,
+} from "../../nodeConversions/nodeConversions";
-export async function selectedFragmentToHTML<
+async function fragmentToExternalHTML<
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema
>(
- view: EditorView,
+ view: pmView.EditorView,
+ selectedFragment: Fragment,
editor: BlockNoteEditor
-): Promise<{
- clipboardHTML: string;
- externalHTML: string;
- markdown: string;
-}> {
- // Checks if a `blockContent` node is being copied and expands
- // the selection to the parent `blockContainer` node. This is
- // for the use-case in which only a block without content is
- // selected, e.g. an image block.
- if (
- "node" in view.state.selection &&
- (view.state.selection.node as Node).type.spec.group === "blockContent"
- ) {
- editor.dispatch(
- editor._tiptapEditor.state.tr.setSelection(
- new NodeSelection(view.state.doc.resolve(view.state.selection.from - 1))
- )
- );
- }
-
- // Uses default ProseMirror clipboard serialization.
- const clipboardHTML: string = (pmView as any).__serializeForClipboard(
- view,
- view.state.selection.content()
- ).dom.innerHTML;
-
- let selectedFragment = view.state.selection.content().content;
-
- // Checks whether block ancestry should be included when creating external
- // HTML. If the selection is within a block content node, the block ancestry
- // is excluded as we only care about the inline content.
+) {
let isWithinBlockContent = false;
const isWithinTable = view.state.selection instanceof CellSelection;
+
if (!isWithinTable) {
+ // Checks whether block ancestry should be included when creating external
+ // HTML. If the selection is within a block content node, the block ancestry
+ // is excluded as we only care about the inline content.
const fragmentWithoutParents = view.state.doc.slice(
view.state.selection.from,
view.state.selection.to,
@@ -75,14 +55,89 @@ export async function selectedFragmentToHTML<
}
}
+ let externalHTML: string;
+
await initializeESMDependencies();
const externalHTMLExporter = createExternalHTMLExporter(
view.state.schema,
editor
);
- const externalHTML = externalHTMLExporter.exportProseMirrorFragment(
+
+ if (isWithinTable) {
+ if (selectedFragment.firstChild?.type.name === "table") {
+ // contentNodeToTableContent expects the fragment of the content of a table, not the table node itself
+ // but cellselection.content() returns the table node itself if all cells and columns are selected
+ selectedFragment = selectedFragment.firstChild.content;
+ }
+
+ // first convert selection to blocknote-style table content, and then
+ // pass this to the exporter
+ const ic = contentNodeToTableContent(
+ selectedFragment as any,
+ editor.schema.inlineContentSchema,
+ editor.schema.styleSchema
+ );
+
+ externalHTML = externalHTMLExporter.exportInlineContent(ic as any, {
+ simplifyBlocks: false,
+ });
+ } else if (isWithinBlockContent) {
+ // first convert selection to blocknote-style inline content, and then
+ // pass this to the exporter
+ const ic = contentNodeToInlineContent(
+ selectedFragment as any,
+ editor.schema.inlineContentSchema,
+ editor.schema.styleSchema
+ );
+ externalHTML = externalHTMLExporter.exportInlineContent(ic, {
+ simplifyBlocks: false,
+ });
+ } else {
+ const blocks = fragmentToBlocks(selectedFragment, editor.schema);
+ externalHTML = externalHTMLExporter.exportBlocks(blocks, {});
+ }
+ return externalHTML;
+}
+
+export async function selectedFragmentToHTML<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(
+ view: EditorView,
+ editor: BlockNoteEditor
+): Promise<{
+ clipboardHTML: string;
+ externalHTML: string;
+ markdown: string;
+}> {
+ // Checks if a `blockContent` node is being copied and expands
+ // the selection to the parent `blockContainer` node. This is
+ // for the use-case in which only a block without content is
+ // selected, e.g. an image block.
+ if (
+ "node" in view.state.selection &&
+ (view.state.selection.node as Node).type.spec.group === "blockContent"
+ ) {
+ editor.dispatch(
+ editor._tiptapEditor.state.tr.setSelection(
+ new NodeSelection(view.state.doc.resolve(view.state.selection.from - 1))
+ )
+ );
+ }
+
+ // Uses default ProseMirror clipboard serialization.
+ const clipboardHTML: string = (pmView as any).__serializeForClipboard(
+ view,
+ view.state.selection.content()
+ ).dom.innerHTML;
+
+ const selectedFragment = view.state.selection.content().content;
+
+ const externalHTML = await fragmentToExternalHTML(
+ view,
selectedFragment,
- { simplifyBlocks: !isWithinBlockContent && !isWithinTable }
+ editor
);
const markdown = cleanHTMLToMarkdown(externalHTML);
diff --git a/packages/core/src/api/exporters/html/externalHTMLExporter.ts b/packages/core/src/api/exporters/html/externalHTMLExporter.ts
index 2d6a859d9d..4f2542f643 100644
--- a/packages/core/src/api/exporters/html/externalHTMLExporter.ts
+++ b/packages/core/src/api/exporters/html/externalHTMLExporter.ts
@@ -1,13 +1,17 @@
-import { DOMSerializer, Fragment, Node, Schema } from "prosemirror-model";
+import { DOMSerializer, Schema } from "prosemirror-model";
import { PartialBlock } from "../../../blocks/defaultBlocks";
import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
-import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema";
+import {
+ BlockSchema,
+ InlineContent,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../../schema";
import { esmDependencies } from "../../../util/esmDependencies";
-import { blockToNode } from "../../nodeConversions/nodeConversions";
import {
- serializeNodeInner,
- serializeProseMirrorFragment,
+ serializeBlocks,
+ serializeInlineContent,
} from "./util/sharedHTMLConversion";
import { simplifyBlocks } from "./util/simplifyBlocksRehypePlugin";
@@ -24,26 +28,6 @@ import { simplifyBlocks } from "./util/simplifyBlocksRehypePlugin";
// 3. While nesting for list items is preserved, other types of blocks nested
// inside a list are un-nested and a new list is created after them.
// 4. The HTML is wrapped in a single `div` element.
-//
-// The serializer has 2 main methods:
-// `exportBlocks`: Exports an array of blocks to HTML.
-// `exportFragment`: Exports a ProseMirror fragment to HTML. This is mostly
-// useful if you want to export a selection which may not start/end at the
-// start/end of a block.
-export interface ExternalHTMLExporter<
- BSchema extends BlockSchema,
- I extends InlineContentSchema,
- S extends StyleSchema
-> {
- exportBlocks: (
- blocks: PartialBlock[],
- options: { document?: Document }
- ) => string;
- exportProseMirrorFragment: (
- fragment: Fragment,
- options: { document?: Document; simplifyBlocks?: boolean }
- ) => string;
-}
// Needs to be sync because it's used in drag handler event (SideMenuPlugin)
// Ideally, call `await initializeESMDependencies()` before calling this function
@@ -54,7 +38,7 @@ export const createExternalHTMLExporter = <
>(
schema: Schema,
editor: BlockNoteEditor
-): ExternalHTMLExporter => {
+) => {
const deps = esmDependencies;
if (!deps) {
@@ -63,67 +47,63 @@ export const createExternalHTMLExporter = <
);
}
- // TODO: maybe cache this serializer (default prosemirror serializer is cached)?
- const serializer = new DOMSerializer(
- DOMSerializer.nodesFromSchema(schema),
- DOMSerializer.marksFromSchema(schema)
- ) as DOMSerializer & {
- serializeNodeInner: (
- node: Node,
- options: { document?: Document }
- ) => HTMLElement;
- exportProseMirrorFragment: (
- fragment: Fragment,
- options: { document?: Document; simplifyBlocks?: boolean }
- ) => string;
+ const serializer = DOMSerializer.fromSchema(schema);
+
+ return {
exportBlocks: (
blocks: PartialBlock[],
options: { document?: Document }
- ) => string;
- };
+ ) => {
+ const html = serializeBlocks(
+ editor,
+ blocks,
+ serializer,
+ true,
+ options
+ ).outerHTML;
- serializer.serializeNodeInner = (
- node: Node,
- options: { document?: Document }
- ) => serializeNodeInner(node, options, serializer, editor, true);
+ // Possible improvement: now, we first use the serializeBlocks function
+ // which adds blockcontainer and blockgroup wrappers. We then pass the
+ // result to simplifyBlocks, which then cleans the wrappers.
+ //
+ // It might be easier if we create a version of serializeBlocks that
+ // doesn't add the wrappers in the first place, then we can get rid of
+ // the more complex simplifyBlocks plugin.
+ let externalHTML: any = deps.unified
+ .unified()
+ .use(deps.rehypeParse.default, { fragment: true });
+ if ((options as any).simplifyBlocks !== false) {
+ externalHTML = externalHTML.use(simplifyBlocks, {
+ orderedListItemBlockTypes: new Set(["numberedListItem"]),
+ unorderedListItemBlockTypes: new Set([
+ "bulletListItem",
+ "checkListItem",
+ ]),
+ });
+ }
+ externalHTML = externalHTML
+ .use(deps.rehypeStringify.default)
+ .processSync(html);
- // Like the `internalHTMLSerializer`, also uses `serializeProseMirrorFragment`
- // but additionally runs it through the `simplifyBlocks` rehype plugin to
- // convert the internal HTML to external.
- serializer.exportProseMirrorFragment = (fragment, options) => {
- let externalHTML: any = deps.unified
- .unified()
- .use(deps.rehypeParse.default, { fragment: true });
- if (options.simplifyBlocks !== false) {
- externalHTML = externalHTML.use(simplifyBlocks, {
- orderedListItemBlockTypes: new Set(["numberedListItem"]),
- unorderedListItemBlockTypes: new Set([
- "bulletListItem",
- "checkListItem",
- ]),
- });
- }
- externalHTML = externalHTML
- .use(deps.rehypeStringify.default)
- .processSync(serializeProseMirrorFragment(fragment, serializer, options));
+ return externalHTML.value as string;
+ },
- return externalHTML.value as string;
- };
+ exportInlineContent: (
+ inlineContent: InlineContent[],
+ options: { simplifyBlocks: boolean; document?: Document }
+ ) => {
+ const domFragment = serializeInlineContent(
+ editor,
+ inlineContent as any,
+ serializer,
+ true,
+ options
+ );
- serializer.exportBlocks = (
- blocks: PartialBlock[],
- options
- ) => {
- const nodes = blocks.map((block) =>
- blockToNode(block, schema, editor.schema.styleSchema)
- );
- const blockGroup = schema.nodes["blockGroup"].create(null, nodes);
+ const parent = document.createElement("div");
+ parent.append(domFragment.cloneNode(true));
- return serializer.exportProseMirrorFragment(
- Fragment.from(blockGroup),
- options
- );
+ return parent.innerHTML;
+ },
};
-
- return serializer;
};
diff --git a/packages/core/src/api/exporters/html/htmlConversion.test.ts b/packages/core/src/api/exporters/html/htmlConversion.test.ts
index 850158902f..36291dfd3c 100644
--- a/packages/core/src/api/exporters/html/htmlConversion.test.ts
+++ b/packages/core/src/api/exporters/html/htmlConversion.test.ts
@@ -3,9 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { addIdsToBlocks, partialBlocksToBlocksForTesting } from "../../..";
import { PartialBlock } from "../../../blocks/defaultBlocks";
import { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
-import { BlockSchema } from "../../../schema";
-import { InlineContentSchema } from "../../../schema";
-import { StyleSchema } from "../../../schema";
+import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema";
import { initializeESMDependencies } from "../../../util/esmDependencies";
import { customBlocksTestCases } from "../../testUtil/cases/customBlocks";
import { customInlineContentTestCases } from "../../testUtil/cases/customInlineContent";
@@ -25,6 +23,7 @@ async function convertToHTMLAndCompareSnapshots<
snapshotName: string
) {
addIdsToBlocks(blocks);
+
const serializer = createInternalHTMLSerializer(editor.pmSchema, editor);
const internalHTML = serializer.serializeBlocks(blocks, {});
const internalHTMLSnapshotPath =
diff --git a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
index 9233190674..7d63b42312 100644
--- a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
+++ b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
@@ -1,12 +1,8 @@
-import { DOMSerializer, Fragment, Node, Schema } from "prosemirror-model";
+import { DOMSerializer, Schema } from "prosemirror-model";
import { PartialBlock } from "../../../blocks/defaultBlocks";
import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema";
-import { blockToNode } from "../../nodeConversions/nodeConversions";
-import {
- serializeNodeInner,
- serializeProseMirrorFragment,
-} from "./util/sharedHTMLConversion";
+import { serializeBlocks } from "./util/sharedHTMLConversion";
// Used to serialize BlockNote blocks and ProseMirror nodes to HTML without
// losing data. Blocks are exported using the `toInternalHTML` method in their
// `blockSpec`.
@@ -16,29 +12,6 @@ import {
// editor, including the `blockGroup` and `blockContainer` wrappers. This also
// means that it can be converted back to the original blocks without any data
// loss.
-//
-// The serializer has 2 main methods:
-// `serializeFragment`: Serializes a ProseMirror fragment to HTML. This is
-// mostly useful if you want to serialize a selection which may not start/end at
-// the start/end of a block.
-// `serializeBlocks`: Serializes an array of blocks to HTML.
-export interface InternalHTMLSerializer<
- BSchema extends BlockSchema,
- I extends InlineContentSchema,
- S extends StyleSchema
-> {
- // TODO: Ideally we would expand the BlockNote API to support partial
- // selections so we don't need this.
- serializeProseMirrorFragment: (
- fragment: Fragment,
- options: { document?: Document }
- ) => string;
- serializeBlocks: (
- blocks: PartialBlock[],
- options: { document?: Document }
- ) => string;
-}
-
export const createInternalHTMLSerializer = <
BSchema extends BlockSchema,
I extends InlineContentSchema,
@@ -46,49 +19,16 @@ export const createInternalHTMLSerializer = <
>(
schema: Schema,
editor: BlockNoteEditor
-): InternalHTMLSerializer => {
- // TODO: maybe cache this serializer (default prosemirror serializer is cached)?
- const serializer = new DOMSerializer(
- DOMSerializer.nodesFromSchema(schema),
- DOMSerializer.marksFromSchema(schema)
- ) as DOMSerializer & {
- serializeNodeInner: (
- node: Node,
- options: { document?: Document }
- ) => HTMLElement;
+) => {
+ const serializer = DOMSerializer.fromSchema(schema);
+
+ return {
serializeBlocks: (
blocks: PartialBlock[],
options: { document?: Document }
- ) => string;
- serializeProseMirrorFragment: (
- fragment: Fragment,
- options?: { document?: Document | undefined } | undefined,
- target?: HTMLElement | DocumentFragment | undefined
- ) => string;
+ ) => {
+ return serializeBlocks(editor, blocks, serializer, false, options)
+ .outerHTML;
+ },
};
-
- serializer.serializeNodeInner = (
- node: Node,
- options: { document?: Document }
- ) => serializeNodeInner(node, options, serializer, editor, false);
-
- serializer.serializeProseMirrorFragment = (fragment: Fragment, options) =>
- serializeProseMirrorFragment(fragment, serializer, options);
-
- serializer.serializeBlocks = (
- blocks: PartialBlock[],
- options
- ) => {
- const nodes = blocks.map((block) =>
- blockToNode(block, schema, editor.schema.styleSchema)
- );
- const blockGroup = schema.nodes["blockGroup"].create(null, nodes);
-
- return serializer.serializeProseMirrorFragment(
- Fragment.from(blockGroup),
- options
- );
- };
-
- return serializer;
};
diff --git a/packages/core/src/api/exporters/html/util/sharedHTMLConversion.ts b/packages/core/src/api/exporters/html/util/sharedHTMLConversion.ts
index 3e0c6f9cf2..7fe568ba2d 100644
--- a/packages/core/src/api/exporters/html/util/sharedHTMLConversion.ts
+++ b/packages/core/src/api/exporters/html/util/sharedHTMLConversion.ts
@@ -1,130 +1,159 @@
-import { DOMSerializer, Fragment, Node } from "prosemirror-model";
+import { DOMSerializer, Fragment } from "prosemirror-model";
import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor";
+
+import { PartialBlock } from "../../../../blocks/defaultBlocks";
import {
BlockSchema,
InlineContentSchema,
StyleSchema,
} from "../../../../schema";
-import { nodeToBlock } from "../../../nodeConversions/nodeConversions";
+import { UnreachableCaseError } from "../../../../util/typescript";
+import {
+ inlineContentToNodes,
+ tableContentToNodes,
+} from "../../../nodeConversions/nodeConversions";
+
+export function serializeInlineContent<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(
+ editor: BlockNoteEditor,
+ blockContent: PartialBlock["content"],
+ serializer: DOMSerializer,
+ _toExternalHTML: boolean, // TODO, externalHTML for IC
+ 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,
+ editor.schema.styleSchema
+ );
+ } else if (Array.isArray(blockContent)) {
+ nodes = inlineContentToNodes(
+ blockContent,
+ editor.pmSchema,
+ editor.schema.styleSchema
+ );
+ } else if (blockContent.type === "tableContent") {
+ nodes = tableContentToNodes(
+ blockContent,
+ editor.pmSchema,
+ editor.schema.styleSchema
+ );
+ } 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);
-function doc(options: { document?: Document }) {
- return options.document || window.document;
+ return dom;
}
-// Used to implement `serializeNodeInner` for the `internalHTMLSerializer` and
-// `externalHTMLExporter`. Changes how the content of `blockContainer` nodes is
-// serialized vs the default `DOMSerializer` implementation. For the
-// `blockContent` node, the `toInternalHTML` or `toExternalHTML` function of its
-// corresponding block is used for serialization instead of the node's
-// `renderHTML` method.
-export const serializeNodeInner = <
+function serializeBlock<
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema
>(
- node: Node,
- options: { document?: Document },
- serializer: DOMSerializer,
editor: BlockNoteEditor,
- toExternalHTML: boolean
-) => {
- if (!serializer.nodes[node.type.name]) {
- throw new Error("Serializer is missing a node type: " + node.type.name);
+ block: PartialBlock,
+ serializer: DOMSerializer,
+ toExternalHTML: boolean,
+ 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
+ )) {
+ (props as any)[name] = spec.default;
+ }
}
- const { dom, contentDOM } = DOMSerializer.renderSpec(
- doc(options),
- serializer.nodes[node.type.name](node)
- );
+ const bc = BC_NODE.spec?.toDOM?.(
+ BC_NODE.create({
+ id: block.id,
+ ...props,
+ })
+ ) as {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
- if (contentDOM) {
- if (node.isLeaf) {
- throw new RangeError("Content hole not allowed in a leaf node spec");
- }
+ const impl = editor.blockImplementations[block.type as any].implementation;
+ const ret = toExternalHTML
+ ? impl.toExternalHTML({ ...block, props } as any, editor as any)
+ : impl.toInternalHTML({ ...block, props } as any, editor as any);
- // Handles converting `blockContainer` nodes to HTML.
- if (node.type.name === "blockContainer") {
- const blockContentNode =
- node.childCount > 0 &&
- node.firstChild!.type.spec.group === "blockContent"
- ? node.firstChild!
- : undefined;
- const blockGroupNode =
- node.childCount > 0 && node.lastChild!.type.spec.group === "blockGroup"
- ? node.lastChild!
- : undefined;
-
- // Converts `blockContent` node using the custom `blockSpec`'s
- // `toExternalHTML` or `toInternalHTML` function.
- // Note: While `blockContainer` nodes should always contain a
- // `blockContent` node according to the schema, PM Fragments don't always
- // conform to the schema. This is unintuitive but important as it occurs
- // when copying only nested blocks.
- if (blockContentNode !== undefined) {
- const impl =
- editor.blockImplementations[blockContentNode.type.name]
- .implementation;
- const toHTML = toExternalHTML
- ? impl.toExternalHTML
- : impl.toInternalHTML;
- const blockContent = toHTML(
- nodeToBlock(
- node,
- editor.schema.blockSchema,
- editor.schema.inlineContentSchema,
- editor.schema.styleSchema,
- editor.blockCache
- ),
- editor as any
- );
-
- // Converts inline nodes in the `blockContent` node's content to HTML
- // using their `renderHTML` methods.
- if (blockContent.contentDOM !== undefined) {
- if (node.isLeaf) {
- throw new RangeError(
- "Content hole not allowed in a leaf node spec"
- );
- }
-
- blockContent.contentDOM.appendChild(
- serializer.serializeFragment(blockContentNode.content, options)
- );
- }
-
- contentDOM.appendChild(blockContent.dom);
- }
-
- // Converts `blockGroup` node to HTML using its `renderHTML` method.
- if (blockGroupNode !== undefined) {
- serializer.serializeFragment(
- Fragment.from(blockGroupNode),
- options,
- contentDOM
- );
- }
- } else {
- // Converts the node normally, i.e. using its `renderHTML method`.
- serializer.serializeFragment(node.content, options, contentDOM);
- }
+ if (ret.contentDOM && block.content) {
+ const ic = serializeInlineContent(
+ editor,
+ block.content as any, // TODO
+ serializer,
+ toExternalHTML,
+ options
+ );
+ ret.contentDOM.appendChild(ic);
}
- return dom as HTMLElement;
-};
+ bc.contentDOM?.appendChild(ret.dom);
+
+ if (block.children && block.children.length > 0) {
+ bc.contentDOM?.appendChild(
+ serializeBlocks(
+ editor,
+ block.children,
+ serializer,
+ toExternalHTML,
+ options
+ )
+ );
+ }
+ return bc.dom;
+}
-// Used to implement `serializeProseMirrorFragment` for the
-// `internalHTMLSerializer` and `externalHTMLExporter`. Does basically the same
-// thing as `serializer.serializeFragment`, but takes fewer arguments and
-// returns a string instead, to make it easier to use.
-export const serializeProseMirrorFragment = (
- fragment: Fragment,
+export const serializeBlocks = <
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(
+ editor: BlockNoteEditor,
+ blocks: PartialBlock[],
serializer: DOMSerializer,
+ toExternalHTML: boolean,
options?: { document?: Document }
) => {
- const internalHTML = serializer.serializeFragment(fragment, options);
- const parent = document.createElement("div");
- parent.appendChild(internalHTML);
+ const BG_NODE = editor.pmSchema.nodes["blockGroup"];
+
+ const bg = BG_NODE.spec!.toDOM!(BG_NODE.create({})) as {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
+
+ for (const block of blocks) {
+ const blockDOM = serializeBlock(
+ editor,
+ block,
+ serializer,
+ toExternalHTML,
+ options
+ );
+ bg.contentDOM!.appendChild(blockDOM);
+ }
- return parent.innerHTML;
+ return bg.dom;
};
diff --git a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts
new file mode 100644
index 0000000000..5b8518e233
--- /dev/null
+++ b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts
@@ -0,0 +1,60 @@
+import { Fragment } from "@tiptap/pm/model";
+import { BlockNoteSchema } from "../../editor/BlockNoteSchema";
+import {
+ BlockNoDefaults,
+ BlockSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../schema";
+import { nodeToBlock } from "./nodeConversions";
+
+/**
+ * Converts all Blocks within a fragment to BlockNote blocks.
+ */
+export function fragmentToBlocks<
+ B extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(fragment: Fragment, schema: BlockNoteSchema) {
+ // first convert selection to blocknote-style blocks, and then
+ // pass these to the exporter
+ const blocks: BlockNoDefaults[] = [];
+ fragment.descendants((node) => {
+ if (node.type.name === "blockContainer") {
+ if (node.firstChild?.type.name === "blockGroup") {
+ // selection started within a block group
+ // in this case the fragment starts with:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // instead of:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // so we don't need to serialize this block, just descend into the children of the blockGroup
+ return true;
+ }
+ blocks.push(
+ nodeToBlock(
+ node,
+ schema.blockSchema,
+ schema.inlineContentSchema,
+ schema.styleSchema
+ )
+ );
+ // don't descend into children, as they're already included in the block returned by nodeToBlock
+ return false;
+ }
+ return true;
+ });
+ return blocks;
+}
diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts
index 9d76b4e84c..81fcc44006 100644
--- a/packages/core/src/api/nodeConversions/nodeConversions.ts
+++ b/packages/core/src/api/nodeConversions/nodeConversions.ts
@@ -190,7 +190,7 @@ export function tableContentToNodes<
return rowNodes;
}
-function blockOrInlineContentToContentNode(
+export function blockOrInlineContentToContentNode(
block:
| PartialBlock
| PartialCustomInlineContentFromConfig,
@@ -267,7 +267,7 @@ export function blockToNode(
/**
* Converts an internal (prosemirror) table node contentto a BlockNote Tablecontent
*/
-function contentNodeToTableContent<
+export function contentNodeToTableContent<
I extends InlineContentSchema,
S extends StyleSchema
>(contentNode: Node, inlineContentSchema: I, styleSchema: S) {
diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
index bc7b310b5f..dfe29ac8f9 100644
--- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
+++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
@@ -1,12 +1,13 @@
import { PluginView } from "@tiptap/pm/state";
import { Node } from "prosemirror-model";
import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state";
-import { EditorView } from "prosemirror-view";
import * as pmView from "prosemirror-view";
+import { EditorView } from "prosemirror-view";
import { createExternalHTMLExporter } from "../../api/exporters/html/externalHTMLExporter";
import { cleanHTMLToMarkdown } from "../../api/exporters/markdown/markdownExporter";
import { getBlockInfoFromPos } from "../../api/getBlockInfoFromPos";
+import { fragmentToBlocks } from "../../api/nodeConversions/fragmentToBlocks";
import { Block } from "../../blocks/defaultBlocks";
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
import { UiElementPosition } from "../../extensions-shared/UiElementPosition";
@@ -227,21 +228,20 @@ function dragStart<
const selectedSlice = view.state.selection.content();
const schema = editor.pmSchema;
- const clipboardHML = (pmView as any).__serializeForClipboard(
+ const clipboardHTML = (pmView as any).__serializeForClipboard(
view,
selectedSlice
).dom.innerHTML;
const externalHTMLExporter = createExternalHTMLExporter(schema, editor);
- const externalHTML = externalHTMLExporter.exportProseMirrorFragment(
- selectedSlice.content,
- {}
- );
+
+ const blocks = fragmentToBlocks(selectedSlice.content, editor.schema);
+ const externalHTML = externalHTMLExporter.exportBlocks(blocks, {});
const plainText = cleanHTMLToMarkdown(externalHTML);
e.dataTransfer.clearData();
- e.dataTransfer.setData("blocknote/html", clipboardHML);
+ e.dataTransfer.setData("blocknote/html", clipboardHTML);
e.dataTransfer.setData("text/html", externalHTML);
e.dataTransfer.setData("text/plain", plainText);
e.dataTransfer.effectAllowed = "move";
diff --git a/packages/server-util/src/context/react/ReactServer.test.tsx b/packages/server-util/src/context/react/ReactServer.test.tsx
index d6613cf911..a250d4c7a3 100644
--- a/packages/server-util/src/context/react/ReactServer.test.tsx
+++ b/packages/server-util/src/context/react/ReactServer.test.tsx
@@ -58,6 +58,7 @@ describe("Test ServerBlockNoteEditor with React blocks", () => {
});
const html = await editor.blocksToFullHTML([
{
+ id: "1",
type: "simpleReactCustomParagraph",
content: "React Custom Paragraph",
},
@@ -77,6 +78,7 @@ describe("Test ServerBlockNoteEditor with React blocks", () => {
async () =>
editor.blocksToFullHTML([
{
+ id: "1",
type: "reactContextParagraph",
content: "React Context Paragraph",
},