diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts index 14df079029..72d92d285b 100644 --- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts +++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts @@ -11,7 +11,6 @@ import { InlineContentSchema, StyleSchema, } from "../../../schema/index.js"; -import { initializeESMDependencies } from "../../../util/esmDependencies.js"; import { createExternalHTMLExporter } from "../../exporters/html/externalHTMLExporter.js"; import { cleanHTMLToMarkdown } from "../../exporters/markdown/markdownExporter.js"; import { fragmentToBlocks } from "../../nodeConversions/fragmentToBlocks.js"; @@ -20,7 +19,7 @@ import { contentNodeToTableContent, } from "../../nodeConversions/nodeToBlock.js"; -async function fragmentToExternalHTML< +function fragmentToExternalHTML< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema @@ -61,7 +60,6 @@ async function fragmentToExternalHTML< let externalHTML: string; - await initializeESMDependencies(); const externalHTMLExporter = createExternalHTMLExporter( view.state.schema, editor @@ -82,9 +80,7 @@ async function fragmentToExternalHTML< editor.schema.styleSchema ); - externalHTML = externalHTMLExporter.exportInlineContent(ic as any, { - simplifyBlocks: false, - }); + externalHTML = externalHTMLExporter.exportInlineContent(ic as any, {}); } else if (isWithinBlockContent) { // first convert selection to blocknote-style inline content, and then // pass this to the exporter @@ -93,9 +89,7 @@ async function fragmentToExternalHTML< editor.schema.inlineContentSchema, editor.schema.styleSchema ); - externalHTML = externalHTMLExporter.exportInlineContent(ic, { - simplifyBlocks: false, - }); + externalHTML = externalHTMLExporter.exportInlineContent(ic, {}); } else { const blocks = fragmentToBlocks(selectedFragment, editor.schema); externalHTML = externalHTMLExporter.exportBlocks(blocks, {}); @@ -103,18 +97,18 @@ async function fragmentToExternalHTML< return externalHTML; } -export async function selectedFragmentToHTML< +export 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 @@ -138,7 +132,7 @@ export async function selectedFragmentToHTML< const selectedFragment = view.state.selection.content().content; - const externalHTML = await fragmentToExternalHTML( + const externalHTML = fragmentToExternalHTML( view, selectedFragment, editor @@ -162,16 +156,16 @@ const copyToClipboard = < event.preventDefault(); event.clipboardData!.clearData(); - (async () => { - const { clipboardHTML, externalHTML, markdown } = - await selectedFragmentToHTML(view, editor); + const { clipboardHTML, externalHTML, markdown } = selectedFragmentToHTML( + view, + editor + ); - // TODO: Writing to other MIME types not working in Safari for - // some reason. - event.clipboardData!.setData("blocknote/html", clipboardHTML); - event.clipboardData!.setData("text/html", externalHTML); - event.clipboardData!.setData("text/plain", markdown); - })(); + // TODO: Writing to other MIME types not working in Safari for + // some reason. + event.clipboardData!.setData("blocknote/html", clipboardHTML); + event.clipboardData!.setData("text/html", externalHTML); + event.clipboardData!.setData("text/plain", markdown); }; export const createCopyToClipboardExtension = < @@ -229,16 +223,15 @@ export const createCopyToClipboardExtension = < event.preventDefault(); event.dataTransfer!.clearData(); - (async () => { - const { clipboardHTML, externalHTML, markdown } = - await selectedFragmentToHTML(view, editor); + const { clipboardHTML, externalHTML, markdown } = + selectedFragmentToHTML(view, editor); + + // TODO: Writing to other MIME types not working in Safari for + // some reason. + event.dataTransfer!.setData("blocknote/html", clipboardHTML); + event.dataTransfer!.setData("text/html", externalHTML); + event.dataTransfer!.setData("text/plain", markdown); - // TODO: Writing to other MIME types not working in Safari for - // some reason. - event.dataTransfer!.setData("blocknote/html", clipboardHTML); - event.dataTransfer!.setData("text/html", externalHTML); - event.dataTransfer!.setData("text/plain", markdown); - })(); // Prevent default PM handler to be called return true; }, diff --git a/packages/core/src/api/exporters/html/__snapshots__/complex/misc/external.html b/packages/core/src/api/exporters/html/__snapshots__/complex/misc/external.html index 859d5c7a7e..eee2368524 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/complex/misc/external.html +++ b/packages/core/src/api/exporters/html/__snapshots__/complex/misc/external.html @@ -1 +1 @@ -

Heading 2

Paragraph

\ No newline at end of file +

Heading 2

Paragraph

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/lists/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/lists/basic/external.html index a261871741..ed9e33e52a 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/lists/basic/external.html +++ b/packages/core/src/api/exporters/html/__snapshots__/lists/basic/external.html @@ -1 +1 @@ -
  • Bullet List Item 1

  • Bullet List Item 2

  1. Numbered List Item 1

  2. Numbered List Item 2

  • Check List Item 1

  • Check List Item 2

\ No newline at end of file +
  • Bullet List Item 1

  • Bullet List Item 2

  1. Numbered List Item 1

  2. Numbered List Item 2

  • Check List Item 1

  • Check List Item 2

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/lists/nested/external.html b/packages/core/src/api/exporters/html/__snapshots__/lists/nested/external.html index 9cfe048b34..73877536f1 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/lists/nested/external.html +++ b/packages/core/src/api/exporters/html/__snapshots__/lists/nested/external.html @@ -1 +1 @@ -
  • Bullet List Item 1

  • Bullet List Item 2

    1. Numbered List Item 1

    2. Numbered List Item 2

      • Check List Item 1

      • Check List Item 2

\ No newline at end of file +
  • Bullet List Item 1

  • Bullet List Item 2

    1. Numbered List Item 1

    2. Numbered List Item 2

      • Check List Item 1

      • Check List Item 2

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/externalHTMLExporter.ts b/packages/core/src/api/exporters/html/externalHTMLExporter.ts index 2e0bcf3ba3..c0fa524804 100644 --- a/packages/core/src/api/exporters/html/externalHTMLExporter.ts +++ b/packages/core/src/api/exporters/html/externalHTMLExporter.ts @@ -8,12 +8,10 @@ import { InlineContentSchema, StyleSchema, } from "../../../schema/index.js"; -import { esmDependencies } from "../../../util/esmDependencies.js"; import { - serializeBlocks, - serializeInlineContent, -} from "./util/sharedHTMLConversion.js"; -import { simplifyBlocks } from "./util/simplifyBlocksRehypePlugin.js"; + serializeBlocksExternalHTML, + serializeInlineContentExternalHTML, +} from "./util/serializeBlocksExternalHTML.js"; // Used to export BlockNote blocks and ProseMirror nodes to HTML for use outside // the editor. Blocks are exported using the `toExternalHTML` method in their @@ -39,14 +37,6 @@ export const createExternalHTMLExporter = < schema: Schema, editor: BlockNoteEditor ) => { - const deps = esmDependencies; - - if (!deps) { - throw new Error( - "External HTML exporter requires ESM dependencies to be initialized" - ); - } - const serializer = DOMSerializer.fromSchema(schema); return { @@ -54,49 +44,27 @@ export const createExternalHTMLExporter = < blocks: PartialBlock[], options: { document?: Document } ) => { - const html = serializeBlocks( + const html = serializeBlocksExternalHTML( editor, blocks, serializer, - true, + new Set(["numberedListItem"]), + new Set(["bulletListItem", "checkListItem"]), options - ).outerHTML; - - // 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); - - return externalHTML.value as string; + ); + const div = document.createElement("div"); + div.append(html); + return div.innerHTML; }, exportInlineContent: ( inlineContent: InlineContent[], - options: { simplifyBlocks: boolean; document?: Document } + options: { document?: Document } ) => { - const domFragment = serializeInlineContent( + const domFragment = serializeInlineContentExternalHTML( editor, inlineContent as any, serializer, - true, options ); diff --git a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts index 43f4a7a3b6..f8b1888091 100644 --- a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts +++ b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts @@ -6,7 +6,7 @@ import { InlineContentSchema, StyleSchema, } from "../../../schema/index.js"; -import { serializeBlocks } from "./util/sharedHTMLConversion.js"; +import { serializeBlocksInternalHTML } from "./util/serializeBlocksInternalHTML.js"; // Used to serialize BlockNote blocks and ProseMirror nodes to HTML without // losing data. Blocks are exported using the `toInternalHTML` method in their // `blockSpec`. @@ -31,8 +31,13 @@ export const createInternalHTMLSerializer = < blocks: PartialBlock[], options: { document?: Document } ) => { - return serializeBlocks(editor, blocks, serializer, false, options) - .outerHTML; + return serializeBlocksInternalHTML( + editor, + blocks, + serializer, + false, + options + ).outerHTML; }, }; }; diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts new file mode 100644 index 0000000000..65256a67fa --- /dev/null +++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts @@ -0,0 +1,263 @@ +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"; + +function addAttributesAndRemoveClasses(element: HTMLElement) { + // Removes all BlockNote specific class names. + const className = + [...element.classList].filter( + (className) => !className.startsWith("bn-") + ) || []; + + if (className.length > 0) { + element.className = className.join(" "); + } else { + element.removeAttribute("class"); + } +} + +export function serializeInlineContentExternalHTML< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + editor: BlockNoteEditor, + blockContent: PartialBlock["content"], + serializer: DOMSerializer, + 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); + + if (dom.nodeType === 1 /* Node.ELEMENT_NODE */) { + addAttributesAndRemoveClasses(dom as HTMLElement); + } + + return dom; +} + +/** + * TODO: there's still quite some logic that handles getting and filtering properties, + * we should make sure the `toExternalHTML` methods of default blocks actually handle this, + * instead of the serializer. + */ +function serializeBlock< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + fragment: DocumentFragment, + editor: BlockNoteEditor, + block: PartialBlock, + serializer: DOMSerializer, + orderedListItemBlockTypes: Set, + unorderedListItemBlockTypes: Set, + options?: { document?: Document } +) { + const doc = 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 bc = BC_NODE.spec?.toDOM?.( + BC_NODE.create({ + id: block.id, + ...props, + }) + ) as { + dom: HTMLElement; + contentDOM?: HTMLElement; + }; + + // the container node is just used as a workaround to get some block-level attributes. + // we should change toExternalHTML so that this is not necessary + const attrs = [...bc.dom.attributes]; + + const ret = editor.blockImplementations[ + block.type as any + ].implementation.toExternalHTML({ ...block, props } as any, editor as any); + + const elementFragment = doc.createDocumentFragment(); + if (ret.dom.classList.contains("bn-block-content")) { + const blockContentDataAttributes = [...attrs, ...ret.dom.attributes].filter( + (attr) => + attr.name.startsWith("data") && + attr.name !== "data-content-type" && + attr.name !== "data-file-block" && + attr.name !== "data-node-view-wrapper" && + attr.name !== "data-node-type" && + attr.name !== "data-id" && + attr.name !== "data-index" && + attr.name !== "data-editable" + ); + + // ret.dom = ret.dom.firstChild! as any; + for (const attr of blockContentDataAttributes) { + (ret.dom.firstChild! as HTMLElement).setAttribute(attr.name, attr.value); + } + + addAttributesAndRemoveClasses(ret.dom.firstChild! as HTMLElement); + elementFragment.append(...ret.dom.childNodes); + } else { + elementFragment.append(ret.dom); + } + + if (ret.contentDOM && block.content) { + const ic = serializeInlineContentExternalHTML( + editor, + block.content as any, // TODO + serializer, + options + ); + + ret.contentDOM.appendChild(ic); + } + + let listType = undefined; + if (orderedListItemBlockTypes.has(block.type!)) { + listType = "OL"; + } else if (unorderedListItemBlockTypes.has(block.type!)) { + listType = "UL"; + } + + if (listType) { + if (fragment.lastChild?.nodeName !== listType) { + const list = doc.createElement(listType); + fragment.append(list); + } + const li = doc.createElement("li"); + li.append(elementFragment); + fragment.lastChild!.appendChild(li); + } else { + fragment.append(elementFragment); + } + + if (block.children && block.children.length > 0) { + const childFragment = doc.createDocumentFragment(); + serializeBlocksToFragment( + childFragment, + editor, + block.children, + serializer, + orderedListItemBlockTypes, + unorderedListItemBlockTypes, + options + ); + if ( + fragment.lastChild?.nodeName === "UL" || + fragment.lastChild?.nodeName === "OL" + ) { + // add nested lists to the last list item + while ( + childFragment.firstChild?.nodeName === "UL" || + childFragment.firstChild?.nodeName === "OL" + ) { + fragment.lastChild!.lastChild!.appendChild(childFragment.firstChild!); + } + } + + fragment.append(childFragment); + } +} + +const serializeBlocksToFragment = < + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + fragment: DocumentFragment, + editor: BlockNoteEditor, + blocks: PartialBlock[], + serializer: DOMSerializer, + orderedListItemBlockTypes: Set, + unorderedListItemBlockTypes: Set, + options?: { document?: Document } +) => { + for (const block of blocks) { + serializeBlock( + fragment, + editor, + block, + serializer, + orderedListItemBlockTypes, + unorderedListItemBlockTypes, + options + ); + } +}; + +export const serializeBlocksExternalHTML = < + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + editor: BlockNoteEditor, + blocks: PartialBlock[], + serializer: DOMSerializer, + orderedListItemBlockTypes: Set, + unorderedListItemBlockTypes: Set, + options?: { document?: Document } +) => { + const doc = options?.document ?? document; + const fragment = doc.createDocumentFragment(); + + serializeBlocksToFragment( + fragment, + editor, + blocks, + serializer, + orderedListItemBlockTypes, + unorderedListItemBlockTypes, + options + ); + return fragment; +}; diff --git a/packages/core/src/api/exporters/html/util/sharedHTMLConversion.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts similarity index 95% rename from packages/core/src/api/exporters/html/util/sharedHTMLConversion.ts rename to packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts index ac8ff29f14..1f6867a231 100644 --- a/packages/core/src/api/exporters/html/util/sharedHTMLConversion.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts @@ -13,7 +13,7 @@ import { tableContentToNodes, } from "../../../nodeConversions/blockToNode.js"; -export function serializeInlineContent< +export function serializeInlineContentInternalHTML< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema @@ -99,7 +99,7 @@ function serializeBlock< : impl.toInternalHTML({ ...block, props } as any, editor as any); if (ret.contentDOM && block.content) { - const ic = serializeInlineContent( + const ic = serializeInlineContentInternalHTML( editor, block.content as any, // TODO serializer, @@ -113,7 +113,7 @@ function serializeBlock< if (block.children && block.children.length > 0) { bc.contentDOM?.appendChild( - serializeBlocks( + serializeBlocksInternalHTML( editor, block.children, serializer, @@ -125,7 +125,7 @@ function serializeBlock< return bc.dom; } -export const serializeBlocks = < +export const serializeBlocksInternalHTML = < BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema diff --git a/packages/core/src/api/exporters/html/util/simplifyBlocksRehypePlugin.ts b/packages/core/src/api/exporters/html/util/simplifyBlocksRehypePlugin.ts deleted file mode 100644 index ba27ed69fd..0000000000 --- a/packages/core/src/api/exporters/html/util/simplifyBlocksRehypePlugin.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { Element as HASTElement, Parent as HASTParent } from "hast"; -import { esmDependencies } from "../../../../util/esmDependencies.js"; - -type SimplifyBlocksOptions = { - orderedListItemBlockTypes: Set; - unorderedListItemBlockTypes: Set; -}; - -function addAttributesAndRemoveClasses( - element: HASTElement, - attributes: Record -) { - // Removes all BlockNote specific class names. - const className = - ((element.properties?.className as string[]) || []).filter( - (className) => !className.startsWith("bn-") - ) || []; - // Adds all block props as data attributes. - element.properties = { - ...element.properties, - ...attributes, - className: className.length > 0 ? className : undefined, - }; -} - -/** - * Rehype plugin which converts the HTML output string rendered by BlockNote into a simplified structure which better - * follows HTML standards. It does several things: - * - Removes all block related div elements, leaving only the actual content inside the block. - * - Lifts nested blocks to a higher level for all block types that don't represent list items. - * - Wraps blocks which represent list items in corresponding ul/ol HTML elements and restructures them to comply - * with HTML list structure. - * @param options Options for specifying which block types represent ordered and unordered list items. - */ -export function simplifyBlocks(options: SimplifyBlocksOptions) { - const deps = esmDependencies; - - if (!deps) { - throw new Error( - "simplifyBlocks requires ESM dependencies to be initialized" - ); - } - - const listItemBlockTypes = new Set([ - ...options.orderedListItemBlockTypes, - ...options.unorderedListItemBlockTypes, - ]); - - const simplifyBlocksHelper = (tree: HASTParent) => { - // Checks whether blocks in the tree are wrapped by a parent `blockGroup` - // element, in which case the `blockGroup`'s children are lifted out, and it - // is removed. - if ( - tree.children.length === 1 && - (tree.children[0] as HASTElement).properties?.["dataNodeType"] === - "blockGroup" - ) { - const blockGroup = tree.children[0] as HASTElement; - tree.children.pop(); - tree.children.push(...blockGroup.children); - } - - let numChildElements = tree.children.length; - let activeList: HASTElement | undefined; - - for (let i = 0; i < numChildElements; i++) { - const blockOuter = tree.children[i] as HASTElement; - const blockContainer = blockOuter.children[0] as HASTElement; - const blockContent = blockContainer.children.find((child) => { - const properties = (child as HASTElement).properties; - const classNames = properties?.["className"] as string[] | undefined; - - return classNames?.includes("bn-block-content"); - }) as HASTElement | undefined; - const blockGroup = blockContainer.children.find((child) => { - const properties = (child as HASTElement).properties; - const classNames = properties?.["className"] as string[] | undefined; - - return classNames?.includes("bn-block-group"); - }) as HASTElement | undefined; - - // Saves the data attributes of the block container, excluding the block - // ID and node type as we're removing the block structure. This means that - // only attributes for the block's props are saved. - const blockContainerDataAttributes = Object.fromEntries( - Object.entries(blockContainer.properties || {}).filter( - ([key]) => - key.startsWith("data") && key !== "dataId" && key !== "dataNodeType" - ) - ); - // Saves the data attributes of the block content, excluding the block - // content type as we're removing the block structure. This means that - // only attributes for the block's props are saved. - const blockContentDataAttributes = Object.fromEntries( - Object.entries(blockContent?.properties || {}).filter( - ([key]) => - key.startsWith("data") && - key !== "dataContentType" && - key !== "dataFileBlock" && - key !== "dataNodeViewWrapper" && - key !== "dataEditable" - ) - ); - // All the block's props as data attributes. - const blockPropsDataAttributes = { - ...blockContainerDataAttributes, - ...blockContentDataAttributes, - }; - - // When the selection starts in a nested block, the Fragment from it omits - // the `blockContent` node of the parent `blockContainer` if it's not also - // included in the selection. This is because ProseMirror preserves the - // nesting hierarchy of the nested nodes, even if their ancestors aren't - // fully selected. In this case, we just lift the child `blockContainer` - // nodes up. - // NOTE: This only happens for the first `blockContainer`, since to get to - // any nested blocks later in the document, the selection must also - // include their parents. - if (!blockContent) { - tree.children.splice(i, 1, ...blockGroup!.children); - simplifyBlocksHelper(tree); - - return; - } - - const isListItemBlock = listItemBlockTypes.has( - blockContent.properties!["dataContentType"] as string - ); - - const listItemBlockType = isListItemBlock - ? options.orderedListItemBlockTypes.has( - blockContent.properties!["dataContentType"] as string - ) - ? "ol" - : "ul" - : null; - - // Plugin runs recursively to process nested blocks. - if (blockGroup) { - simplifyBlocksHelper(blockGroup); - } - - // Checks that there is an active list, but the block can't be added to it as it's of a different type. - if (activeList && activeList.tagName !== listItemBlockType) { - // Blocks that were copied into the list are removed and the list is inserted in their place. - tree.children.splice( - i - activeList.children.length, - activeList.children.length, - activeList - ); - - // Updates the current index and number of child elements. - const numElementsRemoved = activeList.children.length - 1; - i -= numElementsRemoved; - numChildElements -= numElementsRemoved; - - activeList = undefined; - } - - // Checks if the block represents a list item. - if (isListItemBlock) { - // Checks if a list isn't already active. We don't have to check if the block and the list are of the same - // type as this was already done earlier. - if (!activeList) { - // Creates a new list element to represent an active list. - activeList = deps.hastUtilFromDom.fromDom( - document.createElement(listItemBlockType!) - ) as HASTElement; - } - - // Creates a new list item element to represent the block. - const listItemElement = deps.hastUtilFromDom.fromDom( - document.createElement("li") - ) as HASTElement; - - // Adds only the content inside the block to the active list. - listItemElement.children.push(...blockContent.children); - // Nested blocks have already been processed in the recursive function call, so the resulting elements are - // also added to the active list. - if (blockGroup) { - listItemElement.children.push(...blockGroup.children); - } - - // Adds the list item representing the block to the active list. - activeList.children.push(listItemElement); - } else if (blockGroup) { - // Lifts all children out of the current block, as only list items should allow nesting. - tree.children.splice(i + 1, 0, ...blockGroup.children); - // Replaces the block with only the content inside it. - const content = blockContent.children[0] as HASTElement; - addAttributesAndRemoveClasses(content, blockPropsDataAttributes); - tree.children[i] = content; - - // Updates the current index and number of child elements. - const numElementsAdded = blockGroup.children.length; - i += numElementsAdded; - numChildElements += numElementsAdded; - } else { - // Replaces the block with only the content inside it. - const content = blockContent.children[0] as HASTElement; - addAttributesAndRemoveClasses(content, blockPropsDataAttributes); - tree.children[i] = content; - } - } - - // Since the active list is only inserted after encountering a block which can't be added to it, there are cases - // where it remains un-inserted after processing all blocks, which are handled here. - if (activeList) { - tree.children.splice( - numChildElements - activeList.children.length, - activeList.children.length, - activeList - ); - } - }; - - return simplifyBlocksHelper; -} diff --git a/packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts b/packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts index 51964b1d03..ee2ae06e30 100644 --- a/packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts +++ b/packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts @@ -11,7 +11,7 @@ export function addSpacesToCheckboxes() { if (!deps) { throw new Error( - "simplifyBlocks requires ESM dependencies to be initialized" + "addSpacesToCheckboxes requires ESM dependencies to be initialized" ); } diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 0b16a2cb49..e93111b215 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -73,7 +73,6 @@ import { nodeToBlock } from "../api/nodeConversions/nodeToBlock.js"; import { NodeSelectionKeyboardPlugin } from "../extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.js"; import { PreviousBlockTypePlugin } from "../extensions/PreviousBlockType/PreviousBlockTypePlugin.js"; import "../style.css"; -import { initializeESMDependencies } from "../util/esmDependencies.js"; export type BlockNoteEditorOptions< BSchema extends BlockSchema, @@ -1028,7 +1027,6 @@ export class BlockNoteEditor< public async blocksToHTMLLossy( blocks: PartialBlock[] = this.document ): Promise { - await initializeESMDependencies(); const exporter = createExternalHTMLExporter(this.pmSchema, this); return exporter.exportBlocks(blocks, {}); } diff --git a/packages/core/src/schema/blocks/createSpec.ts b/packages/core/src/schema/blocks/createSpec.ts index 4c2bfb9bca..209e604085 100644 --- a/packages/core/src/schema/blocks/createSpec.ts +++ b/packages/core/src/schema/blocks/createSpec.ts @@ -231,6 +231,8 @@ export function createBlockSpec< blockContentDOMAttributes ); }, + // TODO: this should not have wrapInBlockStructure and generally be a lot simpler + // post-processing in externalHTMLExporter should not be necessary toExternalHTML: (block, editor) => { const blockContentDOMAttributes = node.options.domAttributes?.blockContent || {}; @@ -242,7 +244,6 @@ export function createBlockSpec< if (output === undefined) { output = blockImplementation.render(block as any, editor as any); } - return wrapInBlockStructure( output, block.type, diff --git a/packages/react/src/test/htmlConversion.test.tsx b/packages/react/src/test/htmlConversion.test.tsx index d2481e2799..057dcd5766 100644 --- a/packages/react/src/test/htmlConversion.test.tsx +++ b/packages/react/src/test/htmlConversion.test.tsx @@ -9,7 +9,6 @@ import { addIdsToBlocks, createExternalHTMLExporter, createInternalHTMLSerializer, - initializeESMDependencies, partialBlocksToBlocksForTesting, } from "@blocknote/core"; @@ -56,7 +55,6 @@ async function convertToHTMLAndCompareSnapshots< expect(parsed).toStrictEqual(fullBlocks); // Create the "external" HTML, which is a cleaned up HTML representation, but lossy - await initializeESMDependencies(); const exporter = createExternalHTMLExporter(editor.pmSchema, editor); const externalHTML = exporter.exportBlocks(blocks, {}); const externalHTMLSnapshotPath = diff --git a/packages/server-util/src/context/ServerBlockNoteEditor.ts b/packages/server-util/src/context/ServerBlockNoteEditor.ts index 31d8763139..8365623ec6 100644 --- a/packages/server-util/src/context/ServerBlockNoteEditor.ts +++ b/packages/server-util/src/context/ServerBlockNoteEditor.ts @@ -13,7 +13,6 @@ import { blocksToMarkdown, createExternalHTMLExporter, createInternalHTMLSerializer, - initializeESMDependencies, nodeToBlock, } from "@blocknote/core"; @@ -226,7 +225,6 @@ export class ServerBlockNoteEditor< blocks: PartialBlock[] ): Promise { return this._withJSDOM(async () => { - await initializeESMDependencies(); const exporter = createExternalHTMLExporter( this.editor.pmSchema, this.editor diff --git a/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap b/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap index 4ce7b821a9..5da0509a79 100644 --- a/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap +++ b/packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap @@ -2,7 +2,7 @@ exports[`Test ServerBlockNoteEditor > converts to HTML (blocksToFullHTML) 1`] = `"

Heading 2

Paragraph

list item

"`; -exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 1`] = `"

Heading 2

Paragraph

  • list item

"`; +exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 1`] = `"

Heading 2

Paragraph

  • list item

"`; exports[`Test ServerBlockNoteEditor > converts to and from HTML (blocksToHTMLLossy) 2`] = ` [